diff --git a/.dockerignore b/.dockerignore index 0c6cec4..5157745 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ target .git server +docs +play diff --git a/BESTIARY.md b/BESTIARY.md new file mode 100644 index 0000000..12560f1 --- /dev/null +++ b/BESTIARY.md @@ -0,0 +1,468 @@ +# Crookfinger's Bestiary + +

The bestiary in the game

+ +> *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 + +### Pit Imp + +Pit Imp, level 1 Pit Imp, level 5 Pit Imp, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 45 | 3 every 1.0 s (3/s) | close combat | 3.2 tiles/s | 0 gold | + +**Comes for:** the dungeon itself; the summon spell brings more +**Loves:** Treasury +**From level 3:** Old hand - digs a quarter faster. + +My little cousins. They dig, they claim, they carry gold, they repair towers and they never complain - well, they complain, but in a language nobody understands. + +Up to five imps help each other with one task. Treat them well: without imps there is no dungeon. + +### Kobold + +Kobold, level 1 Kobold, level 5 Kobold, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 75 | 8 every 0.9 s (9/s) | close combat | 2.6 tiles/s | 40 gold | + +**Comes for:** any lair: always the first through the portal +**Loves:** Training Hall +**Cannot stand:** Shadow Mage +**From level 5:** Dash - leaps at enemies a few tiles away with a heavy blow. + +Small, scaly, sharp-toothed and endlessly enthusiastic. A kobold will fight anything, including things ten times its size, and lose with a happy face. + +Cheap to keep, quick on its feet. I keep a jar of their shed scales on my shelf. + +### Cave Spider + +Cave Spider, level 1 Cave Spider, level 5 Cave Spider, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 95 | 10 every 1.0 s (10/s) | close combat | 2.9 tiles/s | 60 gold | + +**Comes for:** a mushroom garden of 6+ tiles +**Loves:** Mushroom Garden +**Cannot stand:** Giant Bat +**From level 4:** Web - its bites often leave enemies stuck and slow. + +Eight legs, eight eyes and a heart of pure velvet. Cave spiders love the damp warmth of a mushroom garden and the crunchy sound of armour. + +Quick and tough. Do not let the knights see them first: knights scream in a very high voice. + +### Shadow Mage + +Shadow Mage, level 1 Shadow Mage, level 5 Shadow Mage, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 60 | 13 every 1.4 s (9/s) | 4.5 tiles | 2.0 tiles/s | 110 gold | + +**Comes for:** a library of 6+ tiles +**Loves:** Library +**Cannot stand:** Kobold, Stone Ogre +**From level 6:** Shadow step - badly hurt in a fight, it vanishes back to its bed. + +Shadow mages read everything, forget nothing and hurl dark bolts from a safe distance. They are polite, quiet and slightly terrifying. + +Fragile, so keep them behind the ogres. They pay you back in burnt paladins. + +### Stone Ogre + +Stone Ogre, level 1 Stone Ogre, level 5 Stone Ogre, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 230 | 20 every 1.4 s (14/s) | close combat | 1.8 tiles/s | 150 gold | + +**Comes for:** a training hall of 9+ tiles +**Loves:** Training Hall +**Cannot stand:** Shadow Mage, Cogling +**From level 4:** Stomp - shakes the ground: enemies around it are hurt and stunned. + +A stone ogre is a wall that walks and hugs. Mostly it hugs enemies, and they do not get up again. + +Slow, stubborn and wonderfully sturdy. Feed it well, pay it on time and it will stand in front of your core until the end of the world. + +### Jailer + +Jailer, level 1 Jailer, level 5 Jailer, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 260 | 16 every 1.3 s (12/s) | close combat | 1.6 tiles/s | 140 gold | + +**Comes for:** a torture chamber of 6+ tiles +**Loves:** Torture Chamber +**Cannot stand:** Flame Priestess +**From level 7:** Shackle - its blows sometimes chain enemies in place. + +The jailer pulls enemies close with his chain and has a gift for persuasion: prisoners he looks after change sides much more often. + +Big, quiet, hums while he works. Children love him. Well, ours do. + +### Flame Priestess + +Flame Priestess, level 1 Flame Priestess, level 5 Flame Priestess, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 70 | 14 every 1.6 s (9/s) | 5.0 tiles | 2.0 tiles/s | 120 gold | + +**Comes for:** a temple of 6+ tiles +**Loves:** Temple +**Cannot stand:** Jailer +**From level 5:** Healing pulse - heals all hurt creatures around her at once. + +The flame priestess throws fireballs that splash onto everyone around the target, and she heals the soul core with her prayers. + +She sleeps on glowing embers and smells of cinnamon. Do not ask her about the knights who burned her temple. Just let her work. + +### Chainbreaker + +Chainbreaker, level 1 Chainbreaker, level 5 Chainbreaker, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 600 | 36 every 1.1 s (33/s) | close combat | 2.3 tiles/s | 400 gold | + +**Comes for:** a big dungeon: 40+ room tiles, a Lv 5 veteran and 4000 gold +**Loves:** Training Hall +**From level 6:** Whirlwind - spins its axes through every enemy around it. + +A legend. Twin axes, broken chains, and a temper that has flattened whole fortresses. Only one at a time, and only for a master who is worth it. + +Every swing also hits a second enemy. He gets restless when the dungeon is small - give him space and enemies, and he is yours. + +### Cogling + +Cogling, level 1 Cogling, level 5 Cogling, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 90 | 9 every 0.9 s (10/s) | close combat | 2.4 tiles/s | 70 gold | + +**Comes for:** a forge of 6+ tiles +**Loves:** Forge +**Cannot stand:** Stone Ogre +**From level 4:** Bomb - throws a home-made bomb that explodes among the enemies. + +A cogling is soot, brass goggles and pure enthusiasm. It builds traps two and a half times as fast as anyone else and hums while it hammers. + +It cannot fight much, but every corridor it touches becomes a terrible place to walk. I adore them. + +### Revenant + +Revenant, level 1 Revenant, level 5 Revenant, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 160 | 14 every 1.2 s (12/s) | close combat | 1.9 tiles/s | 30 gold | + +**Comes for:** rises from the graveyard +**Loves:** Graveyard +**Cannot stand:** Flame Priestess +**From level 5:** Life drain - heals itself by a third of the damage it deals. + +A revenant does not eat, does not sleep and does not complain - the perfect employee, if you can stand the smell. + +It drinks the strength of those it strikes once it has some experience. The priestess and it do not get along, for obvious reasons. + +### Giant Bat + +Giant Bat, level 1 Giant Bat, level 5 Giant Bat, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 55 | 6 every 0.7 s (9/s) | close combat | 3.6 tiles/s | 30 gold | + +**Comes for:** an aerie of 4+ tiles +**Loves:** Aerie +**Cannot stand:** Cave Spider +**From level 5:** Shriek - a piercing cry that stuns enemies nearby. + +Giant bats! Fast, cheeky, and they fly over water and lava. They patrol the edges of your realm and dive at lonely enemies. + +Weak alone, a plague in swarms. They flee when badly hurt and come back once they have rested. + +### Chain Gargoyle + +Chain Gargoyle, level 1 Chain Gargoyle, level 5 Chain Gargoyle, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 260 | 15 every 1.2 s (12/s) | close combat | 2.2 tiles/s | 120 gold | + +**Comes for:** an aerie of 6+ tiles and a torture chamber of 4+ +**Loves:** Torture Chamber +**Cannot stand:** Arcane Owl +**From level 5:** Stone form - badly hurt, it turns to stone and takes half damage for a while. + +A chain gargoyle is carved stone with bad manners and a soft spot for dungeons with a proper torture chamber. + +Tough as a castle wall, flies over rivers and does not mind arrows at all. It sits very still in the aerie - that is how it relaxes. + +### Arcane Owl + +Arcane Owl, level 1 Arcane Owl, level 5 Arcane Owl, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 70 | 12 every 1.3 s (9/s) | 4.5 tiles | 2.8 tiles/s | 100 gold | + +**Comes for:** an aerie of 6+ tiles and a library of 6+ +**Loves:** Library +**Cannot stand:** Chain Gargoyle +**From level 5:** Volley - every bolt is followed by a second one at another enemy. + +The arcane owl has read every book in your library twice and now shoots glowing bolts at heroes from the air. + +Clever, fragile and terribly judgemental. It hoots when you build crooked rooms. + +### Ember Wyrm + +Ember Wyrm, level 1 Ember Wyrm, level 5 Ember Wyrm, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Wage | +|---|---|---|---|---| +| 650 | 30 every 1.4 s (21/s) | 3.0 tiles | 2.5 tiles/s | 380 gold | + +**Comes for:** an aerie of 16+ tiles and a treasury of 16+ (only one) +**Loves:** Treasury +**Cannot stand:** Giant Bat +**From level 7:** Inferno - its fire breath spreads much wider. + +An ember wyrm. In your dungeon. Breathing fire on knights. Master, I may cry. + +Very rare, very strong, burns everything around its target. It wants a huge aerie and a rich treasury, and it will not share the sky with another wyrm. + +## The humans + +> *Hold your nose, master. Here come the heroes.* + +### Knight + +Knight, level 1 Knight, level 5 Knight, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 140 | 11 every 1.1 s (10/s) | close combat | 1.9 tiles/s | 120 gold | + +The backbone of the Order: polished, punctual and utterly devoid of imagination. Knights march in circles, sing about the sun and smell of boot wax. + +They hit hard and hold the line. Disgusting. Capture them if you can - some of them can be persuaded in the torture chamber. + +### Archer + +Archer, level 1 Archer, level 5 Archer, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 75 | 9 every 1.3 s (7/s) | 4.5 tiles | 2.1 tiles/s | 100 gold | + +Archers stand far away, shoot from behind walls and call it bravery. They wear little green hats. Little green hats, master. + +Weak up close. Send the bats, they hate it. + +### Priest + +Priest, level 1 Priest, level 5 Priest, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 80 | 5 every 1.5 s (3/s) | 3.5 tiles | 1.9 tiles/s | 120 gold | + +Priests heal other heroes from afar and pray loudly enough to curdle milk. Every one of them has a sermon about us, and none of them is short. + +Kill them first, or the fight never ends. + +### Dwarf Miner + +Dwarf Miner, level 1 Dwarf Miner, level 5 Dwarf Miner, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 110 | 9 every 1.0 s (9/s) | close combat | 2.0 tiles/s | 100 gold | + +The dwarf miners of the Order dig their way through earth three times as fast as anyone else, straight towards your treasury. They have no respect for walls - your walls in particular. + +Reinforce your walls. Steel walls make them weep. + +### Fairy Spy + +Fairy Spy, level 1 Fairy Spy, level 5 Fairy Spy, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 70 | 5 every 1.2 s (4/s) | 3.5 tiles | 3.2 tiles/s | 150 gold | + +The Order's little informer: a glowing gnat with wings, sent ahead to find your traps and tell everybody. Once it has seen a trap, the heroes smash it. + +It also sprinkles healing dust on the heroes. Swat it first. Twice, to be sure. + +### Battle Mage + +Battle Mage, level 1 Battle Mage, level 5 Battle Mage, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 120 | 18 every 1.8 s (10/s) | 5.0 tiles | 1.8 tiles/s | 250 gold | + +A mage in the Order's robes who thinks walls are a personal insult. Reinforced walls burst under his fireballs, and he never stops talking about it. + +Fragile up close. Steel walls annoy him most - good. + +### Giant + +Giant, level 1 Giant, level 5 Giant, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 900 | 34 every 1.6 s (21/s) | close combat | 1.3 tiles/s | 600 gold | + +A giant is what happens when a knight eats his vegetables for three hundred years. Slow, enormous, and it treats doors as a suggestion. + +It wrecks traps simply by standing next to them. Revolting manners. Hit it with everything, preferably from afar. + +### Paladin of Light + +Paladin of Light, level 1 Paladin of Light, level 5 Paladin of Light, level 10 + +*Level 1, 5 and 10 - the gear grows with the rank.* + +| Health | Attack | Range | Speed | Bounty | +|---|---|---|---|---| +| 700 | 26 every 1.2 s (22/s) | close combat | 1.7 tiles/s | 1000 gold | + +Paladins of light: glowing, enormous and absolutely convinced of themselves. They are the Order's heroes and have the bounty to prove it. + +Tough as an ogre, twice as smug. Bring everything you have, and maybe a bucket for the glare. + +## The neutrals + +### Cave dwarf + +cave dwarf + +Not all dwarves serve the Order. The cave dwarves keep to themselves in caves full of gold and gems and want nothing but to be left alone. + +They fight whoever comes close and never leave their cave. The more your army grows, the tougher they get - they have heard of you. Some of their chests hold the plans of the rune forge tower. + +## Rooms of the dungeon + +| | Room | | +|---|---|---| +| | **Treasury** | Stores gold: +500 capacity per tile. *Where the gold sleeps. The richer the treasury, the happier everyone is. Imps bring their loads to the nearest treasury, and payday is paid from here. | Each tile holds 500 gold; the core keeps a reserve when it is full.* | +| | **Lair** | One bed per tile. Determines how many creatures come. *Every creature wants its own bed, and only arrives when one is free. A big lair also brings new creatures faster.* | +| | **Mushroom Garden** | Food for creatures. With 6 tiles or more, cave spiders come. *Mushrooms! Juicy, pale, slightly glowing. Hungry creatures are grumpy creatures, and spiders love a big garden.* | +| | **Training Hall** | Creatures gain levels (costs gold). With 9 tiles or more, stone ogres come. *Creatures train here and gain levels. Training is billed on payday. Ogres come for a big hall.* | +| | **Library** | Creatures generate mana. With 6 tiles or more, shadow mages come. *Research, quiet reading and the occasional explosion. Mages and owls come for a library.* | +| | **Torture Chamber** | Holds one prisoner per 2 tiles. Defeated heroes are carried here and tortured until they die or defect. From 6 tiles on, Jailers come. *Captured heroes end up here. With time and a jailer, some of them change sides. Gargoyles feel at home with one.* | +| | **Prison** | Captured heroes sit in cells here - apart from the torture chamber. Without torture they do not change sides, and hunger wears them down. Pick a prisoner up with the hand to move it to the torture chamber. *Rows of cells. Heroes rot here quietly until somebody decides what to do with them.* | +| | **Temple** | Creatures pray here: more mana than studying, and it lifts their mood. From 6 tiles on, Flame Priestesses come. *Prayers heal the soul core and keep spirits high. The flame priestess comes for a temple.* | +| | **Aerie** | Perches and nests for flying creatures. 4+ tiles attract giant bats; with a torture chamber chain gargoyles, with a library arcane owls; a big aerie (16+) and treasury (16+) may lure an ember wyrm. Flyers cross water and lava. *Perches and nests for flying creatures. Bats, gargoyles, owls and even the ember wyrm need room here.* | +| | **Forge** | Creatures manufacture traps and doors here - work, no gold. Finished pieces wait in crates until an imp installs them. Coglings come for a forge of 6+ tiles. *The clang of hammers, the smell of hot iron. Every trap and door is made here first - the more creatures work, the faster.* | +| | **Graveyard** | The fallen - heroes and creatures - are buried here. After a while some of them rise as revenants and serve you. *Moss, stones and fresh earth. Nobody stays dead for long down here.* | +| | **Guard Post** | Creatures hold the position here for a while and watch far: they attack every enemy that comes into view. *A brazier, a banner and a bored creature with sharp eyes.* | +| | **Arena** | Creatures spar in pairs: twice as much experience as training and free of charge, but they get bruised. *Sand, sweat and the occasional tooth. Experience comes fast here.* | +| | **Sacrifice Altar** | Drop a creature on the altar to sacrifice it. Certain pairs of sacrifices bring a rare creature, otherwise the dungeon is blessed: better mood and mana. *Black marble that drinks. Some say it listens to pairs. Ogre and spider, mage and bat, jailer and priestess, kobold and cogling...* | + +## Towers, traps and doors + +| | | | +|---|---|---| +| | **Arrow Tower** | Cheap and fast. Arrows pierce up to two more heroes behind the target. Click your tower with this tool to upgrade it. *The arrow pierces up to two more enemies behind the target. Cheap and reliable.* | +| | **Cannon Tower** | Slow, long range, splash damage that knocks heroes back. Cannot hit targets right next to it. *Splash damage and knockback - push the knights into the lava.* | +| | **Magic Tower** | Uses mana per shot. Never misses, slows the target and jumps to a second hero. *Never misses, slows the target and jumps on to a second enemy. Uses mana.* | +| | **Rune Forge Tower** | Plans from a dwarven chest: built for free, one tower per chest. Huge range, rune bolts jump on to three more enemies. Sell it and the base tower comes back once, in the next mission. *The masterpiece of the cave dwarves: rune bolts that jump on to three more enemies, enormous range and walls of granite. One free build per chest; sell it and it comes back once.* | +| | **Spike Pit** | Spikes shoot up when an enemy steps on it. Strikes again and again. *Simple, honest, reusable. Knights hate it when the floor bites back.* | +| | **Poison Vent** | Releases a cloud of poison that hurts every enemy inside for a while. *A puff of green and a lot of coughing. Best in narrow corridors.* | +| | **Fire Jet** | A demon head spits a jet of fire at enemies up to three tiles away. *A demon head with a temper. Put it where the heroes queue.* | +| | **Stone Roller** | Sends a boulder rolling down the corridor: hits hard and knocks enemies aside. *A boulder and a long corridor - physics does the rest.* | +| | **Alarm Bell** | Rings when enemies come near: your creatures nearby rush to fight them. *Not dangerous itself, but it calls everybody who is.* | +| | **Rune Mine** | Explodes once with great force when an enemy steps close. *One step too many and the corridor gets redecorated.* | +| | **Frost Glow** | An ice crystal: enemies around it are slowed and chilled, always. *Cold makes heroes slow, and slow heroes make easy targets.* | +| | **Wooden Door** | Your creatures walk through, enemies have to smash it. Click a door again to lock it. *Keeps the draught and the knights out - for a while.* | +| | **Iron Door** | A heavy door that takes a long time to break. Click a door again to lock it. *Heavy, riveted, stubborn. Bring a battering ram, heroes.* | +| | **Magic Door** | The strongest door: it hurls back whoever beats on it. Click a door again to lock it. *Strike it and it strikes back. Locked, it is the end of every corridor.* | + +## Spells + +| | Spell | Mana | | +|---|---|---|---| +| | **Lightning** | 50 | 55 damage to heroes in the area. | +| | **Heal** | 60 | Heals your units in the area by 50%. | +| | **Summon Pit Imp** | 80+ | Creates a new Pit Imp on your own territory. | +| | **Seer's Eye** | 40 | Opens a magic eye: a wide area around the spot is revealed for 20 seconds. | +| | **Cave-in** | 70 | The ceiling comes down: open floor turns back into earth (3x3), whoever stands there is hurt and pushed out. Blocks corridors. | +| | **Haste** | 30 | Your creatures around the spot move and act much faster for 30 seconds. | +| | **Stoneskin** | 40 | Your creatures around the spot take only half damage for 30 seconds. | +| | **Gold Rain** | 100 | Turns mana into 250 gold for your treasury. | +| | **Invisibility** | 50 | One creature becomes invisible for 25 seconds: enemies and towers ignore it until it attacks. | +| | **Plague** | 80 | A sickness that eats at the enemies around the spot and jumps from one to the next. | +| | **Quake** | 90 | The ground shakes: enemies around the spot are hurt and stunned for two seconds. Your creatures keep their footing. | + +--- + +*This page is written from the game's own data: `python3 tools/make_bestiary.py`.* diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..53b8424 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,131 @@ +# Installing and running DungeonCraft + +> *Technical matters, master. Crookfinger does not understand half of it, but the imps say it works.* + +**Just want to play?** Download [`play/DungeonCraft.html`](play/DungeonCraft.html) and open it in a browser - +no server, no installation, no login. Savegames are downloaded as `.dcsave` files and loaded again with +*Load Game* (or dropped onto the game window). + +Everything below is for running your own server (with user accounts and savegames on the server), +building the game yourself or working on it. + +## Running a server + +The easiest way is Docker; the image builds the WASM file and serves it with nginx. A second container runs the savegame service (see below): + +```sh +docker compose up -d --build +# open http://localhost:18093 +``` + +### Install as an app (PWA) + +DungeonCraft is a progressive web app: browsers offer to install it (Chrome/Edge: install icon in the address bar or "Install app" in the menu; Android: "Add to home screen"; iOS Safari: Share → "Add to Home Screen"). The installed game starts in full screen and also works offline (the savegame list on the server then falls back to savegame files). + +Browsers only allow the installation over **HTTPS** (or on `localhost`), so a public installation needs a reverse proxy with a certificate in front of port 18093. + +### Building without Docker + +Requirements: Rust with the `wasm32-unknown-unknown` target. + +```sh +rustup target add wasm32-unknown-unknown +cargo build --release --target wasm32-unknown-unknown +cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm www/ +cp ~/.cargo/registry/src/*/macroquad-*/js/mq_js_bundle.js www/ +python3 -m http.server 8080 -d www # open http://localhost:8080 +``` + +### Testing the game logic + +The tests run natively: a headless simulation of whole games, a savegame round trip, rendering of all music modules and the audio engine: + +```sh +cargo test --release +``` + +## Music + +The soundtrack consists of FastTracker 2 modules in `assets/music/`, generated by `tools/compose_music.py` (`python3 tools/compose_music.py`). They can be opened and edited in MilkyTracker. The game plays them with its own XM/MOD replayer (`src/xm.rs`); sound effects are synthesised chip sounds (`src/sfx.rs`). Both are mixed in `src/audio.rs` and streamed to the browser by `www/dc_plugin.js`. + +## Controls + +| Input | Action | +|---|---| +| Esc | Pause menu (save, load, settings) | +| Left click / drag | Use the active tool (dig and rooms: drag a rectangle) | +| Right click | Drop a unit from your hand (prisoners anywhere on your ground; onto a free torture place = locked up) | +| Shift + left click (hand) | Backhand own units (15 s faster, costs a little HP and mood) / punch heroes on your ground (15 damage, 1 s stun) | +| Right drag, WASD, arrows, screen edge | Move the camera | +| Middle drag, Q/E, R/F | Rotate / tilt the camera | +| Mouse wheel | Zoom | +| 1–9, 0, X, V, K, J, B, C, T, U, Y, O, N | Select a tool or spell (more can be bound in the settings) | +| P / Space | Pause | +| M, +/- | Sound on/off, volume | +| G | Tile outlines on/off | +| H | Help | + +All keys except Esc and the arrow keys can be changed in Settings → Keys. + +The toolbar groups the tools into categories (Dig, Hand, Rooms, Towers, Traps, Spells, Orders) plus a menu button; categories with several tools open a row above the toolbar. The keys select tools directly. + +### Touch (phones and tablets) + +The touch interface switches on automatically on touch devices or after the first touch (Settings → Game → Touch interface: Auto / On / Off). It uses a larger interface and these gestures: + +| Gesture | Action | +|---|---| +| Tap | Use the tool (hand: pick up a unit; spells, flag, towers: at that spot) | +| Drag | Dig, rooms and sell: mark a rectangle; other tools: move the map | +| Two fingers | Move the map, pinch to zoom, twist to rotate the camera | +| Long press | Information about the spot; hand: drop the units there | +| Double tap (hand) | Backhand own units / punch heroes on your ground | + +The savegame name is entered with the browser's text dialog when the name field is tapped. + +## Savegames + +When the game is served together with the service (`server/`, started by `docker compose`), it starts with a **login**: + +- The very first login creates the **admin account** with the entered name and password - do this right after installing. +- The admin creates further users (title screen -> *Account*); every user can change the password there and log out. +- Every user only sees the **own savegames** on the server: *Save Game* and *Load Game* open the list (save under a new name, overwrite, load, delete). +- *Download Standalone* (title screen) downloads `DungeonCraft.html`: the whole game in one file, without login and without server. Savegame files (*Download as file*) open there as well. + +If the service cannot be reached (for example the standalone file, `www/` opened as local files or a plain web server), there is no login and the game uses savegame files: *Save Game* downloads a `.dcsave` file (JSON), *Load Game* opens the file picker; dropping the file onto the game window works too. + +The game also autosaves into the browser every two minutes and when the tab is closed; *Continue* on the title screen resumes from there. + +### Savegame service + +A small Rust service (axum) in `server/` keeps the user accounts (Argon2 password hashes) and the savegames as files in the Docker volume `savegames` (one folder per user). nginx forwards `/api/` to it; the service itself is not published. The standalone file is served under `/standalone/` only for logged-in users (nginx `auth_request`). + +| Request | Meaning | +|---|---| +| `GET /api/health` | `ok` when the service runs (`ok setup` while there is no account yet) | +| `POST /api/login` | `{name, password}` -> `{token, name, admin}`; the first login creates the admin | +| `POST /api/logout`, `GET /api/me`, `POST /api/password` | session, own account, change password | +| `GET/POST /api/users`, `DELETE /api/users/{name}` | admin: list, create, delete users | +| `GET /api/saves` | own savegames (id, name, date, map size, wave, play time), newest first | +| `POST /api/saves?name=...` | store a new savegame (body: savegame JSON) | +| `PUT /api/saves/{id}?name=...` | overwrite a savegame | +| `GET /api/saves/{id}` | savegame JSON | +| `DELETE /api/saves/{id}` | delete a savegame | + +A forgotten password is set again on the server with `docker compose stop saves && docker compose run --rm --no-deps saves dungeoncraft-saves set-password '' && docker compose up -d saves` (ends the user's sessions). To start over with no accounts at all, delete `users.json` and `sessions.json` in the `savegames` volume. + +All requests except health and login need `Authorization: Bearer `. Limits: 1 MB per savegame, 500 MB in total, 30 write requests and 20 login attempts per minute. Settings: `SAVE_DIR` (default `/data`), `LISTEN` (default `0.0.0.0:8080`). + +## Adding a language + +1. Copy `lang/en.lang` to `lang/.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are). +2. Add the file to `SOURCES` in `src/i18n.rs`. +3. Note: the DINOBYTE font only covers the CP437 character set. + +## Refreshing the generated files + +| File | How | +|---|---| +| `play/DungeonCraft.html` | build the WASM file, then `python3 tools/make_standalone.py play/DungeonCraft.html` (the Docker image builds the same file under `/standalone/`) | +| `docs/img/units`, `docs/img/icons` | `cargo test --release export_doc_images -- --ignored` | +| `BESTIARY.md` | `python3 tools/make_bestiary.py` | diff --git a/README.md b/README.md index 3f26908..a961880 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,169 @@ -# DungeonCraft +

+ DungeonCraft - the soul core +

-> **Made with the help of [Claude Code](https://claude.com/claude-code).** The code, graphics, texts and -> documentation of this game were written together with Claude Code, Anthropic's AI coding assistant. +

DungeonCraft

-An original dungeon building & defence game that runs in the browser (Rust + [macroquad](https://github.com/not-fl3/macroquad), compiled to WebAssembly). +

Dig. Build. Defend. Conquer.
+An evil little dungeon game for the browser - in glorious pixels, in 3D, for free.

-Dig out your underground realm, claim the portal to attract creatures, keep them fed, rested and paid, fend off endless hero waves that adapt to your army – and finally destroy the Hero Gate. +

+ Rust + WebAssembly + Made with Claude Code + MIT License + English and German +

-- 3D isometric view with a freely rotatable and tiltable camera -- Procedural pixel art, irregular cave walls, flickering torch light -- CP437 text-mode style interface with the DINOBYTE bitmap font -- Map sizes from 64×64 to 256×256 tiles (one portal per 64×64 tiles, dwarf caves with treasure and rune forge plans) -- English and German (language files in `lang/`, easy to extend) +

+ ▶ Play now - download one HTML file, open it, done
+ Bestiary · Run your own server · Design notes +

-See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets. +

+ The title screen with the endless bridge battle +

-## Running +--- -The easiest way is Docker; the image builds the WASM file and serves it with nginx. A second container runs the savegame service (see below): +## Wake up, master. -```sh -docker compose up -d --build -# open http://localhost:18093 -``` +Yes, you. Don't look around, there's nobody else down here - just you, four imps and me, old **Crookfinger**. -### Install as an app (PWA) +A hundred years ago the Order of the Lightwatch smashed your soul core into shards and carried them off like trophies. +They polished their boots, sang songs about the sun and forgot about you. **Big mistake.** -DungeonCraft is a progressive web app: browsers offer to install it (Chrome/Edge: install icon in the address bar or "Install app" in the menu; Android: "Add to home screen"; iOS Safari: Share → "Add to Home Screen"). The installed game starts in full screen and also works offline (the savegame list on the server then falls back to savegame files). +Your heart still beats. Hear that? *Lub-dub. Lub-dub.* That's the sound of a grudge. -Browsers only allow the installation over **HTTPS** (or on `localhost`), so a public installation needs a reverse proxy with a certificate in front of port 18093. +So pick up the pickaxe - well, point at the rock, the imps carry the pickaxes - and let's dig ourselves a realm. +Gold is in the walls, creatures are waiting behind the portal, and the knights... the knights are about to have +a very bad century. -### Building without Docker +

+ The beating crystal heart, the demon gate and the dark towers +
Your heart, master, held in chains on its obsidian throne. And the demon gate. I polished it myself. +

-Requirements: Rust with the `wasm32-unknown-unknown` target. +## What awaits you down here -```sh -rustup target add wasm32-unknown-unknown -cargo build --release --target wasm32-unknown-unknown -cp target/wasm32-unknown-unknown/release/dungeoncraft.wasm www/ -cp ~/.cargo/registry/src/*/macroquad-*/js/mq_js_bundle.js www/ -python3 -m http.server 8080 -d www # open http://localhost:8080 -``` +**🕳 Dig your realm.** Mark the rock and your imps dig, claim the ground, carry gold, gems and diamonds to the +treasury and hang torches on the walls. Up to five imps help each other with one job. Every tile you claim is *yours*. -### Testing the game logic +**🏰 Build rooms that bring the right guests.** A lair and a mushroom garden open the portal. A library brings +shadow mages, a training hall stone ogres, a temple the flame priestess, an aerie giant bats, gargoyles, arcane +owls - and maybe, one day, an **ember wyrm**. Fourteen rooms in total, from the forge to the sacrifice altar. -The tests run natively: a headless simulation of whole games, a savegame round trip, rendering of all music modules and the audio engine: +**👹 Keep them happy, or they leave.** Every creature wants a bed, food and its wage on payday - and it walks to the +treasury to fetch it. Each has a favourite room, and some of them simply cannot stand each other. +Put a mage next to an ogre and watch the fur fly. (Nobody dies. Probably.) -```sh -cargo test --release -``` +**⚔ Train them into legends.** Ten ranks from Recruit to Warlord, with chevrons, bars and stars - and gear that grows +with them, up to golden armour, a crown and a halo. With the level come abilities: the ogre learns to *stomp*, the mage +to *step through shadows*, the chainbreaker to spin like a *whirlwind*. -## Music +

+ A battle at a bridge over an underground river +
The knights brought a bridge. How thoughtful. Now they're bringing it back, one piece at a time. +

-The soundtrack consists of FastTracker 2 modules in `assets/music/`, generated by `tools/compose_music.py` (`python3 tools/compose_music.py`). They can be opened and edited in MilkyTracker. The game plays them with its own XM/MOD replayer (`src/xm.rs`); sound effects are synthesised chip sounds (`src/sfx.rs`). Both are mixed in `src/audio.rs` and streamed to the browser by `www/dc_plugin.js`. +**🔥 Defend with towers, traps and doors.** Arrow spires, hellmouth cannons and dark spires on the walls. In the +**forge** your creatures build spike pits, poison vents, fire jets, stone rollers, alarm bells, rune mines and +frost glows - and doors of wood, iron and magic. Lock them, and nobody gets through. Not even the giant. Well. Not for a while. -## Controls +**✨ Cast nasty spells.** Lightning, healing, seer's eye, cave-in, haste, stoneskin, gold rain, invisibility, +plague and quake. Your scholars **research** them in the library - one discovery after another. -| Input | Action | +**🌫 Explore the dark.** Everything beyond your walls is black until your creatures go there. +Somewhere out there: a cave of neutral **dwarves** guarding gold and gems - and sometimes a chest with the plans +of a dwarven **rune forge tower**. Islands in lava rivers. Mana springs. Creatures in chains, waiting to be freed. + +

+ The forge with coglings and every kind of trap and door + Prison, graveyard, arena, altar and guard posts +
Left: the forge and its little coglings. Right: prison, graveyard, arena, guard posts and the altar. Cosy. +

+ +**👑 Other masters want your realm, too.** Mossgrim the Mouldy (green, smells like a wet cellar) and Lady Nightwell +(blue, writes threatening letters on scented paper) dig their own dungeons in free play and in the campaign. +And the humans attack *everybody*. Let them bite each other first. + +**📜 A campaign of thirty missions.** Take back the shards of your soul core - from the first little outpost by the +border brook to the citadel of Grand Master Valerian Brightshield. Rivers, fortresses, treasure islands, sealed +caves, rival masters, and me, telling you the story. With tea. + +

+ A dwarf cave with a rune chest + The start of a game in the fog of war +
A dwarf cave, master. Nice dwarves. Don't go too close. - And this is where every game begins: in the dark. +

+ +## Meet the household + +

+ Pit imp + Kobold + Cave spider + Shadow mage + Stone ogre + Jailer + Flame priestess + Cogling + Revenant + Giant bat + Chain gargoyle + Arcane owl + Ember wyrm + Chainbreaker +

+ +> *Fourteen kinds of wonderful creatures, master, and eight kinds of humans in shiny tin. I wrote everything down for +> you - who loves what, who hates whom, who learns what - in* **[Crookfinger's Bestiary](BESTIARY.md)**. +> *The same book is in the game, by the way - on the title screen and in the pause menu.* + +## How to play + +1. **Download [`play/DungeonCraft.html`](play/DungeonCraft.html)** and open it in your browser. That's all. + It runs offline, on desktop and on phones (touch controls switch on by themselves). +2. Or **run your own server** with Docker - with user accounts, savegames on the server and the game as an + installable app: see **[INSTALL.md](INSTALL.md)**. + +**The basics, master:** + +| | | |---|---| -| Esc | Pause menu (save, load, settings) | -| Left click / drag | Use the active tool (dig and rooms: drag a rectangle) | -| Right click | Drop a unit from your hand (prisoners anywhere on your ground; onto a free torture place = locked up) | -| Shift + left click (hand) | Backhand own units (15 s faster, costs a little HP and mood) / punch heroes on your ground (15 damage, 1 s stun) | -| Right drag, WASD, arrows, screen edge | Move the camera | -| Middle drag, Q/E, R/F | Rotate / tilt the camera | -| Mouse wheel | Zoom | -| 1–9, 0, X, V, K, J, B, C, T | Select a tool or spell | -| P / Space | Pause | -| M, +/- | Sound on/off, volume | -| G | Tile outlines on/off | -| H | Help | +| Left click / drag | use the tool - dig, build rooms (drag a rectangle), cast, place towers and traps | +| Right click | drop what your hand holds (creatures, prisoners - or a sacrifice on the altar) | +| Shift + click | slap your own creatures (faster for a while) or punch a hero | +| Right drag, WASD, screen edge | move the camera · middle drag, Q/E, R/F: rotate and tilt · wheel: zoom | +| Esc | pause menu: save, load, settings, bestiary | -All keys except Esc and the arrow keys can be changed in Settings → Keys. +On phones: tap to use a tool, drag to mark or move, two fingers to zoom and turn, long press for information. -The toolbar groups the tools into categories (Dig, Hand, Rooms, Towers, Spells, Orders) plus a menu button; categories with several tools open a row above the toolbar. The keys select tools directly. +## Under the hood -### Touch (phones and tablets) +- **Rust**, compiled to **WebAssembly**, with [macroquad](https://github.com/not-fl3/macroquad) - one small module, no engine, no plugins. +- A real 3D isometric world in pixel art: chunked meshes, flickering point lights, a glow pass, particles, + billboard sprites with eight viewing directions, every frame of it drawn by code - **no image files** except the font. +- Procedural chiptune soundtrack as FastTracker 2 modules with its own XM replayer and synthesised sound effects. +- A tiny Rust savegame server (axum, Argon2 passwords) - optional; without it the game uses savegame files. +- English and German, more languages are one text file away. -The touch interface switches on automatically on touch devices or after the first touch (Settings → Game → Touch interface: Auto / On / Off). It uses a larger interface and these gestures: +## Made with Claude Code -| Gesture | Action | -|---|---| -| Tap | Use the tool (hand: pick up a unit; spells, flag, towers: at that spot) | -| Drag | Dig, rooms and sell: mark a rectangle; other tools: move the map | -| Two fingers | Move the map, pinch to zoom, twist to rotate the camera | -| Long press | Information about the spot; hand: drop the units there | -| Double tap (hand) | Backhand own units / punch heroes on your ground | - -The savegame name is entered with the browser's text dialog when the name field is tapped. - -## Savegames - -When the game is served together with the service (`server/`, started by `docker compose`), it starts with a **login**: - -- The very first login creates the **admin account** with the entered name and password - do this right after installing. -- The admin creates further users (title screen -> *Account*); every user can change the password there and log out. -- Every user only sees the **own savegames** on the server: *Save Game* and *Load Game* open the list (save under a new name, overwrite, load, delete). -- *Download Standalone* (title screen) downloads `DungeonCraft.html`: the whole game in one file, without login and without server. Savegame files (*Download as file*) open there as well. - -If the service cannot be reached (for example the standalone file, `www/` opened as local files or a plain web server), there is no login and the game uses savegame files: *Save Game* downloads a `.dcsave` file (JSON), *Load Game* opens the file picker; dropping the file onto the game window works too. - -The game also autosaves into the browser every two minutes and when the tab is closed; *Continue* on the title screen resumes from there. - -### Savegame service - -A small Rust service (axum) in `server/` keeps the user accounts (Argon2 password hashes) and the savegames as files in the Docker volume `savegames` (one folder per user). nginx forwards `/api/` to it; the service itself is not published. The standalone file is served under `/standalone/` only for logged-in users (nginx `auth_request`). - -| Request | Meaning | -|---|---| -| `GET /api/health` | `ok` when the service runs (`ok setup` while there is no account yet) | -| `POST /api/login` | `{name, password}` -> `{token, name, admin}`; the first login creates the admin | -| `POST /api/logout`, `GET /api/me`, `POST /api/password` | session, own account, change password | -| `GET/POST /api/users`, `DELETE /api/users/{name}` | admin: list, create, delete users | -| `GET /api/saves` | own savegames (id, name, date, map size, wave, play time), newest first | -| `POST /api/saves?name=...` | store a new savegame (body: savegame JSON) | -| `PUT /api/saves/{id}?name=...` | overwrite a savegame | -| `GET /api/saves/{id}` | savegame JSON | -| `DELETE /api/saves/{id}` | delete a savegame | - -A forgotten password is set again on the server with `docker compose stop saves && docker compose run --rm --no-deps saves dungeoncraft-saves set-password '' && docker compose up -d saves` (ends the user's sessions). To start over with no accounts at all, delete `users.json` and `sessions.json` in the `savegames` volume. - -All requests except health and login need `Authorization: Bearer `. Limits: 1 MB per savegame, 500 MB in total, 30 write requests and 20 login attempts per minute. Settings: `SAVE_DIR` (default `/data`), `LISTEN` (default `0.0.0.0:8080`). - -## Adding a language - -1. Copy `lang/en.lang` to `lang/.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are). -2. Add the file to `SOURCES` in `src/i18n.rs`. -3. Note: the DINOBYTE font only covers the CP437 character set. +

Claude Code mark  DungeonCraft was developed with the help of +Claude Code, Anthropic's AI coding assistant - the code, the graphics, +the music, the texts and this page. Claude, Claude Code and Anthropic are trademarks of Anthropic; the pixel mark only +credits the tool.

+
## License -DungeonCraft is released under the [MIT License](LICENSE) – this covers the code, the generated music in `assets/music/` and the procedural graphics. -The DINOBYTE font (`assets/dbyte_1x.png`) by mby is licensed separately under CC BY 4.0, see [CREDITS.md](CREDITS.md). +DungeonCraft is released under the **[MIT License](LICENSE)** - © 2026 Richard Zink. It covers the code, the generated +music in `assets/music/` and the procedural graphics. +The **DINOBYTE** font (`assets/dbyte_1x.png`) by [mby](https://mby.itch.io/dinobyte) is licensed separately under +**CC BY 4.0**. Details in [CREDITS.md](CREDITS.md). + +--- + +

"The underworld is yours, master. Now - tea?"
- Crookfinger

diff --git a/docs/img/claude-mark.png b/docs/img/claude-mark.png new file mode 100644 index 0000000..e010671 Binary files /dev/null and b/docs/img/claude-mark.png differ diff --git a/docs/img/icons/aerie.png b/docs/img/icons/aerie.png new file mode 100644 index 0000000..cfd0b67 Binary files /dev/null and b/docs/img/icons/aerie.png differ diff --git a/docs/img/icons/alarm.png b/docs/img/icons/alarm.png new file mode 100644 index 0000000..e1447aa Binary files /dev/null and b/docs/img/icons/alarm.png differ diff --git a/docs/img/icons/altar.png b/docs/img/icons/altar.png new file mode 100644 index 0000000..84f2e87 Binary files /dev/null and b/docs/img/icons/altar.png differ diff --git a/docs/img/icons/arena.png b/docs/img/icons/arena.png new file mode 100644 index 0000000..692d09e Binary files /dev/null and b/docs/img/icons/arena.png differ diff --git a/docs/img/icons/arrow.png b/docs/img/icons/arrow.png new file mode 100644 index 0000000..ab20c99 Binary files /dev/null and b/docs/img/icons/arrow.png differ diff --git a/docs/img/icons/cannon.png b/docs/img/icons/cannon.png new file mode 100644 index 0000000..4da9371 Binary files /dev/null and b/docs/img/icons/cannon.png differ diff --git a/docs/img/icons/cavein.png b/docs/img/icons/cavein.png new file mode 100644 index 0000000..9c1ecf7 Binary files /dev/null and b/docs/img/icons/cavein.png differ diff --git a/docs/img/icons/door_iron.png b/docs/img/icons/door_iron.png new file mode 100644 index 0000000..57e8a3e Binary files /dev/null and b/docs/img/icons/door_iron.png differ diff --git a/docs/img/icons/door_magic.png b/docs/img/icons/door_magic.png new file mode 100644 index 0000000..f678bbb Binary files /dev/null and b/docs/img/icons/door_magic.png differ diff --git a/docs/img/icons/door_wood.png b/docs/img/icons/door_wood.png new file mode 100644 index 0000000..3ca787e Binary files /dev/null and b/docs/img/icons/door_wood.png differ diff --git a/docs/img/icons/eye.png b/docs/img/icons/eye.png new file mode 100644 index 0000000..0d956c4 Binary files /dev/null and b/docs/img/icons/eye.png differ diff --git a/docs/img/icons/farm.png b/docs/img/icons/farm.png new file mode 100644 index 0000000..7e0fb78 Binary files /dev/null and b/docs/img/icons/farm.png differ diff --git a/docs/img/icons/fire.png b/docs/img/icons/fire.png new file mode 100644 index 0000000..869531f Binary files /dev/null and b/docs/img/icons/fire.png differ diff --git a/docs/img/icons/forge.png b/docs/img/icons/forge.png new file mode 100644 index 0000000..5f2996e Binary files /dev/null and b/docs/img/icons/forge.png differ diff --git a/docs/img/icons/frost.png b/docs/img/icons/frost.png new file mode 100644 index 0000000..bb581bf Binary files /dev/null and b/docs/img/icons/frost.png differ diff --git a/docs/img/icons/goldrain.png b/docs/img/icons/goldrain.png new file mode 100644 index 0000000..d62c7ba Binary files /dev/null and b/docs/img/icons/goldrain.png differ diff --git a/docs/img/icons/graveyard.png b/docs/img/icons/graveyard.png new file mode 100644 index 0000000..74ee19d Binary files /dev/null and b/docs/img/icons/graveyard.png differ diff --git a/docs/img/icons/guardpost.png b/docs/img/icons/guardpost.png new file mode 100644 index 0000000..27b839b Binary files /dev/null and b/docs/img/icons/guardpost.png differ diff --git a/docs/img/icons/haste.png b/docs/img/icons/haste.png new file mode 100644 index 0000000..74da141 Binary files /dev/null and b/docs/img/icons/haste.png differ diff --git a/docs/img/icons/heal.png b/docs/img/icons/heal.png new file mode 100644 index 0000000..e840e85 Binary files /dev/null and b/docs/img/icons/heal.png differ diff --git a/docs/img/icons/invisible.png b/docs/img/icons/invisible.png new file mode 100644 index 0000000..3bb5a3b Binary files /dev/null and b/docs/img/icons/invisible.png differ diff --git a/docs/img/icons/lair.png b/docs/img/icons/lair.png new file mode 100644 index 0000000..d525f11 Binary files /dev/null and b/docs/img/icons/lair.png differ diff --git a/docs/img/icons/library.png b/docs/img/icons/library.png new file mode 100644 index 0000000..f97eb72 Binary files /dev/null and b/docs/img/icons/library.png differ diff --git a/docs/img/icons/lightning.png b/docs/img/icons/lightning.png new file mode 100644 index 0000000..e456338 Binary files /dev/null and b/docs/img/icons/lightning.png differ diff --git a/docs/img/icons/magic.png b/docs/img/icons/magic.png new file mode 100644 index 0000000..3da1d38 Binary files /dev/null and b/docs/img/icons/magic.png differ diff --git a/docs/img/icons/mine.png b/docs/img/icons/mine.png new file mode 100644 index 0000000..48e8459 Binary files /dev/null and b/docs/img/icons/mine.png differ diff --git a/docs/img/icons/plague.png b/docs/img/icons/plague.png new file mode 100644 index 0000000..e05b2ac Binary files /dev/null and b/docs/img/icons/plague.png differ diff --git a/docs/img/icons/poison.png b/docs/img/icons/poison.png new file mode 100644 index 0000000..79de6ed Binary files /dev/null and b/docs/img/icons/poison.png differ diff --git a/docs/img/icons/prison.png b/docs/img/icons/prison.png new file mode 100644 index 0000000..75f3b41 Binary files /dev/null and b/docs/img/icons/prison.png differ diff --git a/docs/img/icons/quake.png b/docs/img/icons/quake.png new file mode 100644 index 0000000..797b9dc Binary files /dev/null and b/docs/img/icons/quake.png differ diff --git a/docs/img/icons/roller.png b/docs/img/icons/roller.png new file mode 100644 index 0000000..7fb5293 Binary files /dev/null and b/docs/img/icons/roller.png differ diff --git a/docs/img/icons/rune.png b/docs/img/icons/rune.png new file mode 100644 index 0000000..be058f4 Binary files /dev/null and b/docs/img/icons/rune.png differ diff --git a/docs/img/icons/spikes.png b/docs/img/icons/spikes.png new file mode 100644 index 0000000..716d42a Binary files /dev/null and b/docs/img/icons/spikes.png differ diff --git a/docs/img/icons/stoneskin.png b/docs/img/icons/stoneskin.png new file mode 100644 index 0000000..853881b Binary files /dev/null and b/docs/img/icons/stoneskin.png differ diff --git a/docs/img/icons/summon.png b/docs/img/icons/summon.png new file mode 100644 index 0000000..d810d8e Binary files /dev/null and b/docs/img/icons/summon.png differ diff --git a/docs/img/icons/temple.png b/docs/img/icons/temple.png new file mode 100644 index 0000000..51ac687 Binary files /dev/null and b/docs/img/icons/temple.png differ diff --git a/docs/img/icons/torture.png b/docs/img/icons/torture.png new file mode 100644 index 0000000..205e6fd Binary files /dev/null and b/docs/img/icons/torture.png differ diff --git a/docs/img/icons/training.png b/docs/img/icons/training.png new file mode 100644 index 0000000..7792c2f Binary files /dev/null and b/docs/img/icons/training.png differ diff --git a/docs/img/icons/treasury.png b/docs/img/icons/treasury.png new file mode 100644 index 0000000..d44e4b7 Binary files /dev/null and b/docs/img/icons/treasury.png differ diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000..ec49239 Binary files /dev/null and b/docs/img/logo.png differ diff --git a/docs/img/units/archer_1.png b/docs/img/units/archer_1.png new file mode 100644 index 0000000..dfc901e Binary files /dev/null and b/docs/img/units/archer_1.png differ diff --git a/docs/img/units/archer_10.png b/docs/img/units/archer_10.png new file mode 100644 index 0000000..12e3c18 Binary files /dev/null and b/docs/img/units/archer_10.png differ diff --git a/docs/img/units/archer_5.png b/docs/img/units/archer_5.png new file mode 100644 index 0000000..042da85 Binary files /dev/null and b/docs/img/units/archer_5.png differ diff --git a/docs/img/units/bat_1.png b/docs/img/units/bat_1.png new file mode 100644 index 0000000..f3a98ec Binary files /dev/null and b/docs/img/units/bat_1.png differ diff --git a/docs/img/units/bat_10.png b/docs/img/units/bat_10.png new file mode 100644 index 0000000..684b73f Binary files /dev/null and b/docs/img/units/bat_10.png differ diff --git a/docs/img/units/bat_5.png b/docs/img/units/bat_5.png new file mode 100644 index 0000000..23738a2 Binary files /dev/null and b/docs/img/units/bat_5.png differ diff --git a/docs/img/units/battlemage_1.png b/docs/img/units/battlemage_1.png new file mode 100644 index 0000000..85cc7cf Binary files /dev/null and b/docs/img/units/battlemage_1.png differ diff --git a/docs/img/units/battlemage_10.png b/docs/img/units/battlemage_10.png new file mode 100644 index 0000000..e915b97 Binary files /dev/null and b/docs/img/units/battlemage_10.png differ diff --git a/docs/img/units/battlemage_5.png b/docs/img/units/battlemage_5.png new file mode 100644 index 0000000..3872851 Binary files /dev/null and b/docs/img/units/battlemage_5.png differ diff --git a/docs/img/units/chainbreaker_1.png b/docs/img/units/chainbreaker_1.png new file mode 100644 index 0000000..4484293 Binary files /dev/null and b/docs/img/units/chainbreaker_1.png differ diff --git a/docs/img/units/chainbreaker_10.png b/docs/img/units/chainbreaker_10.png new file mode 100644 index 0000000..365ee07 Binary files /dev/null and b/docs/img/units/chainbreaker_10.png differ diff --git a/docs/img/units/chainbreaker_5.png b/docs/img/units/chainbreaker_5.png new file mode 100644 index 0000000..b5fd6e3 Binary files /dev/null and b/docs/img/units/chainbreaker_5.png differ diff --git a/docs/img/units/cogling_1.png b/docs/img/units/cogling_1.png new file mode 100644 index 0000000..97aa38f Binary files /dev/null and b/docs/img/units/cogling_1.png differ diff --git a/docs/img/units/cogling_10.png b/docs/img/units/cogling_10.png new file mode 100644 index 0000000..8110777 Binary files /dev/null and b/docs/img/units/cogling_10.png differ diff --git a/docs/img/units/cogling_5.png b/docs/img/units/cogling_5.png new file mode 100644 index 0000000..edac1e4 Binary files /dev/null and b/docs/img/units/cogling_5.png differ diff --git a/docs/img/units/dwarf_1.png b/docs/img/units/dwarf_1.png new file mode 100644 index 0000000..4854d64 Binary files /dev/null and b/docs/img/units/dwarf_1.png differ diff --git a/docs/img/units/dwarf_10.png b/docs/img/units/dwarf_10.png new file mode 100644 index 0000000..37dad7c Binary files /dev/null and b/docs/img/units/dwarf_10.png differ diff --git a/docs/img/units/dwarf_5.png b/docs/img/units/dwarf_5.png new file mode 100644 index 0000000..df215f6 Binary files /dev/null and b/docs/img/units/dwarf_5.png differ diff --git a/docs/img/units/fairy_1.png b/docs/img/units/fairy_1.png new file mode 100644 index 0000000..488be4b Binary files /dev/null and b/docs/img/units/fairy_1.png differ diff --git a/docs/img/units/fairy_10.png b/docs/img/units/fairy_10.png new file mode 100644 index 0000000..9592fbe Binary files /dev/null and b/docs/img/units/fairy_10.png differ diff --git a/docs/img/units/fairy_5.png b/docs/img/units/fairy_5.png new file mode 100644 index 0000000..2089374 Binary files /dev/null and b/docs/img/units/fairy_5.png differ diff --git a/docs/img/units/gargoyle_1.png b/docs/img/units/gargoyle_1.png new file mode 100644 index 0000000..fccab87 Binary files /dev/null and b/docs/img/units/gargoyle_1.png differ diff --git a/docs/img/units/gargoyle_10.png b/docs/img/units/gargoyle_10.png new file mode 100644 index 0000000..3ee7f37 Binary files /dev/null and b/docs/img/units/gargoyle_10.png differ diff --git a/docs/img/units/gargoyle_5.png b/docs/img/units/gargoyle_5.png new file mode 100644 index 0000000..b7ef014 Binary files /dev/null and b/docs/img/units/gargoyle_5.png differ diff --git a/docs/img/units/giant_1.png b/docs/img/units/giant_1.png new file mode 100644 index 0000000..a21297f Binary files /dev/null and b/docs/img/units/giant_1.png differ diff --git a/docs/img/units/giant_10.png b/docs/img/units/giant_10.png new file mode 100644 index 0000000..bd85b58 Binary files /dev/null and b/docs/img/units/giant_10.png differ diff --git a/docs/img/units/giant_5.png b/docs/img/units/giant_5.png new file mode 100644 index 0000000..3b0b4e2 Binary files /dev/null and b/docs/img/units/giant_5.png differ diff --git a/docs/img/units/jailer_1.png b/docs/img/units/jailer_1.png new file mode 100644 index 0000000..b3a6640 Binary files /dev/null and b/docs/img/units/jailer_1.png differ diff --git a/docs/img/units/jailer_10.png b/docs/img/units/jailer_10.png new file mode 100644 index 0000000..6ba986e Binary files /dev/null and b/docs/img/units/jailer_10.png differ diff --git a/docs/img/units/jailer_5.png b/docs/img/units/jailer_5.png new file mode 100644 index 0000000..42c096d Binary files /dev/null and b/docs/img/units/jailer_5.png differ diff --git a/docs/img/units/knight_1.png b/docs/img/units/knight_1.png new file mode 100644 index 0000000..31c2ee6 Binary files /dev/null and b/docs/img/units/knight_1.png differ diff --git a/docs/img/units/knight_10.png b/docs/img/units/knight_10.png new file mode 100644 index 0000000..41efb09 Binary files /dev/null and b/docs/img/units/knight_10.png differ diff --git a/docs/img/units/knight_5.png b/docs/img/units/knight_5.png new file mode 100644 index 0000000..283c793 Binary files /dev/null and b/docs/img/units/knight_5.png differ diff --git a/docs/img/units/kobold_1.png b/docs/img/units/kobold_1.png new file mode 100644 index 0000000..20f9114 Binary files /dev/null and b/docs/img/units/kobold_1.png differ diff --git a/docs/img/units/kobold_10.png b/docs/img/units/kobold_10.png new file mode 100644 index 0000000..ca2e46e Binary files /dev/null and b/docs/img/units/kobold_10.png differ diff --git a/docs/img/units/kobold_5.png b/docs/img/units/kobold_5.png new file mode 100644 index 0000000..c518bb7 Binary files /dev/null and b/docs/img/units/kobold_5.png differ diff --git a/docs/img/units/mage_1.png b/docs/img/units/mage_1.png new file mode 100644 index 0000000..a790980 Binary files /dev/null and b/docs/img/units/mage_1.png differ diff --git a/docs/img/units/mage_10.png b/docs/img/units/mage_10.png new file mode 100644 index 0000000..5ced5ba Binary files /dev/null and b/docs/img/units/mage_10.png differ diff --git a/docs/img/units/mage_5.png b/docs/img/units/mage_5.png new file mode 100644 index 0000000..31edef9 Binary files /dev/null and b/docs/img/units/mage_5.png differ diff --git a/docs/img/units/ogre_1.png b/docs/img/units/ogre_1.png new file mode 100644 index 0000000..127af91 Binary files /dev/null and b/docs/img/units/ogre_1.png differ diff --git a/docs/img/units/ogre_10.png b/docs/img/units/ogre_10.png new file mode 100644 index 0000000..a5c951e Binary files /dev/null and b/docs/img/units/ogre_10.png differ diff --git a/docs/img/units/ogre_5.png b/docs/img/units/ogre_5.png new file mode 100644 index 0000000..113610b Binary files /dev/null and b/docs/img/units/ogre_5.png differ diff --git a/docs/img/units/owl_1.png b/docs/img/units/owl_1.png new file mode 100644 index 0000000..fd91477 Binary files /dev/null and b/docs/img/units/owl_1.png differ diff --git a/docs/img/units/owl_10.png b/docs/img/units/owl_10.png new file mode 100644 index 0000000..67aeb38 Binary files /dev/null and b/docs/img/units/owl_10.png differ diff --git a/docs/img/units/owl_5.png b/docs/img/units/owl_5.png new file mode 100644 index 0000000..cd08d8e Binary files /dev/null and b/docs/img/units/owl_5.png differ diff --git a/docs/img/units/paladin_1.png b/docs/img/units/paladin_1.png new file mode 100644 index 0000000..96dcbc1 Binary files /dev/null and b/docs/img/units/paladin_1.png differ diff --git a/docs/img/units/paladin_10.png b/docs/img/units/paladin_10.png new file mode 100644 index 0000000..a5f3b99 Binary files /dev/null and b/docs/img/units/paladin_10.png differ diff --git a/docs/img/units/paladin_5.png b/docs/img/units/paladin_5.png new file mode 100644 index 0000000..3b3ddf9 Binary files /dev/null and b/docs/img/units/paladin_5.png differ diff --git a/docs/img/units/priest_1.png b/docs/img/units/priest_1.png new file mode 100644 index 0000000..5fa9c53 Binary files /dev/null and b/docs/img/units/priest_1.png differ diff --git a/docs/img/units/priest_10.png b/docs/img/units/priest_10.png new file mode 100644 index 0000000..041fef6 Binary files /dev/null and b/docs/img/units/priest_10.png differ diff --git a/docs/img/units/priest_5.png b/docs/img/units/priest_5.png new file mode 100644 index 0000000..a558984 Binary files /dev/null and b/docs/img/units/priest_5.png differ diff --git a/docs/img/units/priestess_1.png b/docs/img/units/priestess_1.png new file mode 100644 index 0000000..dcaaa0c Binary files /dev/null and b/docs/img/units/priestess_1.png differ diff --git a/docs/img/units/priestess_10.png b/docs/img/units/priestess_10.png new file mode 100644 index 0000000..bd722df Binary files /dev/null and b/docs/img/units/priestess_10.png differ diff --git a/docs/img/units/priestess_5.png b/docs/img/units/priestess_5.png new file mode 100644 index 0000000..50b41d1 Binary files /dev/null and b/docs/img/units/priestess_5.png differ diff --git a/docs/img/units/revenant_1.png b/docs/img/units/revenant_1.png new file mode 100644 index 0000000..2946ac9 Binary files /dev/null and b/docs/img/units/revenant_1.png differ diff --git a/docs/img/units/revenant_10.png b/docs/img/units/revenant_10.png new file mode 100644 index 0000000..65c46c3 Binary files /dev/null and b/docs/img/units/revenant_10.png differ diff --git a/docs/img/units/revenant_5.png b/docs/img/units/revenant_5.png new file mode 100644 index 0000000..34b4b1b Binary files /dev/null and b/docs/img/units/revenant_5.png differ diff --git a/docs/img/units/spider_1.png b/docs/img/units/spider_1.png new file mode 100644 index 0000000..a7fab76 Binary files /dev/null and b/docs/img/units/spider_1.png differ diff --git a/docs/img/units/spider_10.png b/docs/img/units/spider_10.png new file mode 100644 index 0000000..64ec3f5 Binary files /dev/null and b/docs/img/units/spider_10.png differ diff --git a/docs/img/units/spider_5.png b/docs/img/units/spider_5.png new file mode 100644 index 0000000..aedfe72 Binary files /dev/null and b/docs/img/units/spider_5.png differ diff --git a/docs/img/units/worker_1.png b/docs/img/units/worker_1.png new file mode 100644 index 0000000..096fd02 Binary files /dev/null and b/docs/img/units/worker_1.png differ diff --git a/docs/img/units/worker_10.png b/docs/img/units/worker_10.png new file mode 100644 index 0000000..c703aa0 Binary files /dev/null and b/docs/img/units/worker_10.png differ diff --git a/docs/img/units/worker_5.png b/docs/img/units/worker_5.png new file mode 100644 index 0000000..d919c04 Binary files /dev/null and b/docs/img/units/worker_5.png differ diff --git a/docs/img/units/wyrm_1.png b/docs/img/units/wyrm_1.png new file mode 100644 index 0000000..348fea0 Binary files /dev/null and b/docs/img/units/wyrm_1.png differ diff --git a/docs/img/units/wyrm_10.png b/docs/img/units/wyrm_10.png new file mode 100644 index 0000000..e0fead3 Binary files /dev/null and b/docs/img/units/wyrm_10.png differ diff --git a/docs/img/units/wyrm_5.png b/docs/img/units/wyrm_5.png new file mode 100644 index 0000000..0c5f79f Binary files /dev/null and b/docs/img/units/wyrm_5.png differ diff --git a/docs/screenshots/bestiary.png b/docs/screenshots/bestiary.png new file mode 100644 index 0000000..7e33d4f Binary files /dev/null and b/docs/screenshots/bestiary.png differ diff --git a/docs/screenshots/bridge-battle.png b/docs/screenshots/bridge-battle.png new file mode 100644 index 0000000..4c984a5 Binary files /dev/null and b/docs/screenshots/bridge-battle.png differ diff --git a/docs/screenshots/dark-rooms.png b/docs/screenshots/dark-rooms.png new file mode 100644 index 0000000..a889dd1 Binary files /dev/null and b/docs/screenshots/dark-rooms.png differ diff --git a/docs/screenshots/dungeon-heart.png b/docs/screenshots/dungeon-heart.png new file mode 100644 index 0000000..70ed8c7 Binary files /dev/null and b/docs/screenshots/dungeon-heart.png differ diff --git a/docs/screenshots/dwarf-cave.png b/docs/screenshots/dwarf-cave.png new file mode 100644 index 0000000..c5eca01 Binary files /dev/null and b/docs/screenshots/dwarf-cave.png differ diff --git a/docs/screenshots/fog-of-war.png b/docs/screenshots/fog-of-war.png new file mode 100644 index 0000000..3bad60f Binary files /dev/null and b/docs/screenshots/fog-of-war.png differ diff --git a/docs/screenshots/forge-and-traps.png b/docs/screenshots/forge-and-traps.png new file mode 100644 index 0000000..3c36fe5 Binary files /dev/null and b/docs/screenshots/forge-and-traps.png differ diff --git a/docs/screenshots/mana-spring.png b/docs/screenshots/mana-spring.png new file mode 100644 index 0000000..e987c08 Binary files /dev/null and b/docs/screenshots/mana-spring.png differ diff --git a/docs/screenshots/rune-forge.png b/docs/screenshots/rune-forge.png new file mode 100644 index 0000000..dff1867 Binary files /dev/null and b/docs/screenshots/rune-forge.png differ diff --git a/docs/screenshots/title-battle.png b/docs/screenshots/title-battle.png new file mode 100644 index 0000000..3546bbe Binary files /dev/null and b/docs/screenshots/title-battle.png differ diff --git a/play/DungeonCraft.html b/play/DungeonCraft.html new file mode 100644 index 0000000..d8e141c --- /dev/null +++ b/play/DungeonCraft.html @@ -0,0 +1,262 @@ + + + + + DungeonCraft + + + + + + + + + + + + + + + diff --git a/src/gfx.rs b/src/gfx.rs index 055083f..97a1dc6 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -1975,3 +1975,50 @@ impl Gfx { } } } + +#[cfg(test)] +mod doc_images { + use super::*; + + // writes a picture scaled up with nearest pixels + fn save(img: &Image, src: Rect, scale: u32, path: &str) { + let (w, h) = (src.w as u32 * scale, src.h as u32 * scale); + let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0)); + for y in 0..h { + for x in 0..w { + let p = img.get_pixel(src.x as u32 + x / scale, src.y as u32 + y / scale); + out.set_pixel(x, y, p); + } + } + out.export_png(path); + } + + // `cargo test --release export_doc_images -- --ignored` refreshes the pictures in docs/img + #[test] + #[ignore] + fn export_doc_images() { + std::fs::create_dir_all("docs/img/units").unwrap(); + std::fs::create_dir_all("docs/img/icons").unwrap(); + let sprites = gen_sprites(); + for &k in SPRITE_KINDS.iter() { + let id = crate::game::st(k).name.trim_start_matches("unit."); + for (lv, tag) in [(1u32, "1"), (5, "5"), (10, "10")] { + save(&sprites, Gfx::sprite_cell(k, false, lv), 8, &format!("docs/img/units/{}_{}.png", id, tag)); + } + } + let atlas = gen_atlas(); + for (k, t) in crate::traps::TRAP_KINDS.iter().enumerate() { + save(&atlas, Gfx::cell(I_TRAPS + k as u32), 4, &format!("docs/img/icons/{}.png", t.id())); + } + for (k, s) in crate::spells::SPELLS.iter().enumerate() { + save(&atlas, Gfx::cell(I_SPELLS + k as u32), 4, &format!("docs/img/icons/{}.png", s.id())); + } + for (id, cell) in [("lightning", I_BOLT), ("heal", I_HEAL), ("summon", I_SUMMON), ("arrow", I_ARROW), ("cannon", I_CANNON), ("magic", I_MAGIC), ("rune", I_RUNE)] { + save(&atlas, Gfx::cell(cell), 4, &format!("docs/img/icons/{}.png", id)); + } + for (k, id) in crate::game::ROOM_NAMES.iter().enumerate() { + let tex = crate::ui::tool_info(crate::ui::Tool::Room(k)).icon; + save(&atlas, Gfx::cell(tex), 4, &format!("docs/img/icons/{}.png", id.trim_start_matches("room."))); + } + } +} diff --git a/tools/make_bestiary.py b/tools/make_bestiary.py new file mode 100644 index 0000000..35265fc --- /dev/null +++ b/tools/make_bestiary.py @@ -0,0 +1,130 @@ +#!/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'{name}, level 1 ' + f'{name}, level 5 ' + f'{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'| | **{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", + "", + '

The bestiary in the game

', + "", + "> *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']}", "", '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'| | **{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'| | **{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")