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>
435 lines
26 KiB
HTML
435 lines
26 KiB
HTML
<!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="{"$preview":{"width":1920,"height":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: '12–17 slashing', dmg: [12, 17], target: true },
|
||
{ id: 'reckless', name: 'Reckless Swing', cost: 'action', key: '2', desc: '18–24, guard down', dmg: [18, 24], target: true },
|
||
{ id: 'bash', name: 'Shield Bash', cost: 'bonus', key: '3', desc: '2–6 + stagger', dmg: [2, 6], target: true },
|
||
{ id: 'wind', name: 'Second Wind', cost: 'bonus', key: '4', desc: 'heal 5–14', heal: [5, 14], target: false },
|
||
{ id: 'bomb', name: 'Bile Bomb', cost: 'action', key: '5', desc: '6–12 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>
|