Patch notes page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-21 15:36:16 +01:00
parent eb51bc80b6
commit b27f8682c5
4 changed files with 59 additions and 6 deletions

View file

@ -6,3 +6,12 @@
needed and none is held) — recipe inputs, building costs, order slots, gold, needed and none is held) — recipe inputs, building costs, order slots, gold,
tooltips, server messages. Never `need / have`. tooltips, server messages. Never `need / have`.
- In-game text is terse: icons and numbers over prose, details in tooltips. - 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.

18
game.py
View file

@ -4457,6 +4457,23 @@ _assign_glyphs()
ORDER_RECIPES, ORDER_RAW = _order_sources() 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: def content() -> dict:
"""The static tables the client renders from.""" """The static tables the client renders from."""
buildings = { 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, "resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "stations": list(STATIONS), "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"regen_per_hour": REGEN_PER_HOUR, "regen_per_hour": REGEN_PER_HOUR,
"fires": FIRES, "flames": FLAMES, "firewood": FIREWOOD, "tend_xp": TEND_XP, "fire_low": FIRE_LOW, "fires": FIRES, "flames": FLAMES, "firewood": FIREWOOD, "tend_xp": TEND_XP, "fire_low": FIRE_LOW,
"patch_notes": PATCH_NOTES,
} }

View file

@ -276,6 +276,7 @@ function render() {
for (const el of document.querySelectorAll("[data-keep]")) kept[el.dataset.keep] = el.value; for (const el of document.querySelectorAll("[data-keep]")) kept[el.dataset.keep] = el.value;
const focus = document.activeElement?.dataset?.keep; const focus = document.activeElement?.dataset?.keep;
if (page === "notes") remember("notes", notesKey());
setHtml($("#side"), S.me ? sidebar() : `<div class="brand"><img src="/static/icon.svg" alt="">Guildhall <small>beta</small></div>${S.dev ? devbar() : ""}`); setHtml($("#side"), S.me ? sidebar() : `<div class="brand"><img src="/static/icon.svg" alt="">Guildhall <small>beta</small></div>${S.dev ? devbar() : ""}`);
setHtml($("#top"), topbar()); setHtml($("#top"), topbar());
// The fires keep burning through a re-render: put the old canvases back in place of the new ones. // 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 ? `<span class="badge">${takeable}</span>` : "", takeable ? `${takeable} for you` : "")} ${nav("orders", ORDER_ICON(), "Work orders", "", takeable ? `<span class="badge">${takeable}</span>` : "", takeable ? `${takeable} for you` : "")}
${nav("board", "💬", "Town board")} ${nav("board", "💬", "Town board")}
${nav("roster", "👥", "Guild roster", `${S.players.length}`, "", `${S.players.length} ${S.players.length === 1 ? "adventurer" : "adventurers"}`)} ${nav("roster", "👥", "Guild roster", `${S.players.length}`, "", `${S.players.length} ${S.players.length === 1 ? "adventurer" : "adventurers"}`)}
${nav("notes", "📰", "Patch notes", "", notesUnread() ? `<span class="badge">new</span>` : "", notesUnread() ? `What's new: ${C.patch_notes[0].title}` : "")}
<div class="navhead">You</div> <div class="navhead">You</div>
${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`, "", `Combat level ${me.levels.combat}`)} ${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`)}`; ${nav("bank", "🏦", "Bank", `${me.slots_used}/${me.slots}`, "", `${me.slots_used} of ${me.slots} bank slots used`)}`;
@ -379,7 +381,7 @@ function devbar() {
</div></div>`; </div></div>`;
} }
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() { function topbar() {
let title = TITLES[page] || "Guildhall"; let title = TITLES[page] || "Guildhall";
@ -439,7 +441,7 @@ function pageHtml() {
} }
if (page.startsWith("character:")) return characterSheet(page.slice(10)); if (page.startsWith("character:")) return characterSheet(page.slice(10));
if (page === "character") return characterSheet(S.uid); 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. // Recipe input as "have / need"; without `have`, just the quantity.
@ -1663,6 +1665,19 @@ function roster() {
}).join("")}</div>`; }).join("")}</div>`;
} }
// 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) => `<div class="panel"><h2>${esc(release.title)} <span class="muted small">${esc(release.date)}</span></h2>
<ul class="ledger">${release.notes.map(([kind, text]) => {
const [icon, tone, label] = NOTE_KINDS[kind];
return `<li><span class="tag ${tone}" title="${label}">${icon}</span> ${esc(text)}</li>`;
}).join("")}</ul></div>`).join("");
}
// --- character sheet ----------------------------------------------------- // --- character sheet -----------------------------------------------------
const SLOT_NAMES = { weapon: "Weapon", shield: "Shield", helmet: "Helmet", body: "Body", legs: "Legs" }; const SLOT_NAMES = { weapon: "Weapon", shield: "Shield", helmet: "Helmet", body: "Body", legs: "Legs" };

View file

@ -1571,10 +1571,6 @@ class IconTest(unittest.TestCase):
self.assertIn("glyph", spec, item) self.assertIn("glyph", spec, item)
if __name__ == "__main__":
unittest.main()
class CombatReworkMigrationTest(unittest.TestCase): class CombatReworkMigrationTest(unittest.TestCase):
def test_old_monsters_trophies_and_dragonhide_carry_over(self): def test_old_monsters_trophies_and_dragonhide_carry_over(self):
world = world_with(("kay", "ranger")) 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(set(self.world["city"]["fires"]), set(game.FIRES))
self.assertEqual(self.kai["xp"]["firekeeping"], 0) self.assertEqual(self.kai["xp"]["firekeeping"], 0)
self.assertIn("tool_firekeeping", self.kai["equip"]) 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()