diff --git a/CLAUDE.md b/CLAUDE.md index b7dedf7..33dd38b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,3 +6,12 @@ needed and none is held) — recipe inputs, building costs, order slots, gold, tooltips, server messages. Never `need / have`. - In-game text is terse: icons and numbers over prose, details in tooltips. + +## Shipping + +- Confirm with Fred before shipping anything discussed (patch notes, commit, + push, flake bump). Show the patch note lines with the summary of the change. +- Every shipped change adds a line to `PATCH_NOTES` in `game.py` (`new` / + `balance` / `fix`), in the same commit. Players read these: say what changed + for them, with numbers, not how the code changed. Same-day changes go under + the top entry; a new day or release starts a new entry. diff --git a/game.py b/game.py index b1cedfa..61b47d3 100644 --- a/game.py +++ b/game.py @@ -4457,6 +4457,23 @@ _assign_glyphs() ORDER_RECIPES, ORDER_RAW = _order_sources() +# What changed, shown on the Patch notes page. Newest first; every shipped change adds a +# line here (new / balance / fix), and a new release a new entry on top. +PATCH_NOTES = [ + {"date": "2026-10-21", "title": "Beta", "notes": [ + ("new", "Beta: completely meaningless milestone reached; every class now makes a decent amount of valuable contributions to the guild."), + ("new", "Patch notes menu."), + ("new", "Firekeeping: tend the fire for your guild members' crafting stations. Builders get a bonus. Use tiers of wood to light " + "and maintain a fire, which saves resources on guild members' crafts of the same tier. For every craft completed while " + "the fire is lit, the Firekeeper gains 10% of the XP the craft gave the crafter, shared between all players keeping a " + "fire of that tier. Each player keeps their own \"pile\", which contributes an equal amount to the fire."), + ("new", "Work orders: anyone can provide reagents towards an incomplete work order and gets paid based on the value of the items provided."), + ("balance", "Gear: combat requirement adjusted to match the tier of crafting required to make it."), + ("balance", "Combat XP reduced to keep it in line with professions. Eating now delays your next swing; eat higher tier food to reduce the impact."), + ("balance", "Construction workbench now also affects labour on building sites."), + ]}, +] + def content() -> dict: """The static tables the client renders from.""" buildings = { @@ -4475,4 +4492,5 @@ def content() -> dict: "resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "stations": list(STATIONS), "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE, "regen_per_hour": REGEN_PER_HOUR, "fires": FIRES, "flames": FLAMES, "firewood": FIREWOOD, "tend_xp": TEND_XP, "fire_low": FIRE_LOW, + "patch_notes": PATCH_NOTES, } diff --git a/static/app.js b/static/app.js index 7496c03..96f5e3c 100644 --- a/static/app.js +++ b/static/app.js @@ -276,6 +276,7 @@ function render() { for (const el of document.querySelectorAll("[data-keep]")) kept[el.dataset.keep] = el.value; const focus = document.activeElement?.dataset?.keep; + if (page === "notes") remember("notes", notesKey()); setHtml($("#side"), S.me ? sidebar() : `
Guildhall beta
${S.dev ? devbar() : ""}`); setHtml($("#top"), topbar()); // The fires keep burning through a re-render: put the old canvases back in place of the new ones. @@ -349,6 +350,7 @@ function sidebar() { ${nav("orders", ORDER_ICON(), "Work orders", "", takeable ? `${takeable}` : "", takeable ? `${takeable} for you` : "")} ${nav("board", "💬", "Town board")} ${nav("roster", "👥", "Guild roster", `${S.players.length}`, "", `${S.players.length} ${S.players.length === 1 ? "adventurer" : "adventurers"}`)} + ${nav("notes", "📰", "Patch notes", "", notesUnread() ? `new` : "", notesUnread() ? `What's new: ${C.patch_notes[0].title}` : "")} ${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`, "", `Combat level ${me.levels.combat}`)} ${nav("bank", "🏦", "Bank", `${me.slots_used}/${me.slots}`, "", `${me.slots_used} of ${me.slots} bank slots used`)}`; @@ -379,7 +381,7 @@ function devbar() { `; } -const TITLES = { town: "Town square", council: "Council & threats", orders: "Work orders", board: "Town board", roster: "Guild roster", bank: "Bank", character: "Character" }; +const TITLES = { town: "Town square", council: "Council & threats", orders: "Work orders", board: "Town board", roster: "Guild roster", notes: "Patch notes", bank: "Bank", character: "Character" }; function topbar() { let title = TITLES[page] || "Guildhall"; @@ -439,7 +441,7 @@ function pageHtml() { } if (page.startsWith("character:")) return characterSheet(page.slice(10)); if (page === "character") return characterSheet(S.uid); - return ({ town, council, orders, board, roster, bank }[page] || town)(); + return ({ town, council, orders, board, roster, notes, bank }[page] || town)(); } // Recipe input as "have / need"; without `have`, just the quantity. @@ -1663,6 +1665,19 @@ function roster() { }).join("")}`; } +// Patch notes, newest first. The badge stays until this browser has opened the page since the last release. +const NOTE_KINDS = { new: ["✨", "good", "New"], balance: ["⚖️", "gold", "Balance"], fix: ["🔧", "warn", "Fixed"] }; +const notesKey = () => C.patch_notes[0] ? `${C.patch_notes[0].date} ${C.patch_notes[0].title}` : ""; +const notesUnread = () => !!C.patch_notes[0] && remember("notes") !== notesKey(); + +function notes() { + return C.patch_notes.map((release) => `

${esc(release.title)} ${esc(release.date)}

+
`).join(""); +} + // --- character sheet ----------------------------------------------------- const SLOT_NAMES = { weapon: "Weapon", shield: "Shield", helmet: "Helmet", body: "Body", legs: "Legs" }; diff --git a/tests/test_game.py b/tests/test_game.py index 4df453a..427b589 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -1571,10 +1571,6 @@ class IconTest(unittest.TestCase): self.assertIn("glyph", spec, item) -if __name__ == "__main__": - unittest.main() - - class CombatReworkMigrationTest(unittest.TestCase): def test_old_monsters_trophies_and_dragonhide_carry_over(self): world = world_with(("kay", "ranger")) @@ -2041,3 +2037,18 @@ class FirekeepingTest(unittest.TestCase): self.assertEqual(set(self.world["city"]["fires"]), set(game.FIRES)) self.assertEqual(self.kai["xp"]["firekeeping"], 0) self.assertIn("tool_firekeeping", self.kai["equip"]) + + +class PatchNotesTest(unittest.TestCase): + def test_newest_first_and_known_kinds(self): + dates = [release["date"] for release in game.PATCH_NOTES] + self.assertEqual(dates, sorted(dates, reverse=True)) + for release in game.PATCH_NOTES: + self.assertTrue(release["notes"]) + for kind, text in release["notes"]: + self.assertIn(kind, ("new", "balance", "fix")) + self.assertTrue(text) + + +if __name__ == "__main__": + unittest.main()