From bf013a1648fad55b66c6c912c028da97edaab517 Mon Sep 17 00:00:00 2001 From: rope Date: Sat, 24 Oct 2026 10:01:41 +0100 Subject: [PATCH] Ability icons from game-icons instead of emoji Co-Authored-By: Claude Opus 5.5 --- combat.py | 52 +++++++++++++++++++++++------------------------ game.py | 22 +++++++++++++++++++- static/app.js | 22 ++++++++++++-------- static/icons.svg | 23 +++++++++++++++++++++ static/styles.css | 2 ++ 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/combat.py b/combat.py index 5d4d700..5a17e96 100644 --- a/combat.py +++ b/combat.py @@ -46,66 +46,66 @@ def _aura(kind, dur, on, **extra): ABILITIES = { # Sword and shield: hold the line. - "shield_bash": {"name": "Shield Bash", "icon": "๐Ÿ›ก๏ธ", "needs": "shield", "target": "enemy", "cd": 10, + "shield_bash": {"name": "Shield Bash", "needs": "shield", "target": "enemy", "cd": 10, "offgcd": True, "threat": 2, "effects": [_dmg(1.2), {"t": "stun", "dur": 2}], "auto": {"r": "stop", "else": True}, "tip": "Hit, stun 2s, stop a cast. Bosses only lose the cast."}, - "challenge": {"name": "Challenge", "icon": "๐Ÿ˜ค", "target": "enemy", "cd": 8, "offgcd": True, + "challenge": {"name": "Challenge", "target": "enemy", "cd": 8, "offgcd": True, "effects": [{"t": "taunt", "dur": 3}], "auto": {"r": "taunt"}, "tip": "An enemy turns on you for 3s."}, - "sweeping_cut": {"name": "Sweeping Cut", "icon": "๐ŸŒ€", "target": "aoe", "cd": 10, "threat": 2, + "sweeping_cut": {"name": "Sweeping Cut", "target": "aoe", "cd": 10, "threat": 2, "effects": [{"t": "aoe", "c": 0.8}], "auto": {"r": "aoe", "n": 2}, "tip": "Hits every enemy. Double threat."}, - "shield_wall": {"name": "Shield Wall", "icon": "๐Ÿงฑ", "needs": "shield", "target": "self", "cd": 30, "offgcd": True, + "shield_wall": {"name": "Shield Wall", "needs": "shield", "target": "self", "cd": 30, "offgcd": True, "effects": [_aura("taken", 6, "self", mult=0.5)], "auto": {"r": "tanking", "pct": 0.6}, "tip": "Half damage taken for 6s."}, # Warhammer: slow, heavy, and hard to stand up to. - "crushing_blow": {"name": "Crushing Blow", "icon": "๐Ÿ”จ", "target": "enemy", "cd": 16, "cast": 1.5, + "crushing_blow": {"name": "Crushing Blow", "target": "enemy", "cd": 16, "cast": 1.5, "effects": [_dmg(1.5)], "auto": {"r": "always"}, "tip": "Wind up 1.5s, hit for 1.5ร—."}, - "concuss": {"name": "Concuss", "icon": "๐Ÿ’ซ", "target": "enemy", "cd": 15, "offgcd": True, + "concuss": {"name": "Concuss", "target": "enemy", "cd": 15, "offgcd": True, "effects": [{"t": "stun", "dur": 3}], "auto": {"r": "stop", "else": True}, "tip": "Stun 3s, stop a cast. Bosses only lose the cast."}, - "earthshaker": {"name": "Earthshaker", "icon": "๐ŸŒ‹", "target": "aoe", "cd": 12, + "earthshaker": {"name": "Earthshaker", "target": "aoe", "cd": 12, "effects": [{"t": "aoe", "c": 0.5}, {"t": "delay", "by": 1.5}], "auto": {"r": "aoe", "n": 2}, "tip": "Hits every enemy and puts their next swing back 1.5s."}, - "unstoppable": {"name": "Unstoppable", "icon": "๐Ÿ˜ก", "target": "self", "cd": 45, "offgcd": True, + "unstoppable": {"name": "Unstoppable", "target": "self", "cd": 45, "offgcd": True, "effects": [_aura("done", 12, "self", mult=1.2)], "auto": {"r": "big"}, "tip": "+20% damage for 12s."}, # Staff: magic, and keeping everyone standing. - "bolt": {"name": "Bolt", "icon": "โœจ", "target": "enemy", "cd": 10, "cast": 1.5, + "bolt": {"name": "Bolt", "target": "enemy", "cd": 10, "cast": 1.5, "effects": [_dmg(1.1)], "auto": {"r": "always"}, "tip": "Cast 1.5s, magic hit for 1.1ร—."}, - "mend": {"name": "Mend", "icon": "๐Ÿ’š", "target": "ally", "cd": 10, "cast": 2, + "mend": {"name": "Mend", "target": "ally", "cd": 10, "cast": 2, "effects": [{"t": "heal", "c": 2.0}], "auto": {"r": "heal", "pct": 0.5}, "tip": "Cast 2s, heal someone for 2ร— your hit."}, - "ward": {"name": "Ward", "icon": "๐Ÿ”ฎ", "target": "ally", "cd": 20, + "ward": {"name": "Ward", "target": "ally", "cd": 20, "effects": [{"t": "shield", "c": 2.0, "dur": 20}], "auto": {"r": "ward", "pct": 0.8}, "tip": "Soaks 2ร— your hit for someone, for up to 20s."}, - "thornbind": {"name": "Thornbind", "icon": "๐ŸŒฟ", "target": "enemy", "cd": 20, + "thornbind": {"name": "Thornbind", "target": "enemy", "cd": 20, "effects": [{"t": "cc", "dur": 15}], "auto": {"r": "cc"}, "tip": "Holds an enemy 15s. Damage breaks it. Not bosses."}, # Bow: fast, first, and from a distance. - "aimed_shot": {"name": "Aimed Shot", "icon": "๐ŸŽฏ", "target": "enemy", "cd": 10, "cast": 2, + "aimed_shot": {"name": "Aimed Shot", "target": "enemy", "cd": 10, "cast": 2, "effects": [_dmg(2.0)], "auto": {"r": "always"}, "tip": "Aim 2s, hit for 2ร—."}, - "pinning_shot": {"name": "Pinning Shot", "icon": "๐Ÿ“Œ", "target": "enemy", "cd": 12, "offgcd": True, + "pinning_shot": {"name": "Pinning Shot", "target": "enemy", "cd": 12, "offgcd": True, "effects": [_dmg(0.5), {"t": "interrupt"}], "auto": {"r": "stop"}, "tip": "Hit and stop a cast."}, - "barbed_arrow": {"name": "Barbed Arrow", "icon": "๐Ÿฉธ", "target": "enemy", "cd": 12, + "barbed_arrow": {"name": "Barbed Arrow", "target": "enemy", "cd": 12, "effects": [_dmg(0.4), {"t": "dot", "c": 0.25, "every": 2, "dur": 12}], "auto": {"r": "dot"}, "tip": "Hit, then bleeds 6ร— 0.25 over 12s."}, - "volley": {"name": "Volley", "icon": "๐Ÿน", "target": "aoe", "cd": 10, "cost": 5, + "volley": {"name": "Volley", "target": "aoe", "cd": 10, "cost": 5, "effects": [{"t": "aoe", "c": 0.9}], "auto": {"r": "aoe", "n": 2}, "tip": "Hits every enemy. 5 arrows."}, # Trades: one each, whatever the weapon. - "find_the_seam": {"name": "Find the Seam", "icon": "โ›๏ธ", "target": "enemy", "cd": 30, "offgcd": True, + "find_the_seam": {"name": "Find the Seam", "target": "enemy", "cd": 30, "offgcd": True, "effects": [_aura("seam", 8, "target")], "auto": {"r": "debuff", "pct": 0.6}, "tip": "8s: its resistance doesn't count and its weakness counts double, for everyone."}, - "barricade": {"name": "Barricade", "icon": "๐Ÿšง", "target": "party", "cd": 40, "offgcd": True, + "barricade": {"name": "Barricade", "target": "party", "cd": 40, "offgcd": True, "effects": [_aura("taken", 8, "party", mult=0.75)], "auto": {"r": "danger", "pct": 0.6}, "tip": "Everyone takes 25% less damage for 8s."}, - "hearty_meal": {"name": "Hearty Meal", "icon": "๐Ÿฒ", "target": "party", "cd": 30, + "hearty_meal": {"name": "Hearty Meal", "target": "party", "cd": 30, "effects": [{"t": "hot", "c": 0.3, "every": 2, "dur": 10}], "auto": {"r": "party_hot", "pct": 0.75}, "tip": "Everyone heals 5ร— 0.3 of your hit over 10s."}, - "mark_prey": {"name": "Mark Prey", "icon": "๐Ÿพ", "target": "enemy", "cd": 30, "offgcd": True, + "mark_prey": {"name": "Mark Prey", "target": "enemy", "cd": 30, "offgcd": True, "effects": [_aura("vuln", 15, "target", mult=1.15)], "auto": {"r": "debuff", "pct": 0.6}, "tip": "It takes 15% more damage from everyone for 15s."}, } @@ -147,17 +147,17 @@ def role(style: str, shield: bool) -> str: # the enemy's average hit, damage type: None for its own). Telegraphed casts always land. ENEMY_ABILITIES = { - "heavy_slam": {"name": "Heavy Slam", "icon": "๐Ÿ’ฅ", "cast": 2.0, "every": 14, "first": 5, "interrupt": False, + "heavy_slam": {"name": "Heavy Slam", "cast": 2.0, "every": 14, "first": 5, "interrupt": False, "target": "target", "effect": ("hit", 1.6, "blunt")}, - "lunge": {"name": "Lunge", "icon": "๐Ÿ—ก๏ธ", "cast": 0, "every": 10, "first": 4, "interrupt": False, + "lunge": {"name": "Lunge", "cast": 0, "every": 10, "first": 4, "interrupt": False, "target": "soft", "effect": ("hit", 1.2, None)}, - "hex_bolt": {"name": "Hex Bolt", "icon": "๐Ÿ”ฅ", "cast": 2.5, "every": 12, "first": 3, "interrupt": True, + "hex_bolt": {"name": "Hex Bolt", "cast": 2.5, "every": 12, "first": 3, "interrupt": True, "target": "target", "effect": ("hit", 1.8, "magic")}, - "sweep": {"name": "Sweep", "icon": "๐ŸŒ€", "cast": 1.5, "every": 16, "first": 8, "interrupt": False, + "sweep": {"name": "Sweep", "cast": 1.5, "every": 16, "first": 8, "interrupt": False, "target": "all", "effect": ("aoe", 0.8, "slash")}, - "fire_breath": {"name": "Fire Breath", "icon": "๐Ÿ‰", "cast": 3.0, "every": 18, "first": 6, "interrupt": False, + "fire_breath": {"name": "Fire Breath", "cast": 3.0, "every": 18, "first": 6, "interrupt": False, "target": "all", "effect": ("aoe", 1.4, "magic")}, - "tail_swipe": {"name": "Tail Swipe", "icon": "๐Ÿ’ข", "cast": 0, "every": 12, "first": 7, "interrupt": False, + "tail_swipe": {"name": "Tail Swipe", "cast": 0, "every": 12, "first": 7, "interrupt": False, "target": "target", "effect": ("hit", 1.2, "blunt")}, } ARCHETYPE_ABILITIES = {"brute": ["heavy_slam"], "skirmisher": ["lunge"], "caster": ["hex_bolt"], diff --git a/game.py b/game.py index ea78704..439ad69 100644 --- a/game.py +++ b/game.py @@ -4640,6 +4640,22 @@ ACTION_GLYPHS = { "hunt_elk": ("deer", "#a07a50"), "hunt_sabrecat": ("saber-toothed-cat-head", "#e0a050"), "hunt_mammoth": ("mammoth", "#8a6a50"), } +# Abilities: yours by weapon and trade, then the enemies' (shown as they cast). +ABILITY_GLYPHS = { + "shield_bash": ("shield-bash", "#d4dae2"), "challenge": ("shouting", "#e0a050"), + "sweeping_cut": ("sword-spin", "#d4dae2"), "shield_wall": ("stone-wall", "#a8a29e"), + "crushing_blow": ("hammer-break", "#d1a46a"), "concuss": ("knockout", "#f0d060"), + "earthshaker": ("quake-stomp", "#c08850"), "unstoppable": ("enrage", "#e06a6a"), + "bolt": ("bolt-spell-cast", "#a77be8"), "mend": ("healing", "#5fae5a"), + "ward": ("energy-shield", "#8fb8f0"), "thornbind": ("thorny-vine", "#6f9a4a"), + "aimed_shot": ("target-arrows", "#e0a050"), "pinning_shot": ("arrowed", "#8fd0e0"), + "barbed_arrow": ("barbed-arrow", "#e05a5a"), "volley": ("arrow-cluster", "#5fae8a"), + "find_the_seam": ("armor-downgrade", "#d07a3c"), "barricade": ("barricade", "#d1a46a"), + "hearty_meal": ("cooking-pot", "#e0b04a"), "mark_prey": ("targeted", "#e06a6a"), + "heavy_slam": ("punch-blast", "#e06a6a"), "lunge": ("pierced-body", "#e06a6a"), + "hex_bolt": ("fireball", "#a77be8"), "sweep": ("spinning-blades", "#e06a6a"), + "fire_breath": ("dragon-breath", "#ff8a3a"), "tail_swipe": ("spiked-tail", "#c08850"), +} PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("biceps", "#e06a6a"), "rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"), "superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"), @@ -4731,6 +4747,8 @@ def _assign_glyphs() -> None: spec["glyph"], spec["tint"] = BUILDING_GLYPHS[key] for prayer in PRAYERS: prayer["glyph"], prayer["tint"] = PRAYER_GLYPHS[prayer["id"]] + for key, ability in [*combat.ABILITIES.items(), *combat.ENEMY_ABILITIES.items()]: + ability["glyph"], ability["tint"] = ABILITY_GLYPHS[key] for action in ACTIONS: if action["id"] in ACTION_GLYPHS: action["glyph"], action["tint"] = ACTION_GLYPHS[action["id"]] @@ -4751,7 +4769,8 @@ def _assign_glyphs() -> None: def all_glyphs() -> set[str]: """Every glyph the game draws, for building the sprite.""" names = set(UI_GLYPHS) - for collection in (ITEMS.values(), SKILLS, CLASSES.values(), BUILDINGS.values(), PRAYERS, ACTIONS): + for collection in (ITEMS.values(), SKILLS, CLASSES.values(), BUILDINGS.values(), PRAYERS, ACTIONS, + combat.ABILITIES.values(), combat.ENEMY_ABILITIES.values()): names.update(entry["glyph"] for entry in collection) return names @@ -4781,6 +4800,7 @@ PATCH_NOTES = [ "Retreating is gone."), ("new", "Bosses and dragons are group fights (2+). Start one alone and you wait there for someone to join. " "They grow with the group: +75% HP for each fighter."), + ("fix", "Abilities, enemy casts and fight statuses have proper icons instead of emoji."), ("balance", "Enemies have 20% more HP for the same XP, since everyone has abilities now."), ("balance", "Boss hits: area bosses 1.35 โ†’ 1.2ร— and dragons 1.5 โ†’ 1.9ร— their level's max hit, then ร—1.3 " "against a pair."), diff --git a/static/app.js b/static/app.js index d21e6cd..c48f1f1 100644 --- a/static/app.js +++ b/static/app.js @@ -38,7 +38,8 @@ const COIN = glyph("two-coins", "#e0b04a"), ANY_FOOD = glyph("knife-fork", "#d9c function iconise(content) { const entries = [...Object.values(content.items), ...content.skills, ...Object.values(content.classes), - ...Object.values(content.buildings), ...content.prayers, ...content.actions]; + ...Object.values(content.buildings), ...content.prayers, ...content.actions, + ...Object.values(content.abilities), ...Object.values(content.enemy_abilities)]; for (const entry of entries) if (entry.glyph) entry.icon = glyph(entry.glyph, entry.tint); return content; } @@ -938,7 +939,7 @@ function abilityBar(me) { const potion = me.health_potion; return `
${kit.join("")}${trade ? `${tile(trade, true)}` : ""} โค๏ธ${potion ? `${fmt(me.bank[potion])}` : ""}
`; + title="${esc(potion ? `${item(potion).name}: +${item(potion).drink} HP, once a fight under ${Math.round(C.potion_below * 100)}% ยท ${fmt(me.bank[potion])} left` : "No health potion")}">${item(potion || "health_potion_1").icon}${potion ? `${fmt(me.bank[potion])}` : ""}`; } // Prayers you can switch on and off between swings, and the points left to pay for them. @@ -966,18 +967,18 @@ function fightLine(e, f) { return `
  • ${name(src)} โ†’ ${name(tgt)} ${n}${kind === "crit" ? " crit" : ""}${what}
  • `; case "miss": return `
  • ${name(src)} โ†’ ${name(tgt)} miss
  • `; case "block": return `
  • ${name(tgt)} block${tgt === S.uid ? "" : "s"}${what}
  • `; - case "absorb": return `
  • ๐Ÿ”ฎ ${name(tgt)} soak${tgt === S.uid ? "" : "s"} ${n}
  • `; + case "absorb": return `
  • ${C.abilities.ward.icon} ${name(tgt)} soak${tgt === S.uid ? "" : "s"} ${n}
  • `; case "cast": return `
  • ${name(src)}${what}โ€ฆ
  • `; case "use": return `
  • ${name(src)}${what}${tgt && tgt !== src ? ` โ†’ ${name(tgt)}` : ""}
  • `; case "stop": return `
  • ${name(src)} โœ‹ ${name(tgt)}${what}
  • `; - case "heal": return `
  • ${label === "potion" ? `${name(tgt)} โค๏ธ potion` : `${name(src)}${what} โ†’ ${name(tgt)}`} +${n}
  • `; + case "heal": return `
  • ${label === "potion" ? `${name(tgt)} ${item("health_potion_1").icon} potion` : `${name(src)}${what} โ†’ ${name(tgt)}`} +${n}
  • `; case "mend": return `
  • ${name(tgt)} +${n}${what}
  • `; case "cc": return `
  • ${what} ${name(tgt)} held
  • `; case "free": return `
  • ${name(tgt)} breaks free
  • `; case "die": return `
  • ${name(tgt)} falls
  • `; - case "enrage": return `
  • ๐Ÿ˜ก ${name(src)} enrages
  • `; - case "wind": return `
  • ๐ŸŒฌ๏ธ Second wind! ${name(src)}
  • `; - case "down": return `
  • ${src === S.uid ? "You're knocked out" : `${name(src)} is knocked out`}
  • `; + case "enrage": return `
  • ${C.abilities.unstoppable.icon} ${name(src)} enrages
  • `; + case "wind": return `
  • ${C.prayers.find((p) => p.id === "second_wind").icon} Second wind! ${name(src)}
  • `; + case "down": return `
  • ${C.abilities.concuss.icon} ${src === S.uid ? "You're knocked out" : `${name(src)} is knocked out`}
  • `; case "left": return `
  • ${name(src)} left
  • `; } return ""; @@ -1038,7 +1039,12 @@ function animateFight(now) { el.classList.toggle("out", !!u.out); el.querySelector("[data-u-bar]").style.width = `${Math.max(0, u.hp / u.top * 100)}%`; el.querySelector("[data-u-hp]").textContent = u.out === "left" ? "left" : t < u.from && !resting ? "joiningโ€ฆ" : `${fmt(u.hp)} / ${fmt(u.top)}`; - el.querySelector("[data-u-status]").textContent = u.out === "dead" ? "๐Ÿ’€" : u.out === "down" ? "๐Ÿ’ซ" : u.held ? "๐ŸŒฟ" : u.enraged ? "๐Ÿ˜ก" : ""; + const status = u.out === "dead" ? "" : u.out === "down" ? "concuss" : u.held ? "thornbind" : u.enraged ? "unstoppable" : ""; + const statusEl = el.querySelector("[data-u-status]"); + if (statusEl.dataset.key !== status) { + statusEl.dataset.key = status; + statusEl.innerHTML = status ? C.abilities[status].icon : ""; + } el.querySelector("[data-u-swing]").style.width = gone || t < u.from ? "0%" : `${Math.min(100, (t - (u.swung ?? u.from)) / u.every * 100)}%`; const cast = !gone && u.cast && t < u.cast.until ? u.cast : null; const spec = cast && (el.dataset.u[0] === "e" ? C.enemy_abilities : C.abilities)[cast.label]; diff --git a/static/icons.svg b/static/icons.svg index 16b39a0..5b3a05a 100644 --- a/static/icons.svg +++ b/static/icons.svg @@ -7,17 +7,22 @@ + + + + + @@ -58,17 +63,21 @@ + + + + @@ -87,10 +96,12 @@ + + @@ -106,6 +117,7 @@ + @@ -134,11 +146,14 @@ + + + @@ -162,8 +177,10 @@ + + @@ -172,6 +189,8 @@ + + @@ -185,8 +204,12 @@ + + + + diff --git a/static/styles.css b/static/styles.css index 5f94509..5bbd988 100644 --- a/static/styles.css +++ b/static/styles.css @@ -575,6 +575,8 @@ button.sheet-skill:hover { background: var(--panel-2); } .ab.locked { opacity: .35; filter: grayscale(1); } .ab.fired { border-color: var(--gold); box-shadow: 0 0 0 2px var(--gold); } .ab.spent { opacity: .35; } +.ab .gi { width: 26px; height: 26px; } +.fight-log .gi, .cast .gi, [data-u-status] .gi { width: 15px; height: 15px; vertical-align: -3px; } .ab-cd { position: absolute; left: 0; right: 0; bottom: 0; height: 0; background: rgba(0, 0, 0, .55); pointer-events: none; } .ab-lv { position: absolute; bottom: 0; right: 2px; font-size: 10px; font-weight: 700; color: var(--muted); filter: none; } .ab-gap { width: 6px; }