Patch notes page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
eb51bc80b6
commit
b27f8682c5
4 changed files with 59 additions and 6 deletions
|
|
@ -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
18
game.py
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" };
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue