Files
code_of_conquest_dnd/mockups/Combat HUD.dc.html
Phillip Tarrant 49bc6b22c0 docs(mockups): add the UI/systems bible (11 screens + README)
High-fidelity HTML/CSS/JS design references for the tactical-CRPG direction —
title, character creation, character sheet, inventory, world map, quest log,
shop, pause/settings, dialogue, combat HUD, main exploration window. README is
the handoff spec (visual system, per-screen state models, Godot translation
notes). Not production code — the source of truth for the game's look + systems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 16:06:23 -05:00

435 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet data-dc-atomics>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&display=swap" rel="stylesheet">
<style>
body{margin:0;background:#0a0806;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
@keyframes cturn{0%,100%{box-shadow:0 0 0 0 rgba(200,150,63,.5)}50%{box-shadow:0 0 0 6px rgba(200,150,63,0)}}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex;flex-direction:column;background:repeating-linear-gradient(135deg,#221c15 0 18px,#1c1710 18px 36px)">
<div style="position:absolute;inset:0;background:radial-gradient(110% 90% at 50% 42%,transparent 45%,rgba(0,0,0,.7) 100%);pointer-events:none"></div>
<!-- ===== TURN TRACKER ===== -->
<div style="position:relative;display:flex;align-items:center;gap:18px;padding:16px 26px;background:linear-gradient(180deg,rgba(10,8,6,.9),rgba(10,8,6,.5))">
<div style="flex:none;text-align:center;border-right:1px solid #3a352e;padding-right:18px">
<div style="font:10px ui-monospace,monospace;letter-spacing:.14em;color:#8a7f6c">ROUND</div>
<div style="font:600 30px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1">{{ round }}</div>
</div>
<div style="flex:1;display:flex;align-items:flex-end;gap:10px;overflow:hidden">
<sc-for list="{{ ribbon }}" as="u" hint-placeholder-count="6">
<div style="flex:none;display:flex;flex-direction:column;align-items:center;gap:4px;transform:{{ u.lift }};transition:transform .2s">
<div style="font:9px ui-monospace,monospace;color:#c8963f;height:11px">{{ u.nowLabel }}</div>
<div style="position:relative;width:48px;height:48px;border-radius:50%;border:2px solid {{ u.ring }};background:{{ u.fill }};display:flex;align-items:center;justify-content:center;font:600 14px system-ui;color:#f0e6d0;opacity:{{ u.opacity }};animation:{{ u.anim }}">
{{ u.glyph }}
<sc-if value="{{ u.dead }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:700 20px system-ui;color:#a5442e;background:rgba(10,8,6,.5);border-radius:50%"></div></sc-if>
</div>
<div style="width:48px;height:5px;border-radius:3px;background:#2a251f;overflow:hidden"><div style="width:{{ u.hpPct }};height:100%;background:{{ u.hpColor }}"></div></div>
<div style="font:9px system-ui;color:#9a8f78;max-width:56px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ u.name }}</div>
</div>
</sc-for>
</div>
</div>
<!-- ===== MIDDLE ===== -->
<div style="position:relative;flex:1;display:flex;min-height:0">
<!-- LEFT: party + log -->
<div style="width:330px;flex:none;padding:20px;display:flex;flex-direction:column;gap:16px;min-height:0">
<div>
<div style="font:11px ui-monospace,monospace;letter-spacing:.16em;color:#8a7f6c;margin-bottom:10px">YOUR PARTY · AI</div>
<div style="display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ allies }}" as="a" hint-placeholder-count="2">
<div style="display:flex;gap:11px;align-items:center;background:rgba(18,15,11,.7);border:1px solid {{ a.border }};border-radius:10px;padding:10px 12px">
<div style="width:38px;height:38px;flex:none;border-radius:50%;border:2px solid #5f8a9a;background:#243138;display:flex;align-items:center;justify-content:center;font:600 12px system-ui;color:#cfe0e6">{{ a.glyph }}</div>
<div style="flex:1;min-width:0">
<div style="font:600 14px system-ui;color:#e8d9b6">{{ a.name }}</div>
<div style="height:7px;border-radius:4px;background:#2a251f;overflow:hidden;margin-top:5px"><div style="width:{{ a.hpPct }};height:100%;background:#5f8a9a"></div></div>
</div>
<div style="font:10px ui-monospace,monospace;color:#9a8f78">{{ a.hpText }}</div>
</div>
</sc-for>
</div>
</div>
<!-- log -->
<div style="flex:1;min-height:0;display:flex;flex-direction:column;background:linear-gradient(180deg,#e7dcc4,#ddd0b2);border:1px solid #1a1512;border-radius:10px;overflow:hidden">
<div style="font:10px ui-monospace,monospace;letter-spacing:.14em;color:#8a5a2a;padding:11px 14px 8px">⌂ THE DUNGEON MASTER</div>
<div style="flex:1;overflow-y:auto;padding:0 14px 14px;display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ log }}" as="l" hint-placeholder-count="4">
<div style="font:13px/1.45 Georgia,serif;color:{{ l.color }}">{{ l.text }}</div>
</sc-for>
</div>
</div>
</div>
<!-- CENTER: battlefield -->
<div style="flex:1;min-width:0;position:relative;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px">
<div style="font:600 30px 'Architects Daughter',cursive;color:{{ turnTitleColor }}">{{ turnTitle }}</div>
<div style="position:relative;width:600px;height:330px">
<!-- tiles -->
<sc-for list="{{ board.tiles }}" as="t" hint-placeholder-count="42">
<div style="position:absolute;left:{{ t.left }};top:{{ t.top }};width:88px;height:46px;transform:translate(-50%,-50%);clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);background:{{ t.bg }};box-shadow:inset 0 0 0 1.5px {{ t.inset }}"></div>
</sc-for>
<!-- tokens -->
<sc-for list="{{ board.tokens }}" as="k" hint-placeholder-count="6">
<div style="position:absolute;left:{{ k.cx }};top:{{ k.cy }};opacity:{{ k.opacity }}">
<div style="position:absolute;left:0;top:0;transform:translate(-50%,14px);width:42px;height:14px;border-radius:50%;background:rgba(0,0,0,.45);filter:blur(2px)"></div>
<sc-if value="{{ k.target }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;left:0;top:0;transform:translate(-50%,-50%);width:56px;height:56px;border-radius:50%;border:2px dashed #b8524a;box-sizing:border-box"></div>
</sc-if>
<div style="position:absolute;left:0;top:0;transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%;border:2px solid {{ k.ring }};background:{{ k.fill }};display:flex;align-items:center;justify-content:center;font:600 15px system-ui;color:#f0e6d0;animation:{{ k.anim }};box-shadow:0 2px 6px rgba(0,0,0,.5)">
{{ k.glyph }}
<sc-if value="{{ k.dead }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:700 20px system-ui;color:#a5442e;background:rgba(10,8,6,.55);border-radius:50%"></div></sc-if>
</div>
<div style="position:absolute;left:0;top:0;transform:translate(-50%,22px);width:40px;height:5px;border-radius:3px;background:#1a1512;overflow:hidden"><div style="width:{{ k.hpPct }};height:100%;background:{{ k.hpColor }}"></div></div>
</div>
</sc-for>
<!-- over stamp -->
<sc-if value="{{ isOver }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) rotate(-6deg);border:4px solid {{ overColor }};color:{{ overColor }};font:800 40px ui-monospace,monospace;letter-spacing:.14em;padding:10px 28px;border-radius:10px;background:rgba(10,8,6,.6)">{{ overText }}</div>
</sc-if>
</div>
<div style="font:11px ui-monospace,monospace;letter-spacing:.14em;color:#5a5040">ISOMETRIC BATTLEFIELD · placeholder tokens & tiles</div>
</div>
<!-- RIGHT: enemies -->
<div style="width:360px;flex:none;padding:20px;display:flex;flex-direction:column;gap:12px;min-height:0">
<div style="font:11px ui-monospace,monospace;letter-spacing:.16em;color:#a5442e">ENEMIES · click to target</div>
<div style="display:flex;flex-direction:column;gap:9px;overflow-y:auto">
<sc-for list="{{ enemies }}" as="e" hint-placeholder-count="3">
<div onClick="{{ e.onTarget }}" style="position:relative;display:flex;gap:12px;align-items:center;background:{{ e.bg }};border:1px solid {{ e.border }};border-radius:10px;padding:11px 13px;cursor:{{ e.cursor }};opacity:{{ e.opacity }}">
<div style="width:42px;height:42px;flex:none;border-radius:50%;border:2px solid #a5442e;background:#3a2018;display:flex;align-items:center;justify-content:center;font:600 13px system-ui;color:#e6b0aa">{{ e.glyph }}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:7px"><span style="font:600 15px system-ui;color:#e8d9b6">{{ e.name }}</span><sc-if value="{{ e.selected }}" hint-placeholder-val="{{ false }}"><span style="font:9px ui-monospace,monospace;letter-spacing:.08em;color:#f4e6cf;background:#8f3a34;border-radius:4px;padding:1px 6px">◎ TARGET</span></sc-if></div>
<div style="height:8px;border-radius:4px;background:#2a251f;overflow:hidden;margin-top:6px"><div style="width:{{ e.hpPct }};height:100%;background:linear-gradient(90deg,#b8524a,#8f3a34)"></div></div>
<div style="display:flex;gap:7px;margin-top:5px;font:10px ui-monospace,monospace;color:#9a8f78"><span>{{ e.hpText }}</span><span>· AC {{ e.ac }}</span><span style="color:#c99">{{ e.statusText }}</span></div>
</div>
</div>
</sc-for>
</div>
</div>
</div>
<!-- ===== BOTTOM BAR ===== -->
<div style="position:relative;display:flex;align-items:stretch;gap:18px;padding:16px 24px 20px;background:linear-gradient(0deg,rgba(10,8,6,.95),rgba(10,8,6,.5))">
<!-- active hero -->
<div style="width:300px;flex:none;display:flex;gap:14px;align-items:center;background:rgba(18,15,11,.7);border:1px solid #a9843f;border-radius:12px;padding:14px 16px">
<div style="width:60px;height:60px;flex:none;border-radius:50%;border:2px solid #c8963f;background:#3a332a;display:flex;align-items:center;justify-content:center;font:600 18px system-ui;color:#f0e6d0">VX</div>
<div style="flex:1;min-width:0">
<div style="font:600 18px 'Architects Daughter',cursive;color:#e8d9b6">Vexcca</div>
<div style="margin-top:5px"><div style="font:9px ui-monospace,monospace;color:#caa39c">HP {{ pHpText }}</div><div style="height:8px;border-radius:4px;background:#2a251f;overflow:hidden;margin-top:2px"><div style="width:{{ pHpPct }};height:100%;background:linear-gradient(90deg,#b8524a,#8f3a34)"></div></div></div>
<div style="display:flex;gap:12px;margin-top:8px">
<div style="display:flex;align-items:center;gap:5px"><span style="width:11px;height:11px;border-radius:50%;background:{{ actionColor }};border:1px solid #6b6152"></span><span style="font:9px ui-monospace,monospace;color:#9a8f78">ACTION</span></div>
<div style="display:flex;align-items:center;gap:5px"><span style="width:11px;height:11px;border-radius:50%;background:{{ bonusColor }};border:1px solid #6b6152"></span><span style="font:9px ui-monospace,monospace;color:#9a8f78">BONUS</span></div>
<div style="display:flex;align-items:center;gap:5px"><span style="font:9px ui-monospace,monospace;color:#9a8f78">MOVE 30ft</span></div>
</div>
</div>
</div>
<!-- abilities -->
<div style="flex:1;display:flex;align-items:center;justify-content:center;gap:10px">
<sc-for list="{{ abilities }}" as="ab" hint-placeholder-count="5">
<div onClick="{{ ab.onUse }}" style="width:120px;height:104px;border:1.5px solid {{ ab.border }};border-radius:11px;background:{{ ab.bg }};padding:10px;box-sizing:border-box;position:relative;cursor:{{ ab.cursor }};opacity:{{ ab.opacity }};display:flex;flex-direction:column;justify-content:space-between" style-hover="border-color:#c8963f">
<div style="display:flex;justify-content:space-between;align-items:center"><span style="font:9px ui-monospace,monospace;color:#8a7f6c;border:1px solid #4a4238;border-radius:4px;padding:2px 5px">{{ ab.costLabel }}</span><span style="font:10px ui-monospace,monospace;color:#a9843f">{{ ab.key }}</span></div>
<div style="font:600 14px system-ui;color:#e8d9b6;line-height:1.15">{{ ab.name }}</div>
<div style="font:10px ui-monospace,monospace;color:#9a8f78">{{ ab.desc }}</div>
</div>
</sc-for>
</div>
<!-- turn action -->
<div style="width:250px;flex:none;display:flex;flex-direction:column;justify-content:center;gap:8px">
<div style="font:11px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c;text-align:center">{{ turnSub }}</div>
<div onClick="{{ btnAction }}" style="text-align:center;padding:20px 0;border:1.5px solid {{ btnBorder }};border-radius:12px;background:{{ btnBg }};color:{{ btnFg }};font:600 19px system-ui;letter-spacing:.05em;cursor:{{ btnCursor }};box-shadow:0 4px 16px rgba(0,0,0,.3)">{{ btnLabel }}</div>
</div>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1920,&quot;height&quot;:1080}}">
class Component extends DCLogic {
base() {
return {
vexcca: { name: 'Vexcca', kind: 'player', init: 17, max: 60, glyph: 'VX', ac: 14, status: [] },
cut: { name: 'Redwidow Cutthroat', kind: 'enemy', init: 14, max: 26, glyph: 'CT', ac: 13, status: [] },
bramble: { name: 'Bramble', kind: 'ally', init: 12, max: 44, glyph: 'BR', ac: 16, status: [], verb: 'shield-checks' },
arch: { name: 'Redwidow Archer', kind: 'enemy', init: 11, max: 18, glyph: 'AR', ac: 12, status: [], verb: 'looses an arrow at' },
kess: { name: 'Kess', kind: 'ally', init: 9, max: 30, glyph: 'KS', ac: 12, status: [], verb: 'hexes' },
hound: { name: 'Mangy Warhound', kind: 'enemy', init: 7, max: 14, glyph: 'WH', ac: 11, status: ['Rabid'], verb: 'snaps at' },
};
}
order() { const b = this.base(); return Object.keys(b).sort((a, c) => b[c].init - b[a].init); }
abilityDefs() {
return [
{ id: 'slash', name: 'Falchion Slash', cost: 'action', key: '1', desc: '1217 slashing', dmg: [12, 17], target: true },
{ id: 'reckless', name: 'Reckless Swing', cost: 'action', key: '2', desc: '1824, guard down', dmg: [18, 24], target: true },
{ id: 'bash', name: 'Shield Bash', cost: 'bonus', key: '3', desc: '26 + stagger', dmg: [2, 6], target: true },
{ id: 'wind', name: 'Second Wind', cost: 'bonus', key: '4', desc: 'heal 514', heal: [5, 14], target: false },
{ id: 'bomb', name: 'Bile Bomb', cost: 'action', key: '5', desc: '612 acid', dmg: [6, 12], target: true },
];
}
state = (() => {
const b = this.base(); const hp = {}; Object.keys(b).forEach(k => hp[k] = b[k].max);
hp.vexcca = 42;
return { hp, turnPos: 0, round: 1, target: 'cut', used: { action: false, bonus: false }, over: null,
log: [{ text: 'Three Redwidows have you cornered in the alley. Steel is already out.', color: '#6a5a3a' }] };
})();
rng(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
alive(id) { return this.state.hp[id] > 0; }
curId() { return this.order()[this.state.turnPos]; }
pushLog(text, color) { return { text, color: color || '#4a3f2c' }; }
applyDmg(hp, id, dmg) { hp[id] = Math.max(0, hp[id] - dmg); }
checkOver(hp) {
const b = this.base();
const enemiesLeft = Object.keys(b).filter(k => b[k].kind === 'enemy' && hp[k] > 0);
if (hp.vexcca <= 0) return 'defeat';
if (enemiesLeft.length === 0) return 'victory';
return null;
}
advance(hp, log, round) {
const ord = this.order();
let pos = this.state.turnPos;
let r = round;
for (let i = 1; i <= ord.length; i++) {
const cand = (pos + i) % ord.length;
if (pos + i >= ord.length) r = r + (cand === (pos + i) % ord.length && i >= 1 ? 0 : 0);
if (hp[ord[cand]] > 0) {
if (pos + i >= ord.length) r = round + 1;
return { turnPos: cand, round: r };
}
}
return { turnPos: pos, round: r };
}
useAbility(ab) {
if (this.curId() !== 'vexcca' || this.state.over) return;
if (this.state.used[ab.cost]) { this.setState(s => ({ log: [this.pushLog('No ' + ab.cost + ' left this turn — end your turn.', '#8f3a34'), ...s.log].slice(0, 8) })); return; }
const hp = { ...this.state.hp };
const log = [...this.state.log];
const b = this.base();
if (ab.target) {
const t = this.state.target;
if (!t || hp[t] <= 0) { this.setState(s => ({ log: [this.pushLog('Pick a living target first.', '#8f3a34'), ...s.log].slice(0, 8) })); return; }
const dmg = this.rng(ab.dmg[0], ab.dmg[1]);
this.applyDmg(hp, t, dmg);
let line = `Vexcca's ${ab.name.toLowerCase()} lands on the ${b[t].name.toLowerCase()}${dmg} damage.`;
if (hp[t] <= 0) line += ` It drops.`;
log.unshift(this.pushLog(line, '#4a3f2c'));
} else if (ab.heal) {
const h = this.rng(ab.heal[0], ab.heal[1]);
hp.vexcca = Math.min(b.vexcca.max, hp.vexcca + h);
log.unshift(this.pushLog(`Vexcca grits through it and claws back ${h} HP.`, '#3a6a3a'));
}
const used = { ...this.state.used, [ab.cost]: true };
const over = this.checkOver(hp);
if (over === 'victory') log.unshift(this.pushLog('The alley goes quiet. They are all down.', '#3a6a3a'));
this.setState({ hp, used, log: log.slice(0, 8), over });
}
selectTarget(id) {
if (this.base()[id].kind !== 'enemy' || !this.alive(id)) return;
this.setState({ target: id });
}
endTurn() {
if (this.curId() !== 'vexcca' || this.state.over) return;
const nx = this.advance({ ...this.state.hp }, [], this.state.round);
const landing = this.order()[nx.turnPos];
const used = landing === 'vexcca' ? { action: false, bonus: false } : this.state.used;
this.setState(s => ({ ...nx, used, log: [this.pushLog('You give ground and end your turn.', '#6a5a3a'), ...s.log].slice(0, 8) }));
}
resolveAI() {
const id = this.curId();
const b = this.base();
const u = b[id];
if (u.kind === 'player' || this.state.over) return;
const hp = { ...this.state.hp };
const log = [...this.state.log];
let pool, dmg, targetName;
if (u.kind === 'enemy') {
pool = Object.keys(b).filter(k => (b[k].kind === 'ally' || b[k].kind === 'player') && hp[k] > 0);
dmg = this.rng(4, 9);
} else {
pool = Object.keys(b).filter(k => b[k].kind === 'enemy' && hp[k] > 0);
dmg = this.rng(5, 10);
}
if (pool.length) {
const tid = pool[Math.floor(Math.random() * pool.length)];
this.applyDmg(hp, tid, dmg);
let line = `${u.name} ${u.verb || 'strikes'} ${b[tid].name}${dmg} damage.`;
if (hp[tid] <= 0) line += ` ${b[tid].name} goes down.`;
log.unshift(this.pushLog(line, u.kind === 'enemy' ? '#7a3a34' : '#3a5a6a'));
}
const over = this.checkOver(hp);
if (over === 'victory') log.unshift(this.pushLog('The last of them falls. You are still standing.', '#3a6a3a'));
if (over === 'defeat') log.unshift(this.pushLog('You go down in the mud. The rain keeps falling.', '#8f3a34'));
const nx = over ? {} : this.advance(hp, [], this.state.round);
const landing = over ? null : this.order()[nx.turnPos];
const used = landing === 'vexcca' ? { action: false, bonus: false } : this.state.used;
this.setState({ hp, log: log.slice(0, 8), over, used, ...nx });
}
restart() {
const b = this.base(); const hp = {}; Object.keys(b).forEach(k => hp[k] = b[k].max); hp.vexcca = 42;
this.setState({ hp, turnPos: 0, round: 1, target: 'cut', used: { action: false, bonus: false }, over: null,
log: [this.pushLog('Reset. Three Redwidows, one alley, again.', '#6a5a3a')] });
}
POS() { return { vexcca: [2, 4], bramble: [1, 3], kess: [0, 4], cut: [4, 2], arch: [5, 1], hound: [3, 3] }; }
buildBoard(hp, curId, isPlayerTurn) {
const b = this.base();
const pos = this.POS();
const COLS = 7, ROWS = 6, tw = 88, th = 46, ox = 290, oy = 42;
const proj = (c, r) => ({ x: ox + (c - r) * (tw / 2), y: oy + (c + r) * (th / 2) });
const over = this.state.over;
const target = this.state.target;
const curPos = pos[curId];
const targetPos = target ? pos[target] : null;
const occ = {}; Object.keys(pos).forEach(id => { if (hp[id] > 0) occ[pos[id][0] + ',' + pos[id][1]] = id; });
const tiles = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const p = proj(c, r);
let bg = (c + r) % 2 ? '#332d24' : '#2b2620';
let inset = 'rgba(0,0,0,.45)';
if (isPlayerTurn && curPos && !occ[c + ',' + r]) {
const d = Math.abs(c - curPos[0]) + Math.abs(r - curPos[1]);
if (d > 0 && d <= 2) bg = '#2f4038';
}
if (curPos && !over && c === curPos[0] && r === curPos[1]) { inset = '#c8963f'; bg = '#463a20'; }
if (targetPos && hp[target] > 0 && c === targetPos[0] && r === targetPos[1]) inset = '#8f3a34';
tiles.push({ left: p.x + 'px', top: p.y + 'px', bg, inset });
}
const kindFill = { player: '#7a5a1f', ally: '#243138', enemy: '#3a2018' };
const kindRing = { player: '#c8963f', ally: '#5f8a9a', enemy: '#a5442e' };
const tokens = this.order().map(id => {
const [c, r] = pos[id]; const p = proj(c, r); const dead = hp[id] <= 0;
const isCur = id === curId && !over;
return {
cx: p.x + 'px', cy: (p.y - 12) + 'px',
glyph: b[id].glyph, fill: kindFill[b[id].kind], ring: isCur ? '#f0e6d0' : kindRing[b[id].kind],
dead, opacity: dead ? '0.4' : '1',
target: id === target && b[id].kind === 'enemy' && !dead,
hpPct: Math.round((hp[id] / b[id].max) * 100) + '%',
hpColor: b[id].kind === 'enemy' ? '#a5442e' : (b[id].kind === 'ally' ? '#5f8a9a' : '#c8963f'),
anim: isCur ? 'cturn 1.8s ease-out infinite' : 'none',
};
});
return { tiles, tokens };
}
renderVals() {
const b = this.base();
const ord = this.order();
const hp = this.state.hp;
const curId = this.curId();
const cur = b[curId];
const isPlayerTurn = curId === 'vexcca' && !this.state.over;
const over = this.state.over;
const kindFill = { player: '#7a5a1f', ally: '#243138', enemy: '#3a2018' };
const kindRing = { player: '#c8963f', ally: '#5f8a9a', enemy: '#a5442e' };
const ribbon = ord.map((id, i) => {
const u = b[id]; const dead = hp[id] <= 0; const isCur = i === this.state.turnPos && !over;
return {
glyph: u.glyph, name: u.name,
fill: kindFill[u.kind], ring: isCur ? '#f0e6d0' : kindRing[u.kind],
hpPct: Math.round((hp[id] / u.max) * 100) + '%',
hpColor: u.kind === 'enemy' ? '#a5442e' : (u.kind === 'ally' ? '#5f8a9a' : '#c8963f'),
opacity: dead ? '0.4' : '1', dead,
lift: isCur ? 'translateY(-6px)' : 'translateY(0)',
anim: isCur ? 'cturn 1.8s ease-out infinite' : 'none',
nowLabel: isCur ? '▼ NOW' : '',
};
});
const allies = ord.filter(id => b[id].kind === 'ally').map(id => ({
glyph: b[id].glyph, name: b[id].name,
hpPct: Math.round((hp[id] / b[id].max) * 100) + '%',
hpText: hp[id] + '/' + b[id].max,
border: (id === curId && !over) ? '#c8963f' : '#3a352e',
}));
const enemies = ord.filter(id => b[id].kind === 'enemy').map(id => {
const dead = hp[id] <= 0; const sel = id === this.state.target;
return {
glyph: b[id].glyph, name: b[id].name,
hpPct: Math.round((hp[id] / b[id].max) * 100) + '%',
hpText: hp[id] + '/' + b[id].max, ac: b[id].ac,
statusText: b[id].status.join(' · '),
selected: sel && !dead,
bg: sel && !dead ? 'rgba(143,58,52,.18)' : 'rgba(18,15,11,.7)',
border: (id === curId && !over) ? '#a5442e' : (sel && !dead ? '#8f3a34' : '#3a352e'),
cursor: dead ? 'default' : 'pointer', opacity: dead ? '0.4' : '1',
onTarget: () => this.selectTarget(id),
};
});
const abilities = this.abilityDefs().map(ab => {
const usable = isPlayerTurn && !this.state.used[ab.cost];
return {
key: ab.key, name: ab.name, desc: ab.desc,
costLabel: ab.cost === 'action' ? 'ACTION' : 'BONUS',
onUse: () => this.useAbility(ab),
cursor: usable ? 'pointer' : 'default',
opacity: usable ? '1' : '0.42',
bg: 'rgba(40,34,28,.7)', border: usable ? '#6b6152' : '#3a352e',
};
});
let btnLabel, btnAction, btnEnabled, turnSub, turnTitle, turnTitleColor;
if (over) {
btnLabel = 'FIGHT AGAIN'; btnAction = () => this.restart(); btnEnabled = true; turnSub = 'THE DUST SETTLES';
turnTitle = over === 'victory' ? 'VICTORY' : 'DEFEAT'; turnTitleColor = over === 'victory' ? '#5f8a4f' : '#a5442e';
} else if (isPlayerTurn) {
btnLabel = 'END TURN ▸'; btnAction = () => this.endTurn(); btnEnabled = true; turnSub = 'YOUR TURN — ACT, THEN END';
turnTitle = "Vexcca's Turn"; turnTitleColor = '#c8963f';
} else {
btnLabel = 'RESOLVE ' + cur.name.toUpperCase() + ' ▸'; btnAction = () => this.resolveAI(); btnEnabled = true;
turnSub = 'THE DM RESOLVES THIS TURN';
turnTitle = cur.name + "'s Turn"; turnTitleColor = cur.kind === 'enemy' ? '#a5442e' : '#5f8a9a';
}
const board = this.buildBoard(hp, curId, isPlayerTurn);
return {
round: this.state.round, board,
ribbon, allies, enemies, abilities,
log: this.state.log,
pHpText: hp.vexcca + '/' + b.vexcca.max,
pHpPct: Math.round((hp.vexcca / b.vexcca.max) * 100) + '%',
actionColor: (isPlayerTurn && !this.state.used.action) ? '#c8963f' : '#3a352e',
bonusColor: (isPlayerTurn && !this.state.used.bonus) ? '#c8963f' : '#3a352e',
turnTitle, turnTitleColor,
isOver: !!over, overText: over === 'victory' ? 'VICTORY' : 'DEFEAT',
overColor: over === 'victory' ? '#5f8a4f' : '#a5442e',
turnSub,
btnLabel, btnAction,
btnBg: over ? 'linear-gradient(180deg,#3a2f1f,#2a2015)' : (isPlayerTurn ? 'linear-gradient(180deg,#a5423a,#7a2f29)' : 'linear-gradient(180deg,#2f3a42,#1f2a30)'),
btnFg: '#f4e6cf', btnBorder: isPlayerTurn && !over ? '#8f3a34' : '#5f7a8a', btnCursor: 'pointer',
};
}
}
</script>
</body>
</html>