diff --git a/app.py b/app.py index 73d61e4..aa5f003 100644 --- a/app.py +++ b/app.py @@ -409,6 +409,8 @@ class Handler(BaseHTTPRequestHandler): return game.plant_second_plot(world, player, body.get("id") or None) if path == "/api/perk": return game.buy_perk(player, str(body.get("perk"))) + if path == "/api/pin": + return game.toggle_pin(player, str(body.get("skill"))) if path == "/api/settings": if body.get("overflow") not in ("city", "stop"): return "Bad setting" diff --git a/game.py b/game.py index 9f6ff7e..11e8c4e 100644 --- a/game.py +++ b/game.py @@ -1033,7 +1033,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 18 +WORLD_VERSION = 19 def new_world(now: float, speed: float = 1.0) -> dict: @@ -1183,6 +1183,10 @@ def migrate(world: dict, now: float | None = None) -> dict: for slot in ("ring", "amulet", "trinket"): player["equip"].setdefault(slot, None) world["version"] = 18 + if world["version"] < 19: + for player in world["players"].values(): + player.setdefault("pinned", []) + world["version"] = 19 return world @@ -1425,6 +1429,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict: "perks": {"bank_slot": 0}, "garrison": False, "donated": 0, + "pinned": [], # skills pinned to the top of the sidebar } @@ -3841,6 +3846,17 @@ def buy_perk(player: dict, perk: str) -> str | None: return None +def toggle_pin(player: dict, skill: str) -> str | None: + if skill not in SKILL_IDS: + return "No such skill" + pinned = player["pinned"] + if skill in pinned: + pinned.remove(skill) + else: + pinned.append(skill) + return None + + # --- icons ------------------------------------------------------------------------ # Every item, skill, class, building, prayer and output-less action gets a # game-icons.net glyph (CC BY 3.0) and a tint. Families share a shape and are diff --git a/static/app.js b/static/app.js index d3146f0..e034b9a 100644 --- a/static/app.js +++ b/static/app.js @@ -308,6 +308,16 @@ function nav(id, icon, label, meta = "", extra = "", tip = "") { return ``; } +function skillNav(skill, me, doing) { + const busy = doing?.skill === skill.id ? `` : ""; + return nav("skill:" + skill.id, skill.icon, skill.name, `${me.levels[skill.id]}`, busy); +} + +function pinnedNav(me, doing) { + const pinned = C.skills.filter((s) => me.pinned.includes(s.id) && (!s.class || s.class === me.cls)); + return pinned.length ? `${pinned.map((s) => skillNav(s, me, doing)).join("")}` : ""; +} + function sidebar() { const me = S.me, cls = C.classes[me.cls]; const doing = me.action && C.actions.find((a) => a.id === me.action.id); @@ -320,6 +330,7 @@ function sidebar() {
${cls.icon} ${cls.name} ยท ${HEART} ${fmt(me.hp)}/${me.max_hp}
${COIN} ${fmt(me.gold)}${FAVOUR} ${fmt(me.favour)}
+ ${pinnedNav(me, doing)} ${nav("town", "๐Ÿ˜๏ธ", "Town square", `${S.city.population}`, "", `Population ${S.city.population}`)} ${nav("council", "๐Ÿ“œ", "Council & threats", "", raid ? `` : "", raid ? "A threat is coming" : "")} @@ -336,8 +347,7 @@ function sidebar() { group = skill.group; html += ``; } - const busy = doing?.skill === skill.id ? `` : ""; - html += nav("skill:" + skill.id, skill.icon, skill.name, `${me.levels[skill.id]}`, busy); + html += skillNav(skill, me, doing); } return html + (S.dev ? devbar() : "") + `

Icons by Lorc, Delapouite and others at game-icons.net, CC BY 3.0

`; @@ -423,7 +433,7 @@ function skillPage(id) {
${skill.icon}
-

${skill.name}

+

${skill.name}

Level ${level} / 99 ยท ${fmt(xp)}${ceil ? ` / ${fmt(ceil)}` : ""} XP
@@ -1677,6 +1687,7 @@ document.addEventListener("click", (e) => { if ("plot" in d) return act("/api/plot", { id: d.plot || null }); if (d.garrison) return act("/api/garrison", { join: d.garrison === "1" }); if (d.perk) return act("/api/perk", { perk: d.perk }); + if (d.pin) return act("/api/pin", { skill: d.pin }); if (d.favour) return act("/api/favour", { kind: d.favour }); if (d.food) return act("/api/food", { item: d.food === "cheapest" ? null : d.food }); if (d.potion) return act("/api/potion", { item: d.potion }); diff --git a/static/styles.css b/static/styles.css index 2a31494..5c294b1 100644 --- a/static/styles.css +++ b/static/styles.css @@ -183,6 +183,10 @@ textarea { width: 100%; resize: vertical; min-height: 60px; } .skill-head .info { flex: 1; min-width: 0; } .skill-head h2 { margin: 0; font-size: 22px; text-transform: none; letter-spacing: 0; color: var(--text); } .skill-head .lv { font-variant-numeric: tabular-nums; color: var(--muted); margin: 2px 0 8px; } +.skill-head .pin { background: none; border: 0; padding: 0 4px; font-size: 16px; cursor: pointer; vertical-align: middle; + filter: grayscale(1); opacity: .35; } +.skill-head .pin:hover { opacity: .7; } +.skill-head .pin.on { filter: none; opacity: 1; } .action { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); diff --git a/tests/test_game.py b/tests/test_game.py index 459f8c3..a661f97 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -1496,6 +1496,24 @@ class FittingTest(unittest.TestCase): self.assertEqual(world["city"]["buildings"]["houses"]["fittings"], []) +class PinTest(unittest.TestCase): + def test_pin_toggles(self): + world = world_with(("fred", "commoner")) + p = world["players"]["fred"] + self.assertIsNone(game.toggle_pin(p, "mining")) + self.assertEqual(p["pinned"], ["mining"]) + self.assertIsNone(game.toggle_pin(p, "mining")) + self.assertEqual(p["pinned"], []) + self.assertIsNotNone(game.toggle_pin(p, "juggling")) + + def test_old_players_get_pins(self): + world = world_with(("fred", "commoner")) + del world["players"]["fred"]["pinned"] + world["version"] = 18 + game.migrate(world, T0) + self.assertEqual(world["players"]["fred"]["pinned"], []) + + class IconTest(unittest.TestCase): def test_everything_has_a_glyph_in_the_sprite(self): sprite = (Path(__file__).resolve().parent.parent / "static" / "icons.svg").read_text()