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("")}
+
`;
}
+// 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; }