Achievements page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-25 20:01:33 +00:00
parent f6951b0f12
commit a05530b802
5 changed files with 152 additions and 4 deletions

73
game.py
View file

@ -1524,7 +1524,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 30
WORLD_VERSION = 31
def new_world(now: float, speed: float = 1.0) -> dict:
@ -1764,6 +1764,12 @@ def migrate(world: dict, now: float | None = None) -> dict:
for player in world["players"].values():
player.setdefault("patterns", [])
world["version"] = 30
if world["version"] < 31:
# Uniques ever found, for achievements: whatever's held now.
for player in world["players"].values():
held = [*player["bank"], *player["equip"].values()]
player.setdefault("found", [item for item in UNIQUES if item in held])
world["version"] = 31
return world
@ -2020,7 +2026,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict:
"hp": 44, "hp_at": now,
"action": None, "stopped": None,
"last_active": now, "overflow": "city",
"perks": {"bank_slot": 0}, "manual": [], "patterns": [], "eat_below": EAT_BELOW,
"perks": {"bank_slot": 0}, "manual": [], "patterns": [], "found": [], "eat_below": EAT_BELOW,
"rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK),
"garrison": False,
"donated": 0,
@ -2082,6 +2088,8 @@ def city_uses(item: str) -> list[str]:
def bank_add(world: dict, player: dict, item: str, qty: int, now: float, summary: dict) -> int:
"""Store what fits; the rest goes to the city if it has a use, else is sold. Returns qty kept."""
if ITEMS[item].get("unique") and item not in player["found"]:
player["found"].append(item) # for achievements: once found, always found
keep = min(qty, bank_room(world, player, item))
if keep:
player["bank"][item] = player["bank"].get(item, 0) + keep
@ -5626,6 +5634,65 @@ DRILL_SERGEANT_POINTS = 30 # boosted drilling per hire: doubled points and
DRILL_SERGEANT_FAVOUR = 150 # +50% for every further hire against the same threat
# --- achievements ----------------------------------------------------------------
# What's left to unlock. Read off what you already have, never a source of anything:
# patterns learned, uniques found, titles, looks, your home.
def _got(name: str, done, glyph: str, tint: str, tip: str = "", colour: str | None = None) -> dict:
return {"name": name, "done": bool(done), "glyph": glyph, "tint": tint, "tip": tip, **({"colour": colour} if colour else {})}
def achievements(player: dict) -> list[dict]:
"""Sections of cards, each card a set of things to collect."""
skill_name = {s["id"]: s["name"] for s in SKILLS}
dungeons = sorted(DUNGEON_LOOT, key=lambda key: DUNGEONS[key]["level"])
patterns = []
for key in dungeons:
cards = []
for pattern in ACTION_BY_ID[key]["loot"]["patterns"]:
spec, made = ITEMS[pattern], ITEMS[ITEMS[pattern]["pattern"]]
held = pattern not in player["patterns"] and player["bank"].get(pattern)
tip = f"{skill_name[spec['skill']]} {spec['level']}" + (" · 📜 in bank" if held else "")
cards.append({**_got(made["name"], pattern in player["patterns"], made["glyph"], made["tint"], tip), "held": bool(held)})
patterns.append({"name": DUNGEONS[key]["name"], "entries": cards})
found = player.get("found", [])
uniques = [_got(ITEMS[unique]["name"], unique in found, ITEMS[unique]["glyph"], ITEMS[unique]["tint"],
DUNGEONS[key]["name"]) for key in dungeons for unique in [DUNGEON_LOOT[key][2]]]
def level_title(need: int, rank: str) -> list[dict]:
return [_got(f"{rank} {noun}", skill_level(player, skill) >= need, *SKILL_GLYPHS[skill],
f"{skill_name[skill]} {skill_level(player, skill)} / {need}") for skill, noun in SKILL_TITLES.items()]
deeds = {"defended": ("checked-shield", "#d4dae2", "Raids won on guard"), "dragons": ("dragon-head", "#e05a5a", "Dragons slain on guard"),
"bosses": ("crossed-swords", "#e0b04a", "Bosses beaten")}
feats = [_got(title, skill_level(player, skill) >= need, *SKILL_GLYPHS[skill], f"{skill_name[skill]} {skill_level(player, skill)} / {need}")
for skill, need, title in SKILL_EXTRA_TITLES]
feats += [_got(title, player["deeds"].get(key, 0) >= need, *deeds[key][:2], f"{deeds[key][2]} {player['deeds'].get(key, 0)} / {need}")
for key, need, title in DEED_TITLES]
feats.append(_got("Patron of the City", player["donated"] >= 100_000, "sparkles", "#b98ce8",
f"Given {player['donated']:,} / 100,000"))
def bought(kind: str) -> list[dict]:
return [_got(spec["value"] if kind == "title" else spec["name"], key in player["owned"],
"scroll-unfurled" if kind == "title" else spec.get("value", ""), "#e0b04a",
f"Caravan · {spec['price']:,} gold", spec["value"] if kind == "colour" else None)
for key, spec in COSMETICS.items() if spec["kind"] == kind and spec.get("price")]
bosses = [_got(spec["name"], key in player["owned"], ACTION_BY_ID[spec["boss"]]["glyph"], ACTION_BY_ID[spec["boss"]]["tint"],
ACTION_BY_ID[spec["boss"]]["name"]) for key, spec in COSMETICS.items() if spec.get("boss")]
homes = [_got(name, player.get("home", 0) >= level, "castle", "#d4dae2", f"{cost:,} gold")
for level, (name, cost) in enumerate(HOMES) if level]
return [
{"name": "Masterwork patterns", "cards": patterns},
{"name": "Dungeon loot", "cards": [{"name": "Uniques", "entries": uniques}]},
{"name": "Titles", "cards": [{"name": "Masters", "entries": level_title(50, "Master")},
{"name": "Grandmasters", "entries": level_title(99, "Grandmaster")},
{"name": "Deeds", "entries": feats}, {"name": "Caravan titles", "entries": bought("title")}]},
{"name": "Looks", "cards": [{"name": "Boss emblems", "entries": bosses}, {"name": "Caravan emblems", "entries": bought("emblem")},
{"name": "Dyes", "entries": bought("colour")}, {"name": "Home", "entries": homes}]},
]
def drill_per_action(player: dict) -> float:
return DRILL_PER_ACTION * (1 + 0.25 * (skill_level(player, "combat") // 20)) * ware(player, "drill", "combat")
@ -6014,6 +6081,8 @@ PATCH_NOTES = [
"lens, Wyrmscale quiver. Abilities it changes get a ✦."),
("new", "Choose when you eat between fights: below 25 / 50 / 75% of your HP, always, or never (the food slot, "
"under \"Eat below\"). Default 50%: above it you just rest, so a scratch doesn't cost food."),
("new", "🏆 Achievements (under You): 127 to collect. Masterwork patterns per dungeon (7 each), the 7 uniques, titles, "
"boss emblems, caravan looks, home. Tap an icon for what it is and how to get it. Nothing is awarded there, it only keeps track."),
("new", "Tapping an enemy or ally switches at once, and your next press goes to them: tap, then interrupt, "
"without waiting."),
("new", "HP bars show what was just lost, with each fighter's last hit or heal by their HP."),