From 50f069b069e1d370484d5071cea1fdd484161d15 Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 20 Oct 2026 15:45:33 +0100 Subject: [PATCH] Combat stats grid pinned under the slot details; damage type on the equipped weapon Co-Authored-By: Claude Opus 5.5 --- app.py | 5 +++++ game.py | 3 ++- static/app.js | 39 +++++++++++++++++++++++++++------------ static/icons.svg | 4 ++++ static/styles.css | 18 ++++++++++++++---- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index 7d3b206..aa9796a 100644 --- a/app.py +++ b/app.py @@ -110,6 +110,9 @@ def player_view(world: dict, player: dict, now: float) -> dict: view["slots_used"] = game.slots_used(world, player) view["stack"] = game.stack_size(world) view["power"] = game.combat_power(player) + me = game.fighter(player, game.blessing(player), game.potion_effect(player, "combat")) + view["fighter"] = {"max_hit": me["max_hit"], "accuracy": round(me["accuracy"]), "evasion": me["evasion"], + "crit": me["crit"], "interval": me["interval"], "block": me["block"], "damage": me["damage"]} view["perk_costs"] = {perk: game.perk_cost(player, perk) for perk in player["perks"]} if player["action"]: action, fight = game.growing(player), player["action"].get("fight") @@ -171,6 +174,8 @@ def roster(world: dict, now: float) -> list[dict]: people.append({ "xp": player["xp"], "equip": player["equip"], "joined": player["joined"], "hp": int(player["hp"]), "max_hp": game.max_hp(player), "power": game.combat_power(player), + "fighter": {key: value for key, value in game.fighter(player, game.blessing(player), {}).items() + if key in ("max_hit", "accuracy", "evasion", "crit", "interval", "block", "damage")}, "armour": game.armour(player), "reduction": round(game.damage_reduction(player), 3), "ward": game.magic_ward(player), "ward_reduction": round(game.reduction(game.magic_ward(player)), 3), "favour": player["favour"], diff --git a/game.py b/game.py index 8fd30f9..90f5fc7 100644 --- a/game.py +++ b/game.py @@ -3560,7 +3560,8 @@ ACTION_GLYPHS.update({ for _index, (_area, _level, _prefix, _races) in enumerate(AREAS): for _race, *_rest in _races: ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index]) -UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield", "stopwatch", "swordman", "biceps", "cancel", +UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield", "stopwatch", "swordman", "biceps", "cancel", "striking-diamonds", + "archery-target", "dodging", "shield-reflect", "cape", "gem-pendant", "gauntlet", "ring", "quiver", "leather-boot", "crystal-ball", "visored-helm", "breastplate", "leg-armor", "pointy-sword", "round-shield", *(kind["glyph"] for kind in DAMAGE_TYPES.values())] diff --git a/static/app.js b/static/app.js index 05a75de..dad265c 100644 --- a/static/app.js +++ b/static/app.js @@ -1246,31 +1246,46 @@ function dollSlot(p, slot, label, outline, picked) { const tip = !known ? `${label}: not in the game yet` : id ? `${label}: ${item(id).name}` : `${label}: empty`; return ``; + ${id ? item(id).icon : glyph(outline, "#98a1b4")}${two ? `2H` : ""} + ${slot === "weapon" && id ? `${dmgIcon(C.styles[item(id).style || "sword"].damage)}` : ""}`; } function paperDoll(p, mine) { const pick = dollPick && slot2(dollPick, p); - const totals = `
- ${glyph("biceps", "#e06a6a")} Power${p.power} - ${glyph("checked-shield", "#d4dae2")} Armour${p.armour} - ${glyph("magic-shield", "#a77be8")} Ward${p.ward} -
`; - let side = totals; + let slot = ""; if (pick) { - const label = DOLL.find(([slot]) => slot === pick)[1]; + const label = DOLL.find(([s]) => s === pick)[1]; const two = pick === "shield" && p.equip.weapon && item(p.equip.weapon).two_handed; const id = two ? p.equip.weapon : p.equip[pick], it = id && item(id); - side = `
${it ? `${it.icon}${esc(it.name)}` : `${label}empty`}
- ${it ? gearChips(it, p) : ""}${mine ? swapGrid(p, pick, two) : ""}${totals}`; + slot = `
${it ? `${it.icon}${esc(it.name)}` : `${label}empty`}
+ ${it ? gearChips(it, p) : ""}${mine ? swapGrid(p, pick, two) : ""}`; } return `
${glyph("swordman", "#e6e8ee")} - ${DOLL.map(([slot, name, outline]) => dollSlot(p, slot, name, outline, slot === pick)).join("")}
-
${side}
+ ${DOLL.map(([s, name, outline]) => dollSlot(p, s, name, outline, s === pick)).join("")}
+
${slot}
${combatStats(p)}
`; } +// Your fighting numbers, always in the same place under the slot details: icons and numbers, +// with what each means on hover. +function combatStats(p) { + const f = p.fighter, pct = (x) => `${Math.round(x * 100)}%`, dt = C.damage_types[f.damage]; + const cell = (icon, value, tip) => `${icon}${value}`; + const cells = [ + cell(glyph("biceps", "#e06a6a"), p.power, "Power: from your level and gear"), + cell(glyph("checked-shield", "#d4dae2"), p.armour, `Armour: ${pct(p.reduction)} less damage from blows`), + cell(glyph("magic-shield", "#a77be8"), p.ward, `Ward: ${pct(p.ward_reduction)} less damage from magic`), + cell(glyph(dt.glyph, dt.tint), f.max_hit, `Max hit (${dt.name.toLowerCase()})`), + cell(glyph("striking-diamonds", "#f0d060"), pct(f.crit), "Crit chance"), + cell(glyph("stopwatch", "#9fb3c8"), `${f.interval}s`, `A swing every ${f.interval}s`), + cell(glyph("archery-target", "#e0a050"), fmt(f.accuracy), "Accuracy"), + cell(glyph("dodging", "#8fd0e0"), fmt(f.evasion), "Evasion"), + cell(glyph("shield-reflect", "#e0b04a"), pct(f.block), "Block chance (with a shield)"), + ]; + return `
Combat stats
${cells.join("")}
`; +} + // What else in the bank fits the picked slot, best first: click one to put it on. For weapons the // corner shows the damage type, so you can pick one for what you're about to fight. function swapGrid(p, slot, two) { diff --git a/static/icons.svg b/static/icons.svg index 9631a34..368fc32 100644 --- a/static/icons.svg +++ b/static/icons.svg @@ -4,6 +4,7 @@ + @@ -45,6 +46,7 @@ + @@ -132,6 +134,7 @@ + @@ -148,6 +151,7 @@ + diff --git a/static/styles.css b/static/styles.css index 66598e4..26f351e 100644 --- a/static/styles.css +++ b/static/styles.css @@ -485,10 +485,7 @@ button.sheet-skill:hover { background: var(--panel-2); } .doll-slot .hands { position: absolute; top: 2px; right: 2px; font-size: 9px; padding: 0 3px; } .doll-slot.small { width: 40px; height: 40px; cursor: default; } .doll-slot.small .gi { width: 26px; height: 26px; } -.doll-side { display: grid; gap: 12px; min-width: 0; } -.doll-stats { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; align-items: center; } -.doll-stats b { justify-self: end; } -.doll-stats .gi { width: 14px; height: 14px; } +.doll-side { min-width: 0; } .worn-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .worn-head > span:nth-child(2) { display: grid; } .worn-head .ico .gi { width: 22px; height: 22px; } @@ -541,3 +538,16 @@ button.sheet-skill:hover { background: var(--panel-2); } .bar.mini.foe > span { background: var(--bad); } .pill { padding: 3px 10px 3px 3px; } .pill .dmg { border-radius: 50%; } + +/* The doll's side: slot details on top, combat stats pinned to the bottom */ +.doll-wrap { align-items: stretch; } +.doll-side { display: flex; flex-direction: column; gap: 12px; } +.doll-slot-info { display: grid; gap: 10px; } +.combat-stats { margin-top: auto; display: grid; gap: 4px; } +.slot-type { position: absolute; bottom: -4px; left: -4px; } +.slot-type .dmg { width: 16px; height: 16px; background: var(--panel); border-radius: 4px; } +.doll-slot .slot-type .gi { width: 11px; height: 11px; } +.doll { align-self: start; } +.doll-slot-info { min-height: 200px; align-content: start; } +.swap-grid { flex-wrap: nowrap; overflow-x: auto; padding: 4px 4px 6px 0; scrollbar-width: thin; } +.doll-slot.swap { flex: none; }