- README: logo, badges (Rust, WebAssembly, Claude Code, MIT), intro addressed to the master, features with screenshots, the household, how to play, licences and credits - INSTALL.md: server, PWA, building, tests, savegame service and API, languages, how to refresh generated files - BESTIARY.md generated from the game's data (tools/make_bestiary.py) with exported sprites and icons (docs/img, test export_doc_images) - play/DungeonCraft.html: the whole game in one file to download and play offline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
7.0 KiB
Python
131 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Writes BESTIARY.md from the game's own data: the bestiary texts in lang/en.lang, the unit
|
|
stats in src/game.rs and the personalities in src/personality.rs. Pictures come from
|
|
docs/img (refresh them with `cargo test --release export_doc_images -- --ignored`).
|
|
|
|
usage: python3 tools/make_bestiary.py
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
root = pathlib.Path(__file__).resolve().parent.parent
|
|
lang = {}
|
|
for line in (root / "lang/en.lang").read_text().splitlines():
|
|
if " = " in line and not line.startswith("#"):
|
|
k, v = line.split(" = ", 1)
|
|
lang[k.strip()] = v.strip()
|
|
|
|
game = (root / "src/game.rs").read_text()
|
|
stats = {}
|
|
for m in re.finditer(r'EK::(\w+) => s\("unit\.(\w+)", ([\d.]+), ([\d.]+), ([\d.]+), ([\d.]+), ([\d.]+), (\d+), (\d+)\)', game):
|
|
kind, uid, hp, atk, rng, spd, itv, wage, bounty = m.groups()
|
|
stats[kind] = dict(id=uid, hp=float(hp), atk=float(atk), range=float(rng), speed=float(spd), interval=float(itv), wage=int(wage), bounty=int(bounty))
|
|
|
|
pers = {}
|
|
src = (root / "src/personality.rs").read_text()
|
|
for m in re.finditer(r'EK::(\w+) => p\(Kind::(\w+), &\[([^\]]*)\], (?:Some\(\((\d+), "(\w+)"\)\)|None)\)', src):
|
|
kind, likes, hates, lv, ab = m.groups()
|
|
pers[kind] = dict(likes=likes, hates=[h.replace("EK::", "").strip() for h in hates.split(",") if h.strip()], lv=lv, ab=ab)
|
|
|
|
room_key = {"Treasury": "treasury", "Lair": "lair", "Farm": "farm", "Training": "training", "Library": "library", "Torture": "torture", "Temple": "temple", "Aerie": "aerie", "Forge": "forge", "Prison": "prison", "Graveyard": "graveyard", "GuardPost": "guardpost", "Arena": "arena", "Altar": "altar"}
|
|
|
|
|
|
def para(text):
|
|
return "\n\n".join(p.strip() for p in text.split(" | "))
|
|
|
|
|
|
def unit(kind, human=False):
|
|
s = stats[kind]
|
|
uid = s["id"]
|
|
name = lang.get(f"unit.{uid}", uid)
|
|
out = [f"### {name}", ""]
|
|
out.append(f'<img src="docs/img/units/{uid}_1.png" width="72" alt="{name}, level 1"> '
|
|
f'<img src="docs/img/units/{uid}_5.png" width="72" alt="{name}, level 5"> '
|
|
f'<img src="docs/img/units/{uid}_10.png" width="72" alt="{name}, level 10">')
|
|
out.append("")
|
|
out.append("*Level 1, 5 and 10 - the gear grows with the rank.*")
|
|
out.append("")
|
|
dps = s["atk"] / s["interval"]
|
|
reach = "close combat" if s["range"] <= 1.5 else f"{s['range']:.1f} tiles"
|
|
row = f"| {s['hp']:.0f} | {s['atk']:.0f} every {s['interval']:.1f} s ({dps:.0f}/s) | {reach} | {s['speed']:.1f} tiles/s |"
|
|
if human:
|
|
out += ["| Health | Attack | Range | Speed | Bounty |", "|---|---|---|---|---|", row + f" {s['bounty']} gold |"]
|
|
else:
|
|
out += ["| Health | Attack | Range | Speed | Wage |", "|---|---|---|---|---|", row + f" {s['wage']} gold |"]
|
|
out.append("")
|
|
if not human:
|
|
lure = lang.get(f"best.{uid}.lure")
|
|
if lure:
|
|
out.append(f"**Comes for:** {lure} ")
|
|
p = pers.get(kind)
|
|
if p:
|
|
out.append(f"**Loves:** {lang.get('room.' + room_key[p['likes']], p['likes'])} ")
|
|
if p["hates"]:
|
|
out.append(f"**Cannot stand:** {', '.join(lang.get('unit.' + stats[h]['id'], h) for h in p['hates'] if h in stats)} ")
|
|
if p["ab"]:
|
|
out.append(f"**From level {p['lv']}:** {lang.get('ability.' + p['ab'] + '.name')} - {lang.get('ability.' + p['ab'] + '.desc')}")
|
|
out.append("")
|
|
out.append(para(lang.get(f"best.{uid}.text", "")))
|
|
out.append("")
|
|
return out
|
|
|
|
|
|
def building(key, icon, title=None):
|
|
name = title or lang.get(f"room.{key}") or lang.get(f"tool.{key}.name") or lang.get(f"trap.{key}") or key
|
|
text = lang.get(f"best.b.{key}.text", "")
|
|
desc = lang.get(f"tool.{key}.desc", "")
|
|
return [f'| <img src="docs/img/icons/{icon}.png" width="40"> | **{name}** | {desc} *{text}* |']
|
|
|
|
|
|
creatures = ["Worker", "Kobold", "Spider", "Mage", "Ogre", "Jailer", "Priestess", "Chainbreaker", "Cogling", "Revenant", "Bat", "Gargoyle", "Owl", "Wyrm"]
|
|
humans = ["Knight", "Archer", "Priest", "Dwarf", "Fairy", "BattleMage", "Giant", "Paladin"]
|
|
|
|
md = []
|
|
md += [
|
|
"# Crookfinger's Bestiary",
|
|
"",
|
|
'<p align="center"><img src="docs/screenshots/bestiary.png" width="720" alt="The bestiary in the game"></p>',
|
|
"",
|
|
"> *Ah, master, you want to know who lives in your dungeon? Sit down. Mind the bones. I have written everything down -",
|
|
"> lovingly about our own, and with a peg on my nose about the others.*",
|
|
"> *- Crookfinger, imp, advisor, keeper of the tea*",
|
|
"",
|
|
"Every creature gains experience by fighting, training, studying and sparring. With every level it grows tougher",
|
|
"(+15% health, +12% attack per level), its badge changes (bronze chevrons, silver bars, golden stars) and its gear grows",
|
|
"(shoulder pieces, helmet, cape, golden armour with a crown and a halo). Its attacks look bigger, too.",
|
|
"",
|
|
"- [Your creatures](#your-creatures)",
|
|
"- [The humans](#the-humans)",
|
|
"- [The neutrals](#the-neutrals)",
|
|
"- [Rooms of the dungeon](#rooms-of-the-dungeon)",
|
|
"- [Towers, traps and doors](#towers-traps-and-doors)",
|
|
"- [Spells](#spells)",
|
|
"",
|
|
"## Your creatures",
|
|
"",
|
|
]
|
|
for k in creatures:
|
|
md += unit(k)
|
|
md += ["## The humans", "", "> *Hold your nose, master. Here come the heroes.*", ""]
|
|
for k in humans:
|
|
md += unit(k, human=True)
|
|
md += ["## The neutrals", "", f"### {lang['best.cavedwarf.name']}", "", '<img src="docs/img/units/dwarf_5.png" width="72" alt="cave dwarf">', "", para(lang["best.cavedwarf.text"]), ""]
|
|
md += ["## Rooms of the dungeon", "", "| | Room | |", "|---|---|---|"]
|
|
for key in ["treasury", "lair", "farm", "training", "library", "torture", "prison", "temple", "aerie", "forge", "graveyard", "guardpost", "arena", "altar"]:
|
|
md += building(key, key)
|
|
md += ["", "## Towers, traps and doors", "", "| | | |", "|---|---|---|"]
|
|
for key, icon in [("arrow", "arrow"), ("cannon", "cannon"), ("magic", "magic"), ("rune", "rune")]:
|
|
md += [f'| <img src="docs/img/icons/{icon}.png" width="40"> | **{lang.get("tool." + key + ".name", key)}** | {lang.get("tool." + key + ".desc", "")} *{lang.get("best.b." + key + ".text", "")}* |']
|
|
for key in ["spikes", "poison", "fire", "roller", "alarm", "mine", "frost", "door_wood", "door_iron", "door_magic"]:
|
|
md += building(key, key, lang.get(f"trap.{key}"))
|
|
md += ["", "## Spells", "", "| | Spell | Mana | |", "|---|---|---|---|"]
|
|
costs = {"lightning": 50, "heal": 60, "summon": "80+"}
|
|
spells = (root / "src/spells.rs").read_text()
|
|
for m in re.finditer(r'Spell::(\w+) => ([\d.]+),', spells.split("pub fn cost")[1].split("}")[0]):
|
|
costs[m.group(1).lower()] = int(float(m.group(2)))
|
|
for key in ["lightning", "heal", "summon", "eye", "cavein", "haste", "stoneskin", "goldrain", "invisible", "plague", "quake"]:
|
|
md += [f'| <img src="docs/img/icons/{key}.png" width="40"> | **{lang.get("tool." + key + ".name", key)}** | {costs.get(key, "")} | {lang.get("tool." + key + ".desc", "")} |']
|
|
md += ["", "---", "", "*This page is written from the game's own data: `python3 tools/make_bestiary.py`.*", ""]
|
|
(root / "BESTIARY.md").write_text("\n".join(md))
|
|
print("BESTIARY.md written")
|