From 308c3a3c013a3a6bb5488ce5a429806366912964 Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 20 Oct 2026 15:30:05 +0100 Subject: [PATCH] Combat in one frame: you, prayers and gear left; the enemy, its traits and the log right. Fight chip in enemy red Co-Authored-By: Claude Opus 5.5 --- static/app.js | 69 +++++++++++++++++++++++++---------------------- static/styles.css | 31 ++++++++++++++++++++- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/static/app.js b/static/app.js index ebf7142..05a75de 100644 --- a/static/app.js +++ b/static/app.js @@ -360,7 +360,7 @@ function topbar() { let chips = ""; if (S.me?.action) { const action = C.actions.find((a) => a.id === (S.me.action.growing || S.me.action.id)); - chips += ``; + chips += ``; } else if (S.me) { chips += `Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}`; } @@ -421,7 +421,7 @@ function skillPage(id) { ${fast ? `${cls.icon} ${cls.name} bonus` : ""} `; - if (id === "combat") html += fightPanel() + combatPanel(); + if (id === "combat") html += combatFrame(); if (id === "prayer") html += prayerPanel(); if (id === "construction") html += S.city.construction ? siteBanner() : `
Nothing under construction.
`; @@ -550,21 +550,45 @@ function variantRows(e, o) { }).join("")}`; } -// The fight in progress, replayed from the server's swing-by-swing record. -function fightPanel() { - const f = S.me.action?.fight; - if (!f) return ""; - const side = (who, name, foe) => `
-
${esc(name)}${foe ? `${resistIcons(foe)}` : ""}
-
-
+// Everything about fighting in one frame. Left: you, your prayers (switch them mid-fight) and your +// gear (swap it mid-fight). Right: what you're fighting, what it attacks with, what it's weak to and +// resists (your weapon's type outlined), and the fight as it plays out. +function combatFrame() { + const me = S.me, a = me.action, f = a?.fight; + const encounter = f && C.actions.find((x) => x.id === a.id); + const bars = (who) => `
+
`; + const you = `
+
${esc(me.name)}${HEART} ${fmt(me.hp)} / ${me.max_hp}
+ ${bars("you")} + ${me.edible.length ? "" : `
No food in your bank.
`}
`; - return `
-
${side("you", S.me.name)}⚔️${side("foe", f.enemy, f.foe)}
-
    + let foe = `
    Not fighting.
    `; + if (f) { + const mine = C.styles[(me.equip.weapon && item(me.equip.weapon).style) || "sword"].damage; + const pills = (label, kinds, how) => kinds?.length ? `
    ${label} + ${kinds.map((k) => `${dmgIcon(k, how)}${C.damage_types[k].name}`).join("")}
    ` : ""; + foe = `
    ${encounter?.icon || ""} +
    ${esc(f.enemy)}
    ${bars("foe")}
    + ${f.foe ? `
    ${pills("Attacks with", f.foe.attack, "attack")}${pills("Weak to", f.foe.vulnerable, "vuln")}${pills("Resists", f.foe.resists, "resist")}
    ` : ""} +
      `; + } + return `
      +
      ${you}${prayerBar(me)}${paperDoll(me, true)}
      +
      ${foe}
      `; } +// Prayers you can switch on and off between swings, and the points left to pay for them. +function prayerBar(me) { + const level = me.levels.prayer; + const buttons = C.prayers.filter((p) => p.level <= level).map((p) => { + const on = me.prayers.includes(p.id); + return ``; + }).join(""); + return `
      ${skillOf("prayer").icon} ${fmt(me.pp)}${buttons}
      `; +} + function fightLine(e, enemy) { const [, who, kind, amount, , , food] = e; const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : ""; @@ -617,24 +641,6 @@ function animateFight(now) { }); } -function combatPanel() { - const me = S.me; - const foods = me.edible.map((id) => ``).join(""); - const hpPct = me.hp / me.max_hp * 100; - return `
      -

      Health

      -
      ${HEART} ${fmt(me.hp)} / ${me.max_hp}recovers ${Math.round(C.regen_per_hour * 100)}% an hour
      -
      -
      -

      ${skillOf("prayer").icon} ${me.prayers.length ? `${me.prayers.map((id) => esc(C.prayers.find((p) => p.id === id).name)).join(" + ")} · ${fmt(me.pp)} points${me.blessing.cost ? "" : ` (out of points)`}` : `No prayers active.`} -

      -

      Eats below half HP, retreats when out of food. - ${me.edible.length ? "" : `No food in your bank.`}

      -
      - ${equipmentPanel(me, true, false)}
      `; -} - // Power, armour and ward, and the armour's weight. function gearStats(it) { const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`]; @@ -1439,7 +1445,6 @@ document.addEventListener("click", (e) => { }); document.addEventListener("change", (e) => { - if (e.target.matches("[data-food-pick]")) act("/api/food", { item: e.target.value || null }); }); $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") e.target.hidden = true; }); diff --git a/static/styles.css b/static/styles.css index af1bbfd..751128f 100644 --- a/static/styles.css +++ b/static/styles.css @@ -235,7 +235,7 @@ select { background: var(--bg); border: 1px solid var(--line); border-radius: 8p .fund { display: flex; flex-direction: column; gap: 6px; } .fight .fighters { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; align-items: center; } .fight .vs { font-size: 22px; } -.fight.waiting .fighter:last-child { opacity: .45; } +.fight.waiting .foe-side .foe-head, .fight.waiting .traits { opacity: .45; } .fighter { display: flex; flex-direction: column; gap: 6px; min-width: 0; } .fighter-name { display: flex; justify-content: space-between; gap: 8px; } .fighter-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -510,3 +510,32 @@ button.sheet-skill:hover { background: var(--panel-2); } .swap-qty { position: absolute; top: 1px; left: 3px; font-size: 10px; font-weight: 700; color: var(--muted); } .doll-slot.swap .swap-type .gi { width: 11px; height: 11px; } .doll-slot.swap .hands { top: 1px; right: 1px; font-size: 8px; padding: 0 2px; } + +/* The combat frame: you on the left, the enemy on the right */ +.combat-frame { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; } +.you-side, .foe-side { display: grid; gap: 14px; align-content: start; min-width: 0; } +.foe-head { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center; } +.foe-ico { width: 56px; height: 56px; border-radius: 12px; background: var(--panel-2); display: grid; place-items: center; } +.foe-ico .gi { width: 40px; height: 40px; } +.foe-idle { padding: 18px 0; } +.traits { display: grid; gap: 6px; } +.trait { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.trait > .muted { width: 84px; } +.pill { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; padding: 2px 9px 2px 3px; border-radius: 99px; + background: var(--panel-2); border: 1px solid var(--line); } +.pill.vuln { border-color: rgba(95, 174, 90, .6); } +.pill.resist { border-color: rgba(226, 100, 92, .55); } +.pill.yours { box-shadow: 0 0 0 2px var(--gold); } +.combat-frame .fight-log { margin: 0; } +.prayer-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.prayer-bar .pp { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; margin-right: 4px; } +.pray-btn { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; padding: 0; cursor: pointer; + background: var(--panel-2); border: 1px solid var(--line); color: inherit; opacity: .55; } +.pray-btn .gi { width: 22px; height: 22px; } +.pray-btn.on { opacity: 1; border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); } +@media (max-width: 900px) { + .combat-frame { display: flex; flex-direction: column; gap: 16px; } + .you-side { display: contents; } + .you-side > .fighter { order: 1; } .foe-side { order: 2; } .prayer-bar { order: 3; } .you-side > .doll-wrap { order: 4; } +} +.bar.mini.foe > span { background: var(--bad); }