Custom Roguelike - 9/20/26 — The Town Comes Alive: Quests, the Witch, Blades, Difficulty Tiers, and the Final Demon
Published 2026-09-22
9/20/26
Town: a real dungeon gatehouse, and the shop hint only next to the goods
From the user’s screenshot of the north end of the town: the dungeon entrance was one small archway tile in a low stone wall and easy to miss, and the “Stand next to an item, press ENTER to buy it.” text showed at the top of the screen everywhere in town. Gate: it is now a stone gatehouse - an 11-wide slate roof (two rows) over a stone wall with the archway (the Exit tile) as its doorway at the bottom, the main street running straight up to it (it now starts below the gatehouse), lanterns either side and low stone walls extending left and right. Hint: the town reuses the old shop’s ShoppingActive mode for the whole map, and systems/hud.rs printed the buy hint whenever it was on; the Battle Arena’s hint is unchanged (always shown - its shop is one small room), but in the town it only shows while the player is standing next to the goods (shop_item_near, the same 5-tile adjacency the buy key uses; the HUD system already declared every component that needs). Throwaway test (removed): no hint at the start, a hint in front of the goods, the player still reaches the new gate (107 iterations), and the bot completes runs; the permanent HUD and movement access-panic tests pass with the change. Zero warnings; the game launches. Not verified on screen.
Town phase 3 begins: townsfolk rotate, quest-givers are always loaded
The user approved Phase 2 (merged into town-quests, fast-forward) and asked that not every townsperson be loaded every time: the Blacksmith and Shopkeeper always, the others random, but anyone the player has a quest with must be loaded. Built on the new branch town-phase3-townsfolk: spawner::pick_townsfolk(rng, pool, must_appear, how_many) (the required people first, then random others up to the count, no duplicates, only people in the pool), RANDOM_TOWNSFOLK (Elder, Townswoman, Townsman, Young Child), TownsfolkKind::home_house (each person has their own house doorstep; all six houses now have one, so a future roster can grow), and enter_town loading the Blacksmith plus 1-2 random people, with a must_appear list (empty until the quest system exists) for anyone with an active quest. Throwaway test (removed): over 2000 simulated visits everyone appears about equally (716-750 each), a required person is always included even beyond the usual count, no duplicates, the Blacksmith is not in the random pool, and over 60 real town entries there were 1 or 2 visitors (30 each), the Blacksmith always at his anvil and every visitor outside their own house. Zero warnings; the game launches. When the user’s new townsfolk arrive, adding one is a TownsfolkKind variant + home_house + sheet rows + an entry in RANDOM_TOWNSFOLK.
Town phase 3: talking by proximity
The user said no quest animations for now and proximity next, and described a cat. Proximity: new components/talk.rs - a Talkable { radius } component (range counts diagonals), TOWNSFOLK_TALK_RADIUS = 1, SHOPKEEPER_TALK_RADIUS = 2 (the wider range the user asked for, so the plank apron in front of his counter counts), within_talk_range, talk_greeting (placeholder line per person - the quest system replaces them) and wrap_words. spawn_townsperson adds Talkable; the town’s shopkeeper gets it only for the town layout (the Arena’s has nothing to say). systems/hud.rs draws, for the nearest person in range, a Swamp-frame PanelBox across the top (name on the border, the words inside) and declares #[read_component(Talkable)]. Throwaway test through the real schedule (removed): diagonals count, the apron in front of the shopkeeper is in range and 3 tiles away is not, the blacksmith is talkable from an adjacent tile and not from 2-3 away, nobody at the start, the HUD system ran with the panel path for both kinds of person without a component-access panic, and the Arena has no Talkable. Permanent HUD/movement/effect access tests pass; zero warnings; the game launches. Not verified on screen (panel position at HUD row 3, 48 wide). The cat: 2% chance per town visit, “Meow.” nearby, scratching it behind the ears makes it follow the player and turn up on the next dungeon level, where finding it finds a stash of potions - written into the design doc with six open questions (scratch action, stash size, what if it is missed, how far it follows, solidity, art) and added to the backlog as item 17.
Town phase 3: the quest system, first quest
The user described the interaction: next to a person a box shows the quest and ENTER accepts; the same box later shows the reward and ENTER turns it in, or flavor text about giving up and ENTER cancels; the cat is the same thing but a hidden quest (you are not told anything happened, but the slot is taken - no new quest, no cancelling), auto-cancelled if you leave the floor it appears on without finding it, and it only appears on the very next dungeon floor. Built: new src/quests.rs - Quest, ActiveQuest, QuestGoal::KillEnemies, the QuestLog resource (one active quest, inserted wherever Stats is: State::new, start_game, start_arena, return_to_title, the HUD test), quest_offered_by (the Townsman: kill 5 enemies -> 2 Healing Potions), and talk_view - the single function that decides what the box says and what ENTER does. The HUD (systems/hud.rs, new #[resource] quest_log) draws that box (56 wide: name on the border, lines, a green prompt line) plus a cyan quest tracker top right; nearest_talker (in components/talk.rs, generic over EntityStore) is shared. Kills count in the three kill paths: State::record_enemy_kill (battles), systems/traps.rs and systems/use_items.rs (new #[resource] quest_log). ENTER is handled in the frame loop (State::talk_to_nearest, called from the AwaitingInput arm of tick) before the input systems, and swallowed if it acted so it cannot also buy; guarded by pending_enter_release against a held/auto-repeating ENTER. (First attempt put it inside the player_input system, which broke - legion allows only 8 resources per system and it already had 8 - so it moved to tick.) Throwaway test through the real game state and schedules (removed): far away ENTER does nothing; the offer, accept, tracker “Kill 5 enemies (0/5)”, the in-progress “give up” box, kills capped at the target, the complete box, turn-in (+2 Healing Potions, 3 -> 5) and the free slot, a fresh quest then give-up (no reward), a hidden quest (no tracker, cannot be given up, others say “you already have a quest”), the HUD ran in every state without a component-access panic, and the bot’s Hunter/Amazon runs (trap/Shoot kill counting systems) still complete. The permanent tests pass; zero warnings; the game launches. Not verified on screen (the box is 56 wide at HUD row 3; the tracker at row 3, right). Not verified live: the held-ENTER guard (it needs the real window). The bot ignores quests, so the balance numbers are unaffected. Design doc updated with the interaction model and the cat’s answers (hidden quest, auto-cancel, next floor only).
Correction to the entry above: the previous commit went in with the permanent HUD regression test FAILING (my command chain committed without checking the test result). The test builds its own resource set and did not have the new QuestLog the HUD system now takes - legion panics on a missing resource at runtime, which is exactly what that test exists to catch. Fixed in the next commit, and the test now also puts a person in talk range and an active quest in its world so the talk box and the quest tracker are covered permanently. All 3 normal tests pass.
Town: the shopkeeper can never be in front of an item
The user wanted to be sure the shopkeeper is far enough right that he is never in front of an item. Before this it only held because there were two potions at the far left; more stock would have crept toward him. Now it is a rule the map builder enforces: the shopkeeper’s spot moved to the far right end of the counter’s pocket (the template’s S, counter tiles x 23-28, him at 28), and MapBuilder::new_town only places stock on counters at least TWO tiles to his left (c.x + 2 <= shopkeeper.x), so at most 4 items fit; asking for more now fails an assert_eq! with a clear message instead of silently putting an item in front of him (the caller zips items with points, so a shortfall would have DROPPED items). Throwaway test (removed): for 0-4 items every item is 2+ tiles left of him and never on his column; 5 and 6 items panic as intended; the plank apron in front of him is walkable (so his wider quest range still works from the front). Permanent tests pass, zero warnings, the game launches.
Town: townsfolk face the player
The user reported not seeing the townsfolk turn when the player is to their left or right - I had listed “facing the player” as still to do, and they expected it. Built: facing_toward(from, to) (the dominant axis wins: East/West when the player is more to the side, North/South otherwise, South when tied or on top of them) and TOWNSFOLK_NOTICE_RADIUS = 3 tiles in components/animation.rs; State::face_townsfolk_toward_player (called every frame from the AwaitingInput arm of tick, before the input systems) swaps each townsperson’s IdleAnimation frames to the matching facing’s sheet row while the player is within that radius and back to South when not, keeping frame_index/elapsed_ms so the loop does not restart mid-turn (the same idea systems/movement.rs uses for walkers). Throwaway test (removed): the pure rule for the four directions, ties and standing on them; a Townsman turns East/West/North/South as the player moves to each side, back to South when far away, and his frame index/timer are kept through the turn; every facing has real art on the sheet. Permanent tests pass; zero warnings; the game launches. Not verified on screen. (The sheet also grew here: the cat’s rows were appended - see the next entry.)
The town cat is in
The user delivered Town_Cat.zip (orange tabby, 3 animations x 4 facings: Licking 12 frames on a 48x48 canvas = quest available, Sitting 8 = quest accepted, Yawning 11 = seen in the dungeon) and answered the open questions: missing the cat in the dungeon auto-cancels the quest, and it only appears on the very next dungeon you go to; sounds to find it in the dungeon come later (added to the backlog, item 17). Art: the cat’s frames were added to the shared sheet (shopkeeper_idle.png, now 69 rows, 8832 px tall - past the 8192 of the townsfolk rows; existing sheets go to 7680 wide, so this leans on the GPU supporting a taller texture, which every current GPU does but is worth knowing for the Steam release): 9 frame columns per row, so the longer loops span two rows (idle_frames_for_cat); South facing only for now, laid out in 5-row facing blocks so the other facings can be added later without renumbering; Licking trims about 2 px (its cat is 36 wide). Mechanics (all on the quest machinery): TownCat marker, CatAnimation, spawn_cat (solid, talkable radius 1), QuestGoal::FindCat with cat_quest() (hidden, reward 3 Healing Potions - my default, easy to change), Quest gained accept_prompt/turn_in_prompt, QuestLog::cat_waiting/place_cat_on_floor/cat_missed, TOWN_CAT_CHANCE_PERCENT = 2 (only while the quest slot is free) in enter_town, placement in advance_level (a random open tile 12-90 steps from the start, not on an enemy), cancellation in dungeon_shop_transition, and talk_to_nearest switching the cat to Sitting on scratch (no message) and removing it after the reward. Throwaway test through the real game state (removed): the offer (“Meow.” / “Scratch behind its ears?”, no reward shown), scratching (hidden - no tracker; slot taken; cat sits; nothing more to do with it and no way to cancel; another giver cannot hand out a quest), the next floor has exactly one yawning cat 58 steps from the start on open ground, finding it offers “Press ENTER to take the potions” and pays 3 potions (3 -> 6) and the cat leaves, missing it cancels and there is no cat on the floor after, and over 3000 town visits the cat appeared 55 times (~1.8%) and never while a quest was active. (My first frequency check failed only because the test did not clear the town between visits.) Permanent tests pass, zero warnings, the game launches. Not verified on screen.
Overnight prep: quest text, quest items and where they go
The user went to sleep and asked me to get ready for the next steps: the flavor text for all the quests, and the logic for placing the items in the dungeons - the Blacksmith’s item inside the prefabs, everything else (the cat included) in random places. Text: new docs/quest_text.md with every line (offer, what to do, reward, complete, give-up, greeting) for the Townsman, Townswoman, Elder, Blacksmith, Shopkeeper, Young Child (five rotating lines, one of which hints at the cat), and the cat - written in a deliberately neutral low-fantasy village voice because the game’s story (backlog item 13) is not decided; the code holds the same strings, so an edit is a one-line change. Quest core reshaped (src/quests.rs): goals KillEnemies, FindItem, KillAndFetch, FindCat; Reward::{Items, RandomClassAbility}; Quest gained offer_line; ActiveQuest gained item_found (and ActiveQuest::new); QuestLog gained visits (people rotate their chatter by town visit, talk_greeting(name, visit)), item_found, items_to_place; the tracker text now reads e.g. “Kill 3 enemies and find the Fortress Ingot (3/3 kills, item not found)”. Items: QuestItemKind (Glowing Mushroom, Ancient Scroll, Fortress Ingot, Lost Crate) + QuestItem marker + spawn_quest_item; MapBuilder::random_open_spot and quest_item_spot (Prefab items at the prefab room’s weapon spot; Random ones 8-90 steps from the start, reachable, off enemy/guard spots); advance_level places the active quest’s item on every floor until found and shares the helper with the cat; pickup is detected in systems/movement.rs (new #[read_component(QuestItem)] and #[resource] quest_log - the movement test’s resources got a QuestLog, since a missing resource only panics at runtime); turn-in and give-up take the item out of the pack (State::pay_reward, remove_carried_quest_items). Offered now: Townswoman (mushroom -> 2 Healing Potions + 1 Dungeon Map) and Elder (scroll -> 5 copies of ONE random ability from the class’s own pool - checked for all five classes: Deathblow, Invisible Cloak, Stealth, Battle Cry, Arrow Volley in the test run). Prepared, not offered: Blacksmith and Shopkeeper - what they pay needs Phase 4. Throwaway tests through the real game state (removed): both item quests end to end (offer, accept, tracker, the item on the next floor 44 steps out and again on the floor after until found, pickup through the real movement system, the “found - bring it back” tracker, the complete box, the payout, the item leaving the pack), giving up dropping the item, 300 generated floors (the prefab item is always at the prefab spot and random ones reachable, apart, 8-90 steps out), the Blacksmith machinery (the ingot on the class weapon’s tile with 5 guards within 8 tiles; kills alone do not complete it, kills plus the ingot do), and the bot still completing runs. The permanent access tests pass; zero warnings; the game launches. Not verified on screen; the item symbols are placeholders (checked they have bright pixels in the font). Decisions waiting for the user: the wording, the rewards (potion counts, the Elder’s random ability), and how the Blacksmith/Shopkeeper quests should pay out.
Bot regression found and fixed: solid things in the town
The overnight balance runs showed 1-5 timeouts per 1500-2500 runs where the earlier reports had none. A stall log that names the entity next to the stuck bot found it: names=["Cat"] obstacle=true - in the TOWN, a (solid) town cat sat on a tile the bot’s route passes through, and the bot’s pathfinding only knows about walls, so it walked into the cat forever (a person just walks around it). A bot limitation, not a game bug (the town’s grass areas are wide and the streets are 3 wide, so a single solid cat can never seal a route), but it showed the bot must treat solid entities as obstacles: step_toward now routes around every Obstacle (a townsperson, the cat) with its own BFS (bfs_field_avoiding). The stall log also names whatever is on the neighbouring tiles now, and prints whether shopping mode is on. A deterministic test (removed): with two cats across the street ahead and an Elder further up, the bot still reaches the gate in the same 107 iterations.
Phase 4 finished: potions, chests, kill gold, shop stock, Shopkeeper quest
Speed Potion (ProvidesEffect::SpeedBoost, Hasted component: +50% ATB fill rate for 20 of the player’s actions) and Defense Potion (reuses the ice-armor effect, +1 defense for 20 attacks); both are shop-only. Chests now give a Dungeon Map, 1-2 RANDOM potions (Healing/Speed/Defense) and 15-30 gold. Kill gold is Arena-only: the new KillGold marker on the Arena player gates the three kill paths (battle/resolve.rs, systems/traps.rs, systems/use_items.rs); a Dungeon Crawl player’s only gold is chest gold. Town shop: 4 items on the counter (Healing Potion 5 in stock at 5g, Speed and Defense Potion 2 at 8g, Dungeon Map 2 at 8g); the Arena’s shop is untouched. Shopkeeper quest (Lost Crate, random place in the dungeon): reward Reward::ShopPerk = +2 of every item and 1 gold cheaper for the rest of the run (QuestLog::shop_perk). Text in docs/quest_text.md (Blacksmith and Shopkeeper now marked BUILT). Balance (200 runs per class, ChestWeapon route): Barbarian 81%, Mage 79%, Rogue 74%, Amazon 73%, Hunter 80% (overall 77%). First try with chest gold 10-20 gave 70% overall (Rogue 63%), so it was raised to 15-30. The bot does not buy or use Speed/Defense potions or do the Shopkeeper quest (Phase 6). Not verified on screen: the four-item counter and the new potion glyphs (placeholders).
New item icons in the game, Debug kit
The user liked the Pixellab candidates. New branch item-icons. The abilities all already had finished icons in dungeonfont.png (checked all 29 non-weapon items in template.ron - every real one is covered), so only the NEW items got art: Speed Potion, Defense Potion, Glowing Mushroom, Ancient Scroll, Fortress Ingot, Lost Crate, and the Blade of Death (cell reserved for phase 5). tools/place_icons.py writes them into blank cells (asserting the cells were blank and that nothing else changed); the glyph map lists the cells. The quest items’ old placeholder glyphs collided with Garrote and Arrow Volley, so they moved, and quest items now draw untinted (they were multiplied by gold before). Debug class: 3 of every potion (Healing, Speed, Defense) and Dungeon Maps in its kit, and one of each quest item lying on the floor to the right of its start, so every new icon can be seen at once. Not verified on screen.
Correction and clarification (same day): the Debug kit is now just the two NEW potions (3 Speed, 3 Defense) on top of what it had - I had over-reached (extra Healing Potions, quest items on the floor); both removed. I also briefly made hidden (Invisible/Stealth) players unable to take ANY quest item; the user only meant the Blade of Death at the end of the dungeon (the ingot and the other quest items may be taken while hidden), so that rule was reverted. The Blade has NO placement logic yet - only its icon cell; that is Phase 5 (floor 3’s boss guards it), and when it is built it must not be takeable while Invisible or Stealthed.
The Witch, herb/horn/feather, chests, ability icons, Debug starts in town
The Witch (TownsfolkKind::Witch, house 4 - the town has six houses and four were taken; placeholder art borrows the Townswoman’s rows until hers arrive) is the only quest giver besides the Shopkeeper who pays Speed/Defense Potions, per the user. She has a chain of three quests offered in order (QuestLog::completed counts hand-ins per giver): Healing Herb -> 3 Healing Potions, Ivory Horn -> 3 Defense Potions, Rainbow Feather -> 3 Speed Potions (icons from candidates 44, 46, 54; cells ¡ « »). Text is in quests.rs (add to docs/quest_text.md when reviewing). Bug fixed on the way: enter_town‘s must_appear list was still empty from before quests existed, so a person holding your active quest might not be in town to take it back; it now always includes the active quest’s giver. Chests now give 0-1 potions (random among the three) and 25-45 gold - gold is what chests are for. Balance (200 runs/class): Barbarian 82%, Mage 84%, Rogue 76%, Amazon 78%, Hunter 80% (80% overall); a later 100-run check after the icon work: 72-84%, 79% overall. Ability icons: all 25 Pixellab ability icons replace the old ones in their cells; Freeze Trap moved off Trap’s shared T to its own ¥ cell, and placed traps draw untinted. Debug now starts in the town like every class (the dead skip-the-town branch of start_game is gone); the Theme Select screen no longer offers Random (Random is the standard crawl) and just forces the chosen theme on the floors after the town. Not verified on screen: the new icons at ability-bar size, the placed traps, the Debug town start.
Feedback round after the user’s playtest
Witch art from Witch.zip (48x48 exports, 3 animations x 4 facings x 9 frames) placed by tools/place_witch.py on rows 69-80 of shopkeeper_idle.png (now 10368 px tall - the Steam GPU-texture note grows; existing rows checked byte-identical); one 32x32 window per animation+facing, feet on the cell bottom. townsfolk_row special-cases her (the townsfolk block’s row maths would have run into the cat’s rows). Rules from the playtest: after a hand-in a person offers nothing more until the player has been to the dungeon and back (QuestLog::turned_in_this_visit, cleared in enter_town); the Townsman, Townswoman and Elder have ONE quest per run each (QuestLog::completed); the person who holds your active quest is always in town AND 1-2 other people are too (before, the holder used up one of the random slots). The Blacksmith is exempt from the per-visit rule: with it the bot’s win rate fell from ~78% to ~57% (it could no longer take the tier-2 quest in the same visit as the tier-1 hand-in, so tier 2 was out of reach in 3 floors) - the user may flip this. Debug: the Next Level item now acts like the Exit tile (dungeon floor -> town, town -> next floor); the use_items system got #[resource] shopping and its permanent test the resource. Icons: the Stealth icon now has two eyes (two_eyes edit in tools/place_icons.py, mirrors the left half). Town: the dirt lane tile under each south house’s stone doorstep is now street cobble (only one of stone/dirt). Verified through the real game state (throwaway test, removed): Townswoman hand-in pays 2 Healing + 1 Map and never offers again; Witch chain pays 3 Healing / 3 Defense / 3 Speed in order, blocked the same visit, none after the chain; Blacksmith swaps Rusty -> Shiny Sword (tier 1); Shopkeeper perk flag set; over 60 town visits with an active Townsman quest he is always there plus another person; Debug starts in the town; the Arena player has KillGold and a crawl player does not. Balance with the exemption: 77% overall (100 runs/class). Not verified on screen: the Witch on the sheet in game, the two-eyed icon, the lane change.
Kill quests target the toughest enemy; town buildings
The Townsman’s quest was “kill 5 of anything”, far too easy. It is now a bounty on the toughest ordinary (non-boss) enemy of the next dungeon floor: 5 Orcs, then 5 Ogres, then 3 Ettins (Templates::top_enemy_name_for_level, same “toughest” rule the chest guards use; QuestLog::bounty_target is set on every town visit from the floor about to be entered, so talk_view never reloads templates; QuestGoal::KillNamed; every kill site now passes the enemy’s name to record_kill). Measured supply first (150 generated floors per level): median 21 Orcs / 15 Ogres / 13 Ettins per floor, and even the worst floor had 4 / 5 / 2 - hence 3 for the Ettin. The Blacksmith’s “kill 8 of any enemy” is unchanged (he also needs the ingot). Town art (template only, no new tiles needed): the smithy’s walls are one row shorter (roof unchanged, two shuttered windows added); the Witch’s house now has the smithy’s slate roof, chimney and stone walls; the shop’s hanging sign moved off the wall to a signpost on the ground beside it. Regenerated the whole-town preview to check. Not verified in game.
The shop building is one row shorter too (the roof moved down a row, the wall row with the windows is gone, and the two windows now sit on the awning’s row beside it); the stall, counter and shopkeeper rows are unchanged. Checked in the town preview.
The Blacksmith is no longer a quest; the shop sign is on the ground; the Stealth eyes
Blacksmith: the user’s idea - a good player has to finish the weapon upgrades, so they should never have to give the quest slot to the smith. The upgrade is now a standing exchange: while the weapon is below its top tier a Fortress Ingot is placed in the prefab room on every floor (QuestLog::items_to_place) until the player carries one (QuestLog::has_ingot, set when the ingot is picked up); next to the Blacksmith, talk_view offers TalkAction::HandInIngot whatever quest is active, and State::talk_to_nearest pays the upgrade and removes the ingot. The old “kill 8 enemies and fetch” quest, QuestGoal::KillAndFetch and the Blacksmith’s exemption from the once-per-visit rule are gone. Throwaway test through the real state (removed): with a Townswoman quest active throughout, hand-ins go Rusty -> Shiny -> Huge Sword, the other quest is untouched, no ingot is placed while one is carried or at the top tier, and at the top tier he takes nothing. The bot needed only its “pending”/“visit” checks adapted. Balance (200 runs/class): Barbarian 78%, Mage 78%, Rogue 72%, Amazon 78%, Hunter 76% (76% overall). Shop sign: the user was right that I had only moved a generic signpost; the shop’s real hanging sign now stands on the ground beside the shop - new tile art (tools/make_shop_sign_tile.py rewrites the sign’s tile cell: grass, a post, a bracket arm and the potion board cut from the wall-mounted version). Stealth icon: my first “two eyes” only split the one eye in half; the icon now has the old eye cleared and two separate slit-pupil eyes with a 6px gap. Not verified in game.
Phase 5A: difficulty tiers as data (first pass)
New resources/tiers.ron + src/tiers.rs: one entry per dungeon difficulty (Crawl, 1-4 Blades; the 5 Blades dungeon and Endless use the last entry) with multipliers for enemy health/damage, boss health/damage, an additive enemy defense and chest gold - the tier is the run’s DungeonTier, set into a thread-local by start_game and reset to Crawl by start_arena (the Arena never scales; checked). Applied where things are created: Templates::load (enemy hp/base_damage), spawn_entity (a Defense component when the tier has enemy_defense > 0), chest_gold_range. The simulation plays any tier with SIM_TIER=n and labels the ledger “real @ <tier name>”. First sweep (100 runs/class): Crawl 79%; my first ladder (health x1.3, damage x1.2) gave 5% at 1 Blade - the numbers are tiny integers (enemy damage 1-3), so damage x1.2 rounds up to the next whole hit and health x1.3 becomes ~x1.5; health x1.15 = 72%, x1.25 = 47%, damage x1.5 = 3%. So health is the only smooth lever and there is ~15-20% of headroom above Crawl before the informed route drops under 50%: five tiers need the Blade buff (player growth per Blade owned) or other levers (more/finer stats, gold, quest rewards) - the ladder in tiers.ron is a health-only placeholder, tiers 2+ unbalanced. Next: the Blade buff as a sim knob, then retune.
Class animation swap
The user delivered Rogue.zip, Amazon.zip and Barbarian.zip and asked for the replacement list from the art notes: Barbarian Walk + Death, Rogue Walk, Amazon everything (Hunter Attack is still waiting for its zip). New tools/place_class_animations.py does it with the sheets’ standing conventions and asserts nothing else changed; the exact rows are in the glyph map doc. The Barbarian zip has two Walk folders: the plain Walk only has east/south, Walk-b0eb0b66 has all four facings at native 32x32 - used that one. Measured what the 32px crop loses (under 2% of pixels everywhere). Checked a preview of the new rows, the game launches without a panic, and the permanent tests pass. Not verified in game.
Second animation delivery: the user forgot to delete Barbarian’s old Walk from the first zip and re-sent Barbarian.zip plus Hunter.zip. Barbarian Walk (and Death) re-placed from the new zip, Hunter Attack added, and Amazon’s new Victory_Stairs put on character_victory.png row 12 - I had wrongly written that nothing used Victory_Stairs; it is the forest victory screen’s climb-away pose (rows 8-13, already wired), and the other classes’ stairs in the new zips were identical to what the sheet already had. tools/place_class_animations.py gained a second batch. The new zips also hold different versions of animations that were not on the list (Barbarian Attack/Defend/battle idle/Whirlwind/Victory, most Hunter animations incl. a 32x32 Walk); left alone pending the user. Launches without a panic, tests pass; not verified in game.
Mage was the balance outlier because of Ice Armor
The user asked whether Ice Armor gives too much defense. Tested with Mage alone at 4 Blades (200 runs): Ice Armor at full strength (+14 after the x7 scale) 71%, half 20%, off 0% - so yes, it decides Mage’s whole run: its flat defense cancels low-damage hits completely (Orc damage 7 vs +7 net defense), and the tiers raise enemy HEALTH but not damage, so the armor never loses value while everyone else takes more hits in longer fights. The duration matters far less than the bonus (bonus 14 for 12 hits: 56%). Bonus 10 = 37% at 4 Blades and 75% in Crawl. Added ice_armor_scale (0.7) to tiers.ron, applied on top of the stat scale to Ice Armor’s defense bonus only (the Arena is unscaled); TierTable::load is now cached per thread (it was re-reading the file on every enemy spawn). Full sweep (100 runs/class): Crawl 73/74/78/74/75 (Barbarian/Mage/Rogue/Amazon/Hunter), 1 Blade 54/66/58/70/68, 2 Blades 55/75/69/74/65, 3 Blades 59/62/59/56/46, 4 Blades 26/38/44/43/19 - overall 75, 63, 68, 56, 34. All classes sit in one band per tier now; Hunter and Barbarian are the lowest at 4 Blades. The tier ladder is still a first calibration (1 Blade below 2 Blades is noise at 100 runs).
Phase 5B: the Blade of Death
The last floor of a dungeon no longer has the Amulet. spawn_blade_of_death puts a BladeOfDeath { post } entity on it - no Point, so it is invisible and untouchable - and systems/end_turn.rs gives it its post (the spot where the guarding boss was posted, amulet_start) once no Boss is alive; stepping onto it wins the run (TurnState::Victory). So the guard must be killed and there is nothing to walk around: no pickup rule, no hidden-can’t-take rule (both dropped). It is not an Item (never enters the pack). The floor-3 bosses get a SeesThroughStealth marker (advance_level); systems/chasing.rs no longer returns early for a hidden player, it skips only enemies WITHOUT the marker, so those bosses hunt a Stealthed/Invisible player, and when they start the fight the battle log opens with “Foolish <class>, your tricks won’t work on me.” (once, first line of the battle). Progress: Progress::award(tier) (Crawl earns Blade 1, “N Blades” earns Blade N+1, once each; the 5 Blades dungeon unlocks Endless; Endless earns nothing), Progress::save(), Progress::finished(tier), BladeAward. State::victory awards on its first frame (blade_award, reset in return_to_title; the Debug class’s cheat Victory item earns nothing) and shows “Blade N of 5 is yours - a harder dungeon opens.” / “already own” / “Endless lies open”; the difficulty screen marks finished tiers with “[Blade earned]”. The bot’s last-floor goal is the Blade’s post. Verified with a throwaway test through the real systems (kept as the permanent systems::blade_flow_tests): hidden while a boss lives, appears at the post after the last boss dies, Victory on stepping onto it, all the award rules, the boss chasing a Stealthed Rogue (with the taunt) and an ordinary enemy not. Balance after the change (100 runs/class, Crawl): 73% overall, no timeouts. Not verified on screen: the victory text and the “[Blade earned]” marks; note the taunt only appears if the battle starts while the player is hidden.
Change after the user’s feedback (same day): the Blade of Death is no longer invisible while the boss lives (a player who ran from the boss for a while could have lost track of it). It lies visible at the boss’s post from the start (BladeOfDeath is now a marker component next to a normal Point); systems/end_turn.rs only lets standing on it win the run once no Boss is alive. The permanent blade_flow_tests now checks “standing on it with the boss alive does nothing, with the boss dead wins”. The rest of 5B is unchanged.
Phase 5C: the final boss (mechanics, script, Debug test, boss sim)
The boss is an Enemy template Final Demon (boss_only, no levels - the ordinary boss roll never picks him; placeholder glyph Ω until the art is integrated) placed by advance_level on the last floor of the 5 Blades dungeon only (spawn_final_boss); every other dungeon keeps its floor boss. He carries a FinalBoss { phase, turn, defense_turns, counter_turns } component (phase = daggers stabbed, 0..=5 - the six idle animations 100..50 are these six phases) and is tagged SeesThroughStealth like every Blade guard. His fight is data - resources/final_boss.ron - and interpreted by the new src/battle/boss.rs: a fixed rotation per phase (set program, not random): phase 0 basic attacks only; more moves join as the phases go up: War Cry (player’s attacks x0.6 for 3 of the player’s actions), Double Attack (x2 damage), Defend (he takes 50% for 3 of his turns), Counter (3 of his turns: 40% of the player’s strikes are countered for x2). trigger_enemy_action hands his turns to boss::take_turn; strike_enemy applies the weakening/defense and rolls the counter; the War Cry line becomes “Foolish <class>, your tricks won’t work on me!” if the player is Stealthed/Invisible and he is past phase 0. The Stab: a new BattleAction::Stab (“Stab (Blade)”) appears in the right column of the battle menu when he is at or below 30% health and has phases left; it makes him flee - the battle ends (like the player fleeing), he is healed to full and back one phase further on, next to the player, who gets a moment to drink a potion; after the 5th stab there are no more stabs and he must be killed. (Enemy Flee was designed as “runs from battle and heals to full after we use the blade on him” - this is exactly that.) Debug test: the Debug kit has a “Final Boss” item (fight at phase 0, sets DebugBossTest(Some(0))) and 6 Next Levels; while a test is running, Next Level stabs the next dagger in (a fresh fight one phase further on, up to 5) instead of jumping floors. New resource DebugBossTest (inserted in every place QuestLog is, incl. the permanent use_items test). Boss sim: cargo test --release final_boss_report -- --ignored --nocapture (balance_sim/boss_sim.rs): each class with the class kit, top weapon, the 5-Blade buff, +5 potions and 2 of each technique, fights the boss from each starting phase (start@0 is the whole fight; the bot stabs when offered, flees and drinks potions like the dungeon bot). SIM_SCENARIO, BOSS_POTIONS, BOSS_TECHNIQUES apply. Calibration: my first numbers (28 hp / 4 dmg) won 0%; with a realistic loadout the boss at 178 health / 14 damage in the 5 Blades dungeon (base 19 / 2 x7 x1.34 health) gives whole-fight win rates Barbarian 83%, Amazon 77%, Rogue 70%, Hunter 39%, Mage 0% (start@5, no stabs: 100/58/100/100/100) - the whole fight is knife-edged (hp 170 -> 63-98%, hp 185 -> ~10%) because five phases compound. Mage is far behind (single-target damage: its kit is AoE/defence) - a balance to-do. Permanent tests: Debug Final Boss + Next Level step through all six phases; the script/Stab rules; only the 5 Blades dungeon ends with the final boss. Also fixed while here: the sim’s flee handling for boss fights. Crawl bot win rate still ~75%. Not done: the boss’s art (48x48 sheet, bigger than other enemies; idle rows per phase), his attack/War Cry/Defend/Counter/Flee/Death animations wired to the battle screen, the Blade fight’s UI text for his moves, Mage’s damage.
The final boss on screen: battle sheet, larger portrait, dungeon walk
The user made the demon at 48x48 with more detail and said he may have his own sheet. Battle: new resources/final_boss.png (60x60 cells - his motion reaches 58px in the 64/68px exports, so the 48px crop I first tried clipped a few frames), the six idle-battle loops (one per phase) plus Attack, Death, Defend, Double Attack, Counter Attack, War Cry and Flee; a new fancy console FINAL_BOSS_CONSOLE (43, after the enemy consoles so the z-order of everything else is unchanged - every console from the shopkeeper trio on shifted by +1) draws him at 1.5x. Each of his rotation moves sets his attack animation (boss::take_turn), the Stab plays Flee while its result shows, his death uses the same console (DyingEnemyEffect::boss), and the idle loop follows his phase (EnemyPortrait::boss_phase). Dungeon: his Walk (4 facings) sits on rows 81-84 of the shared 128px-cell sheet (10880 px tall now). Permanent test final_boss_art_indices_fit_their_sheets checks every glyph exists and avoids the blank glyph-32 row. The game launches without a panic. Not verified on screen (the 1.5x scale around the cell’s centre, where he sits relative to the player and the message panel, and the uneven 2.46x nearest scaling of his dungeon walk): use the Debug class’s “Final Boss” item, then Next Level, and send a screenshot - expect a correction round on the scale/position, as with every new console.
After the first play of the boss: fixes and the final victory scene
From the user’s screenshots and notes: (1) the boss “followed the player to the town” - a bug in my Debug test: DebugBossTest stayed set forever, so a later Next Level respawned the boss next to the player wherever they were. The resource is gone: Next Level now stabs the next dagger only while a FinalBoss is actually on the floor (phase = his current phase + 1, a fresh fight), otherwise it is the ordinary jump - and on the last dungeon level (no exit) it wins the run, as the Blade does (ProvidesEffect::DebugNextLevel arms in systems/use_items.rs). (2) A boss counter did not spend the Defense Potion’s charges: boss::maybe_counter called strike_player directly; it now runs the real attack pipeline (resolve_enemy_attack_scaled), so Evasion, Defend, Ice Armor (and its charges) and War Cry all apply. (3) He could be killed by just attacking: a would-be killing blow while daggers are missing now makes him flee (healed, a phase on, battle over) - boss::flee_instead_of_dying, called at the top of record_enemy_kill; at the last phase he dies. (4) Debug runs now earn Blades (I had excluded the Debug class from awards, which is why using the Debug Victory item did not unlock the next tier; earn one, quit to the title, and the difficulty screen shows it; delete saves/progress.ron to reset). (5) The boss’s draw scale is 1.3 (was 1.5) so he no longer covers his name label; still to be checked on screen. (6) Final victory scene: VictoryBackground::FinalTown (shown when the 5 Blades dungeon is won: happy-town backdrop from slot 17 of battle_backgrounds.png, the hero in front, new text: the demon falls / the nether closes / the town is saved / Endless open) - the art is the user’s to generate (prompt in docs/phase5_plan.md); tools/place_victory_background.py places it and FINAL_TOWN_ART_READY switches it on, until then the old procedural fill is used. Four new permanent tests (forced flee, counter spends charges, Next Level jumps floors and wins on the last, Debug boss stepping). Not verified on screen: the victory text, the new scale.
The boss no longer stays next to the player after fleeing (and the player is not sent anywhere): State::relocate_final_boss moves him to a random reachable tile 25-70 steps from the player - “a different room” - when he flees (a Stab, or a would-be killing blow with daggers missing); he comes for the player again when he next sees them. Permanent test extended (he ends up >10 tiles away). The victory-scene prompt in docs/phase5_plan.md is now written for Leonardo (Phoenix/Lucid Origin, with a negative prompt and settings); the placement script center-crops to 16:10 so a 3:2 render is not squashed.
The user asked for OUR townsfolk sprites on the final victory screen, not AI-generated people: the backdrop prompt is now an empty village square, and State::draw_final_victory_townsfolk (screens/end.rs) draws the Witch, Elder, Blacksmith, Townswoman, Townsman, Young Child (their quest-complete loops), the Shopkeeper and the cat on SHOPKEEPER_IDLE_GLIDE_CONSOLE at 3.0-4.6x, a few frames out of step with each other, either side of the hero. Positions/scales are a first guess (unverified on screen); it draws over the procedural background too, so it can be previewed before the painting exists (Progress(blades: 5), pick 5 Blades, Debug, Next Level on level 3).
The user wants the final victory backdrop in pixel art like the other backgrounds: the Leonardo prompt is now a pixel-art one (32-bit JRPG, chunky pixels, limited palette, dithered shading, negative prompt against smooth/painterly/anti-aliased), and tools/place_victory_background.py takes an optional snap factor (... 17 4) that shrinks the picture to 320x200 and scales it back up with nearest-neighbour so a generated picture’s pixels land on a 4px grid.
Bug from the user’s screenshot: with a Defense Potion and a Speed Potion active only one buff badge showed in the dungeon. The Speed Potion’s haste (Hasted) never had a badge; systems/hud.rs now draws the Speed Potion’s own icon next to Ice Armor’s (which the Defense Potion uses) and the battle screen lists it with its remaining actions as a count. The permanent HUD access test now gives its player IceArmored and Hasted too, so the new #[read_component(Hasted)] is exercised.
Two rule changes from the user’s playtest of the final dungeon: (1) the 5 Blades dungeon has no Blade - advance_level places an invisible WinWhenBossDead marker on its last floor instead of the Blade, and end_turn wins the run the moment no Boss is left (the demon’s death); the other dungeons still end with the Blade. (2) A blow that would kill the demon with daggers missing no longer advances his phase: he flees healed and the battle ends, but no dagger went in - the chance to stab him that fight is lost and the player has to wear him down again (boss::flee_instead_of_dying; only the Stab, offered at 30% health, moves him a phase on). Permanent tests updated/added (phase unchanged after a forced flee; final dungeon has no Blade and the demon’s death wins).
Buffs stack (Ice Armor, Haste, Invisible/Stealth, and in battle also Battle Cry, Dodge, Counter): the user asked to see them all stacked downward. The dungeon HUD’s badges now stack DOWN under the portrait (they went sideways), the battle screen’s buff column already did and now tightens its spacing when there are more than 4 so all stay on screen; the demon’s War Cry weakening shows as a badge (the Stun icon) with its remaining actions. Not verified on screen.
Victory screen: the “You have won!” header and its two lines now sit in a bordered black PanelBox (Swamp frame; the text on PANEL_TEXT_CONSOLE), and “Press Enter to return to the title screen.” in its own small box, so they read over any backdrop. The big header needed a console above the UI panels: new BIG_TEXT_TOP_CONSOLE (54, registered last, so nothing renumbered; same grid/font as BIG_TEXT_CONSOLE). Positions come from the existing text rows (HUD rows 10 and 13, the prompt at 60) and the header’s 64-96px band - unverified on screen, expect a correction round.
The Defense Potion’s buff badge now shows the Defense Potion’s own icon (dungeon HUD and battle) instead of Ice Armor’s: IceArmored gained a badge name set by use_items from the item’s name (both apply the same effect); permanent test defense_potion_and_ice_armor_have_their_own_badges.
Phase 5F: the new hints
The Paused screen’s rotating hint box (screens/pause.rs::PAUSE_HINTS) had 11 general tips from before the town, quests and Blades. It now has about 70, grouped: the basics, the town (signposts, gatehouse, gold only from chests, the shop and its limited stock, the cat), quests (one at a time, progress carries over, the holder is always in town, one quest per run for Townsman/Townswoman/Elder, the Witch’s herb/horn/feather, the Shopkeeper’s crate, only the Shopkeeper and the Witch give Speed/Defense Potions), the Blacksmith (ingot upgrades, carrying one during another quest), chests and the dungeon (three floors, the Blade and its guardian, bosses see through Stealth), Blades and difficulty (earned once, saved, harder = tougher enemies and bigger chests, five Blades for the final dungeon - kept deliberately vague about the demon), battle (no potions in battle, Defend is a gamble, buffs and their badges, AoE, Stun/Feint, poison), class abilities and the Arena. Long hints wrap onto up to three lines and stay longer (3.5 s + 45 ms per character); the box got one row taller. Every statement was checked against how the game works today. Not verified on screen.