Combat engine: swing-by-swing fights, Frontier and expeditions, trophies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-19 23:32:07 +01:00
parent 2ca90c43c0
commit 19ccab4ac3
7 changed files with 395 additions and 115 deletions

334
game.py
View file

@ -397,7 +397,118 @@ def _tiered_actions() -> list[dict]:
return actions
# --- combat -----------------------------------------------------------------
# A fight is played out swing by swing: you and the enemy each have HP, a swing
# timer, accuracy and a max hit, and every swing hits, misses or crits. Combat
# is split into the Frontier (bandits and the city's troubles) and expeditions:
# one area per tier, each with a golem, a troll, a spirit and a flyer that drop
# the trophies higher weapons need, and a boss.
CRIT_CHANCE = 0.05
CRIT_MULT = 1.5
RESPAWN = 3 # seconds before the next enemy turns up
PLAYER_INTERVAL = 2.4 # seconds between your swings
MAX_FIGHT = 600 # a fight that drags on longer is broken off
FLEE_BELOW = 0.2 # out of food and under this share of HP: get out
# kind: HP, seconds between swings, accuracy, evasion, max hit (multipliers), damage it shrugs off
MONSTER_KINDS = {
"human": (1.0, 2.6, 1.0, 1.0, 1.0, 0.0),
"beast": (0.9, 2.2, 1.0, 1.0, 0.9, 0.0),
"golem": (1.5, 3.2, 1.0, 0.7, 1.25, 0.2),
"troll": (1.4, 3.0, 0.9, 1.0, 1.4, 0.0),
"spirit": (0.8, 2.4, 1.0, 1.35, 0.9, 0.0),
"flyer": (0.8, 2.0, 1.0, 1.2, 0.8, 0.0),
"dragon": (2.5, 3.0, 1.1, 1.0, 1.5, 0.15),
}
def monster(name: str, kind: str, level: int, boss: bool = False) -> dict:
hp, interval, accuracy, evasion, hit, shrug = MONSTER_KINDS[kind]
return {"name": name, "kind": kind, "level": level, "boss": boss,
"hp": round((20 + 6 * level) * hp * (2.5 if boss else 1)), "interval": interval,
"accuracy": round((12 + 2.2 * level) * accuracy), "evasion": round((10 + 2 * level) * evasion),
"max_hit": max(1, round((2 + level / 3) * hit * (1.2 if boss else 1))), "reduction": shrug}
def _fight(id: str, enemy: dict, group: str, gold: int = 0, drops=(), name: str | None = None, **extra) -> dict:
"""A combat action: XP per kill, needs the enemy's level in combat."""
xp = round(enemy["hp"] * 0.4 + enemy["level"] * 1.5)
return _action(id, "combat", name or enemy["name"], enemy["level"], RESPAWN, xp, gold=gold,
drops=[list(drop) for drop in drops], enemy=enemy, group=group, **extra)
# Expedition areas, tiers II to VI: name, level, and the trophy each kind of monster drops there.
AREAS = ["Old Watchtower", "Sunken Barrows", "Iron Hills", "Frostpeak", "Dragon's Spine"]
AREA_LEVELS = [10, 20, 30, 40, 50]
AREA_MONSTERS = { # kind: (level offset, monsters, trophies)
"golem": (0, ["Clay golem", "Stone golem", "Iron golem", "Obsidian golem", "Runic golem"],
["Clay core", "Stone core", "Iron core", "Obsidian core", "Runic core"]),
"troll": (2, ["Cave troll", "Hill troll", "Rock troll", "Frost troll", "Elder troll"],
["Cave troll hide", "Hill troll hide", "Rock troll hide", "Frost troll hide", "Elder troll hide"]),
"spirit": (1, ["Will-o'-wisp", "Bog wraith", "Mine ghost", "Banshee", "Shade"],
["Wisp essence", "Wraith essence", "Ghost essence", "Banshee essence", "Shade essence"]),
"flyer": (0, ["Giant bat", "Harpy", "Wyvern", "Griffin", "Roc"],
["Bat sinew", "Harpy sinew", "Wyvern sinew", "Griffin sinew", "Roc sinew"]),
}
TROPHY_ODDS = 0.33
AREA_BONES = ["bones", "big_bones", "big_bones", "dragon_bones", "dragon_bones"]
def _slug(name: str) -> str:
return "".join(c if c.isalnum() else "_" for c in name.lower().replace("'", "")).strip("_")
TROPHIES = {} # id: (name, kind, tier)
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
for _tier, _trophy in enumerate(_trophies, start=1):
TROPHIES[_slug(_trophy)] = (_trophy, _kind, _tier)
ITEMS[_slug(_trophy)] = {"name": _trophy, "icon": "🏆", "value": 15 * _tier, "trophy": _kind, "tier": _tier}
def _area_fights() -> list[dict]:
fights = []
for index, (area, level) in enumerate(zip(AREAS, AREA_LEVELS)):
for kind, (offset, monsters, trophies) in AREA_MONSTERS.items():
enemy = monster(monsters[index], kind, level + offset)
fights.append(_fight(f"fight_{_slug(monsters[index])}", enemy, area, gold=level + offset,
drops=[(AREA_BONES[index], 1, 1.0), (_slug(trophies[index]), 1, TROPHY_ODDS)]))
return fights
def _boss_trophies(tier: int) -> list[tuple]:
return [(_slug(trophies[tier - 1]), 1, 0.25) for _offset, _monsters, trophies in AREA_MONSTERS.values()]
COMBAT_ACTIONS = [
# The Frontier: the city's own troubles.
_fight("fight_scouts", monster("Bandit scouts", "human", 10), "Frontier", gold=10,
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
_fight("sabotage", monster("Camp guards", "human", 12), "Frontier", name="Sabotage the bandit camp",
special="sabotage"),
_fight("fight_stronghold", monster("Bandit stronghold", "human", 40, boss=True), "Frontier", gold=60,
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
# Greenwood: where everyone starts.
_fight("fight_wolves", monster("Wolves", "beast", 1), "Greenwood", gold=2, name="Wolves in the woods",
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
_fight("fight_goblins", monster("Cave goblins", "human", 5), "Greenwood", gold=4,
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
*_area_fights(),
_fight("fight_ruins", monster("Watchtower ogre", "troll", 18, boss=True), "Old Watchtower", gold=25,
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_trophies(1)]),
_fight("fight_barrow_king", monster("Barrow king", "spirit", 28, boss=True), "Sunken Barrows", gold=40,
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_trophies(2)]),
_fight("fight_green_dragon", monster("Green dragon", "dragon", 38), "Iron Hills", gold=80,
drops=[("green_dragonhide", 1, 0.9), ("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(3)]),
_fight("fight_blue_dragon", monster("Blue dragon", "dragon", 48), "Frostpeak", gold=110,
drops=[("blue_dragonhide", 1, 0.9), ("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(4)]),
_fight("fight_red_dragon", monster("Red dragon", "dragon", 60), "Dragon's Spine", gold=150,
drops=[("red_dragonhide", 1, 0.9), ("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_trophies(5)]),
_fight("fight_black_dragon", monster("Black dragon", "dragon", 75, boss=True), "Dragon's Spine", gold=220,
drops=[("red_dragonhide", 2, 0.9), ("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_trophies(5)]),
]
ACTIONS = [
*COMBAT_ACTIONS,
_action("mine_stone", "mining", "Quarry stone", 1, 20, 5, outputs={"stone": 1}),
_action("farm_wheat", "farming", "Grow wheat", 1, 25, 6, outputs={"wheat": 2}, group="Crops"),
_action("farm_veg", "farming", "Grow vegetables", 12, 35, 13, outputs={"vegetables": 2}, group="Crops"),
@ -435,25 +546,7 @@ ACTIONS = [
_action("pack_bread", "cooking", "Pack bread rations", 1, 20, 6, {"bread": 2}, {"provisions": 1}, group="Rations"),
_action("pack_meat", "cooking", "Pack meat rations", 5, 20, 7, {"cooked_meat": 2}, {"provisions": 1}, group="Rations"),
_action("pack_fish", "cooking", "Pack fish rations", 1, 20, 6, {"cooked_shrimp": 2}, {"provisions": 1}, group="Rations"),
_action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill"),
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5], ["bones", 1, 1.0]]),
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
drops=[["copper_bar", 1, 0.3], ["arrows", 5, 0.4], ["bones", 1, 1.0]]),
_action("fight_ruins", "combat", "Watchtower ruins", 25, 90, 60, difficulty=30, damage=14, gold=25,
drops=[["iron_bar", 1, 0.4], ["iron_sword", 1, 0.03], ["big_bones", 1, 0.8]]),
_action("fight_stronghold", "combat", "Bandit stronghold", 40, 105, 95, difficulty=50, damage=25, gold=60,
drops=[["steel_bar", 1, 0.4], ["coal", 3, 0.5], ["iron_shield", 1, 0.04], ["big_bones", 1, 1.0]]),
_action("fight_green_dragon", "combat", "Green dragon", 50, 120, 135, difficulty=90, damage=30, gold=80,
drops=[["green_dragonhide", 1, 0.9], ["mithril_ore", 2, 0.3], ["dragon_bones", 1, 1.0]]),
_action("fight_blue_dragon", "combat", "Blue dragon", 62, 130, 175, difficulty=115, damage=40, gold=110,
drops=[["blue_dragonhide", 1, 0.9], ["adamantite_ore", 2, 0.3], ["dragon_bones", 1, 1.0]]),
_action("fight_red_dragon", "combat", "Red dragon", 75, 140, 220, difficulty=140, damage=52, gold=150,
drops=[["red_dragonhide", 1, 0.9], ["runite_ore", 1, 0.25], ["dragon_bones", 1, 1.0]]),
_action("fight_black_dragon", "combat", "Black dragon", 85, 150, 280, difficulty=165, damage=65, gold=220,
drops=[["red_dragonhide", 2, 0.9], ["rune_bar", 1, 0.1], ["dragon_bones", 2, 1.0]]),
_action("sabotage", "combat", "Sabotage the bandit camp", 10, 120, 35, difficulty=12, damage=10,
special="sabotage"),
_action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill", group="Frontier"),
_action("bury_bones", "prayer", "Bury bones", 1, 10, 4, {"bones": 1}, special="bury", pp=2, group="Bury"),
_action("bury_big_bones", "prayer", "Bury big bones", 15, 10, 15, {"big_bones": 1}, special="bury", pp=6, group="Bury"),
_action("bury_dragon_bones", "prayer", "Bury dragon bones", 40, 12, 72, {"dragon_bones": 1}, special="bury", pp=20, group="Bury"),
@ -980,12 +1073,87 @@ def regen(world: dict, player: dict, now: float, boost: float = 1) -> None:
player["hp_at"] = now
def fight_damage(player: dict, action: dict, blessing: dict | None = None) -> float:
"""Average damage one fight deals: less when out-levelling the enemy, armoured or praying."""
blessing = blessing or NO_BLESSING
gap = combat_power(player) + blessing["power"] - action["difficulty"]
scale = min(1.5, max(0.3, 1 - gap * 0.03))
return action["damage"] * scale * (1 - damage_reduction(player)) * blessing["taken"]
def fighter(player: dict, blessed: dict, brew: dict) -> dict:
"""Your side of a fight: power from level and gear (plus prayers and potions) aims and hits; armour dodges and soaks."""
power = combat_power(player) + blessed["power"] + brew.get("power", 0)
return {"interval": PLAYER_INTERVAL, "accuracy": 10 + 2 * power, "max_hit": max(2, round(2 + power / 3)),
"evasion": 10 + 2 * (skill_level(player, "combat") + armour(player) // 2),
"taken": (1 - damage_reduction(player)) * blessed["taken"] * (1 - brew.get("reduce", 0))}
def hit_chance(accuracy: float, evasion: float) -> float:
return min(0.95, max(0.05, accuracy / (accuracy + evasion) + 0.15))
def _average_hit(max_hit: float) -> float:
return (1 - CRIT_CHANCE) * (max_hit + 1) / 2 + CRIT_CHANCE * max_hit * CRIT_MULT
def fight_odds(player: dict, action: dict) -> dict:
"""What a fight against this enemy looks like on average, for the cards."""
me, foe = fighter(player, blessing(player), potion_effect(player, "combat")), action["enemy"]
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
dps = hit * _average_hit(me["max_hit"]) * (1 - foe["reduction"]) / me["interval"]
kill = foe["hp"] / dps
hurt = their_hit * _average_hit(foe["max_hit"]) * me["taken"] / foe["interval"] * kill
return {"hit": round(hit, 2), "their_hit": round(their_hit, 2), "max_hit": me["max_hit"],
"their_max": max(1, round(foe["max_hit"] * me["taken"])), "kill_time": round(kill, 1), "damage": round(hurt, 1)}
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
"""Play out one fight swing by swing. Nothing changes until it's over: the result is applied then."""
foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat")
me = fighter(player, blessed, brew)
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
pantry = {item: player["bank"][item] for item in edible(player)}
eaten, events, hurt = {}, [], 0
wind = blessed["wind"] and not (player["action"] or {}).get("wind_used")
pace = speed(world)
mine, theirs, result, t = me["interval"], foe["interval"], None, 0.0
while result is None:
t = min(mine, theirs)
if t > MAX_FIGHT:
result = "fled"
break
crit = rng.random() < CRIT_CHANCE
if mine <= theirs:
mine += me["interval"]
landed = rng.random() < hit
dealt = max(1, round((me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))
* (1 - foe["reduction"]))) if landed else 0
foe_hp = max(0, foe_hp - dealt)
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp), foe_hp])
if not foe_hp:
result = "won"
continue
theirs += foe["interval"]
landed = rng.random() < their_hit
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * me["taken"])) if landed else 0
hp -= dealt
hurt += dealt
events.append([round(t / pace, 2), "foe", ("crit" if crit else "hit") if landed else "miss", dealt, max(0, round(hp)), foe_hp])
while hp < top * EAT_BELOW and any(pantry.values()):
food = next(item for item, left in pantry.items() if left)
pantry[food] -= 1
eaten[food] = eaten.get(food, 0) + 1
hp = min(top, hp + ITEMS[food]["heal"])
events.append([round(t / pace, 2), "you", "eat", ITEMS[food]["heal"], round(hp), foe_hp, food])
beaten, starving = hp <= 0, hp < top * FLEE_BELOW and not any(pantry.values())
if (beaten or starving) and wind:
# Second Wind: the retreat that was coming turns into half health, once per outing.
wind = False
hp = top / 2
events.append([round(t / pace, 2), "you", "wind", 0, round(hp), foe_hp])
elif beaten:
hp, result = 1, "lost"
elif starving:
result = "fled"
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]),
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": (t + RESPAWN) / pace,
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events,
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
"wind_used": blessed["wind"] and not wind and not (player["action"] or {}).get("wind_used")}
# --- prayer -------------------------------------------------------------------
@ -1067,17 +1235,6 @@ def edible(player: dict) -> list[str]:
return foods
def auto_eat(player: dict, summary: dict) -> None:
top = max_hp(player)
while player["hp"] < top * EAT_BELOW:
foods = edible(player)
if not foods:
return
bank_take(player, foods[0], 1)
player["hp"] = min(top, player["hp"] + ITEMS[foods[0]]["heal"])
summary["eaten"][foods[0]] = summary["eaten"].get(foods[0], 0) + 1
def action_time(world: dict, player: dict, action: dict) -> float:
seconds = action["time"]
if action["skill"] in CLASSES[player["cls"]]["fast"]:
@ -1140,12 +1297,6 @@ def plant_second_plot(world: dict, player: dict, action_id: str | None) -> str |
return None
def with_potion(blessed: dict, brew: dict) -> dict:
"""A fight's prayer boosts with a combat potion's added on top."""
return {**blessed, "power": blessed["power"] + brew.get("power", 0),
"taken": blessed["taken"] * (1 - brew.get("reduce", 0))}
def potion_effect(player: dict, skill: str) -> dict:
active = player.get("potions", {}).get(skill)
return ITEMS[active["item"]]["potion"] if active else {}
@ -1227,7 +1378,7 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
return "No raid to prepare for"
if threat.get("drilled", 0) >= drill_needed(threat):
return "The militia is as drilled as it can be"
if "difficulty" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
if "enemy" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
return "Too hurt to fight. Rest, or bring food"
return None
@ -1258,52 +1409,45 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
outputs[item] = int(exact) + (1 if rng.random() < exact - int(exact) else 0)
xp *= mods["xp"]
if "difficulty" in action:
blessed = blessing(player)
if player["prayers"] and not blessed["cost"]:
summary["prayer_out"] = True
if blessed["cost"]:
player["pp"] -= blessed["cost"]
summary["pp_spent"] += blessed["cost"]
gain_xp(world, player, "prayer", blessed["cost"] * PRAYER_XP_PER_POINT, now, summary)
regen(world, player, now, blessed["regen"])
blessed = with_potion(blessed, brew)
auto_eat(player, summary)
chance = 0.5 + (combat_power(player) + blessed["power"] - action["difficulty"]) * 0.04
won = rng.random() < min(0.97, max(0.15, chance))
hurt = fight_damage(player, action, blessed) * rng.uniform(0.6, 1.4) * (1 if won else 1.3)
player["hp"] = min(max_hp(player), player["hp"] - hurt + brew.get("heal", 0))
summary["damage"] += hurt
auto_eat(player, summary)
if "enemy" in action:
outing = player["action"]
if player["hp"] <= 0 and blessed["wind"] and outing and not outing.get("wind_used"):
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
if player["prayers"] and not fight["pp"]:
summary["prayer_out"] = True
paid = min(player["pp"], fight["pp"])
if paid:
player["pp"] -= paid
summary["pp_spent"] += paid
gain_xp(world, player, "prayer", paid * PRAYER_XP_PER_POINT, now, summary)
for food, count in fight["eaten"].items():
count = min(count, player["bank"].get(food, 0))
if count:
bank_take(player, food, count)
summary["eaten"][food] = summary["eaten"].get(food, 0) + count
player["hp"], player["hp_at"] = fight["hp"], now
summary["damage"] += fight["damage"]
if fight["wind_used"]:
outing["wind_used"] = True
player["hp"] = max_hp(player) / 2
summary["second_wind"] += 1
if player["hp"] <= 0:
# Out of food and out of luck: fall back with nothing but your life.
player["hp"] = 1
if fight["result"] != "won":
# Beaten or out of food: fall back with nothing but your life.
summary["losses"] += 1
reason = "Retreated: ran out of food" if not edible(player) else "Retreated"
stop(player, reason)
summary["stopped"] = reason
gain_xp(world, player, "combat", xp // 3, now, summary)
return
if won:
summary["wins"] += 1
player["gold"] += action.get("gold", 0)
summary["gold"] += action.get("gold", 0)
for item, qty, odds in action.get("drops", []):
if rng.random() < odds:
outputs[item] = outputs.get(item, 0) + qty
if action.get("special") == "sabotage":
raid = upcoming_raid(world)
raid["sabotage"] = min(0.3, raid["sabotage"] + 0.03)
if action.get("special") == "problem":
_work_problem(world, player, action, now)
else:
summary["losses"] += 1
xp = xp // 3
summary["wins"] += 1
player["gold"] += action.get("gold", 0)
summary["gold"] += action.get("gold", 0)
for item, qty, odds in action.get("drops", []):
if rng.random() < odds:
outputs[item] = outputs.get(item, 0) + qty
if action.get("special") == "sabotage":
raid = upcoming_raid(world)
raid["sabotage"] = min(0.3, raid["sabotage"] + 0.03)
if action.get("special") == "problem":
_work_problem(world, player, action, now)
if action.get("special") == "recon":
event = upcoming_raid(world)
# Once a threat is fully scouted, scouting only trains the skill (and keeps the informant's leads).
@ -1391,7 +1535,13 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
# The second crop can't grow right now (a problem, too low a level): tend the first only.
current["turn"] = 0
action = growing(player)
duration = action_time(world, player, action)
if "enemy" in action and not can_start(world, player, action):
if not current.get("fight"):
regen(world, player, current["start"], blessing(player)["regen"])
current["fight"] = simulate_fight(world, player, action, rng)
duration = current["fight"]["duration"]
else:
duration = action_time(world, player, action)
if current["start"] + duration > limit:
break
reason = can_start(world, player, action)
@ -2266,10 +2416,10 @@ PROBLEMS = {
}
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
PROBLEM_ACTIONS = [
_action("clear_wolves", "combat", "Drive off the wolves", 1, 60, 16, difficulty=6, damage=3,
special="problem", problem="wolves"),
_action("catch_goblins", "combat", "Catch the goblin thieves", 1, 60, 16, difficulty=8, damage=3,
special="problem", problem="goblins"),
_fight("clear_wolves", monster("Wolf pack", "beast", 4), "Frontier", name="Drive off the wolves",
special="problem", problem="wolves"),
_fight("catch_goblins", monster("Goblin thieves", "human", 5), "Frontier", name="Catch the goblin thieves",
special="problem", problem="goblins"),
_action("drive_bear", "hunting", "Drive off the bear", 1, 45, 14, special="problem", problem="bear"),
_action("cull_boars", "hunting", "Cull the boar herd", 1, 45, 14, special="problem", problem="boars"),
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
@ -2935,6 +3085,18 @@ PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("bic
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
TROPHY_GLYPHS = {"golem": "crystal-growth", "troll": "animal-hide", "spirit": "magic-swirl", "flyer": "sewing-string"}
TIER_TINTS = ["#9aa1ab", "#d4dae2", "#6f8fec", "#4cb878", "#3fd0e0"] # trophies of areas II to VI
MONSTER_GLYPHS = {"golem": ["rock-golem"] * 5, "troll": ["troll"] * 5,
"spirit": ["fairy", "spectre", "ghost", "spectre", "ghost"],
"flyer": ["bat", "harpy", "wyvern", "griffin-symbol", "eagle-head"]}
ACTION_GLYPHS.update({
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
})
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
for _index, _name in enumerate(_monsters):
ACTION_GLYPHS[f"fight_{_slug(_name)}"] = (MONSTER_GLYPHS[_kind][_index], TIER_TINTS[_index])
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield"]
@ -2959,6 +3121,8 @@ def _assign_glyphs() -> None:
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
for suffix, glyph in LEATHER_GLYPHS.items():
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
for trophy, (_name, kind, tier) in TROPHIES.items():
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[kind], tint=TIER_TINTS[tier - 1])
for herb, tint in HERB_TINTS.items():
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion",