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>
This commit is contained in:
2026-07-10 16:06:23 -05:00
parent b639cab0c2
commit 49bc6b22c0
13 changed files with 4671 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ============ LEFT: PORTRAIT BAY ============ -->
<div style="width:660px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);display:flex;flex-direction:column">
<div style="position:absolute;inset:0;background:radial-gradient(120% 70% at 50% 38%,rgba(0,0,0,0) 35%,rgba(0,0,0,.6) 100%)"></div>
<div style="position:relative;padding:30px 34px 0;font:13px ui-monospace,monospace;letter-spacing:.18em;color:#8a7f6c">WHO WILL YOU BE?</div>
<!-- model -->
<div style="position:relative;flex:1;display:flex;align-items:center;justify-content:center;gap:24px">
<div style="color:#a9843f;font:300 40px ui-monospace;cursor:pointer;opacity:.6"></div>
<div style="width:300px;height:540px;border:1px dashed #5a5348;border-radius:16px;background:repeating-linear-gradient(135deg,#2c2620 0 14px,#282219 14px 28px);display:flex;align-items:center;justify-content:center;text-align:center;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.12em;padding:0 20px">CHARACTER<br>MODEL</div>
<div style="color:#a9843f;font:300 40px ui-monospace;cursor:pointer;opacity:.6"></div>
</div>
<!-- nameplate -->
<div style="position:relative;text-align:center;padding:0 34px">
<div style="font:600 34px 'Architects Daughter',cursive;color:#e8d9b6;letter-spacing:.02em">{{ displayName }}</div>
<div style="font:14px ui-monospace,monospace;letter-spacing:.14em;color:#a9843f;margin-top:4px">{{ raceName }} · {{ className }}</div>
</div>
<!-- DM origin -->
<div style="position:relative;margin:26px 34px 34px;background:rgba(231,220,196,.96);border:1px solid #1a1512;border-radius:10px;padding:16px 18px">
<div style="font:600 12px 'Architects Daughter',cursive;letter-spacing:.08em;color:#8a5a2a;margin-bottom:7px">THE DM SIZES YOU UP</div>
<div style="font:16px/1.55 Georgia,serif;font-style:italic;color:#4a3f2c">{{ originText }}</div>
</div>
</div>
<!-- ============ RIGHT: CHARACTER SHEET ============ -->
<div style="width:1260px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:34px 44px;display:flex;flex-direction:column;gap:22px">
<div style="border-bottom:2px solid #bfa878;padding-bottom:16px">
<div style="font:600 30px 'Architects Daughter',cursive;letter-spacing:.04em;color:#5a4326">Create Your Character</div>
<div style="font:13px ui-monospace,monospace;color:#8a7748;letter-spacing:.08em;margin-top:2px">THE WORLD HAS REAL PROBLEMS AND DOES NOT CARE ABOUT YOU</div>
</div>
<!-- RACE -->
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">RACE</div>
<div style="display:flex;gap:14px">
<sc-for list="{{ races }}" as="race" hint-placeholder-count="4">
<div onClick="{{ race.onSelect }}" style="flex:1;min-width:0;position:relative;background:#f1e8d2;border:1px solid #bfa878;border-radius:12px;padding:15px 16px;cursor:pointer;display:flex;flex-direction:column;gap:6px" style-hover="background:#e9dcbd;border-color:#8f7a4a">
<div style="font:600 19px system-ui;color:#3a2f1c">{{ race.name }}</div>
<div style="font:13px/1.4 Georgia,serif;font-style:italic;color:#6a5a3a;flex:1">{{ race.desc }}</div>
<div style="font:11px ui-monospace,monospace;color:#8f6a2a;letter-spacing:.04em">{{ race.trait }}</div>
<sc-if value="{{ race.sel }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-2px;border:3px solid #8f3a34;border-radius:13px;pointer-events:none"></div>
<div style="position:absolute;top:-11px;right:12px;background:#8f3a34;color:#f1e8d2;font:600 10px ui-monospace;letter-spacing:.1em;padding:3px 8px;border-radius:5px">CHOSEN</div>
</sc-if>
</div>
</sc-for>
</div>
</div>
<!-- CALLING -->
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">CALLING</div>
<div style="display:flex;gap:12px">
<sc-for list="{{ classes }}" as="cls" hint-placeholder-count="5">
<div onClick="{{ cls.onSelect }}" style="flex:1;min-width:0;position:relative;background:#f1e8d2;border:1px solid #bfa878;border-radius:12px;padding:14px 15px;cursor:pointer;display:flex;flex-direction:column;gap:6px" style-hover="background:#e9dcbd;border-color:#8f7a4a">
<div style="display:flex;align-items:baseline;justify-content:space-between;gap:6px"><span style="font:600 17px system-ui;color:#3a2f1c">{{ cls.name }}</span><span style="font:10px ui-monospace,monospace;color:#8f6a2a;border:1px solid #cbb684;border-radius:4px;padding:1px 5px">{{ cls.trait }}</span></div>
<div style="font:12.5px/1.4 Georgia,serif;font-style:italic;color:#6a5a3a;flex:1">{{ cls.desc }}</div>
<sc-if value="{{ cls.sel }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-2px;border:3px solid #8f3a34;border-radius:13px;pointer-events:none"></div>
<div style="position:absolute;top:-11px;right:10px;background:#8f3a34;color:#f1e8d2;font:600 10px ui-monospace;letter-spacing:.1em;padding:3px 8px;border-radius:5px">CHOSEN</div>
</sc-if>
</div>
</sc-for>
</div>
</div>
<!-- ABILITIES -->
<div>
<div style="display:flex;align-items:baseline;gap:14px;margin-bottom:11px;flex-wrap:wrap">
<span style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748">ABILITY SCORES</span>
<span style="font:12px ui-monospace,monospace;color:{{ remainingColor }}">points left: {{ remaining }}</span>
<span onClick="{{ reroll }}" style="font:11px ui-monospace,monospace;letter-spacing:.06em;color:#5a4326;border:1px solid #a99464;border-radius:6px;padding:4px 10px;background:#e6d9b8;cursor:pointer" style-hover="background:#d9c9a2">⟳ re-roll</span>
<span style="font:12px 'Architects Daughter',cursive;color:#a99464">rolled 3d6 — that roll is your floor, spend up only</span>
</div>
<div style="display:flex;gap:12px">
<sc-for list="{{ abil }}" as="a" hint-placeholder-count="5">
<div style="flex:1;position:relative;background:#f1e8d2;border:1px solid #bfa878;border-radius:12px;padding:16px 10px 14px;display:flex;flex-direction:column;align-items:center;gap:7px">
<sc-if value="{{ a.isPrimary }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-2px;border:2.5px solid #8f6a2a;border-radius:13px;pointer-events:none"></div>
<div style="position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:#8f6a2a;color:#f6efdd;font:600 9px ui-monospace;letter-spacing:.14em;padding:2px 9px;border-radius:5px;white-space:nowrap">PRIMARY</div>
</sc-if>
<div style="font:12px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">{{ a.key }}</div>
<div style="font:600 40px 'Architects Daughter',cursive;color:#3a2f1c;line-height:1">{{ a.val }}</div>
<div style="font:10px ui-monospace,monospace;color:#a9885a">rolled {{ a.base }} <span style="color:#2f7a4a;font-weight:600">{{ a.plusLabel }}</span></div>
<div onClick="{{ a.onInc }}" style="width:44px;height:32px;border:1px solid #a99464;border-radius:8px;background:#e6d9b8;color:#5a4326;font:600 20px system-ui;display:flex;align-items:center;justify-content:center;cursor:pointer;margin-top:2px" style-hover="background:#d9c9a2">+</div>
</div>
</sc-for>
</div>
</div>
<!-- FOOTER: name + begin -->
<div style="margin-top:auto;display:flex;align-items:flex-end;gap:20px">
<div style="flex:1">
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:9px">NAME</div>
<input type="text" onChange="{{ setName }}" placeholder="What do they call you?" style="width:100%;box-sizing:border-box;background:#f6efdd;border:1px solid #a99464;border-radius:10px;padding:15px 18px;font:20px Georgia,serif;color:#3a2f1c;outline:none" />
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div style="padding:17px 44px;border:1.5px solid #8f3a34;border-radius:10px;background:linear-gradient(180deg,#a5423a,#7a2f29);color:#f4e6cf;font:600 20px system-ui;letter-spacing:.06em;cursor:pointer;box-shadow:0 4px 16px rgba(143,58,52,.35)" style-hover="background:linear-gradient(180deg,#b8524a,#8f3a34)">ENTER THE WORLD</div>
<span style="font:11px 'Architects Daughter',cursive;color:#a99464">no second chances out there</span>
</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 {
VIS = ['STR', 'DEX', 'CON', 'FTH', 'MAG'];
POOL = 3; // spend-up points — TUNABLE
rollDie() { return 1 + Math.floor(Math.random() * 6); } // TUNABLE die
roll3d6() { return this.rollDie() + this.rollDie() + this.rollDie(); } // TUNABLE formula
freshRoll() { const r = {}; ['STR', 'DEX', 'CON', 'FTH', 'MAG', 'LCK'].forEach(k => r[k] = this.roll3d6()); return r; }
state = {
race: 'human',
cls: 'sellsword',
rolled: this.freshRoll(), // LCK rolled here too — silently, never displayed
spent: { STR: 0, DEX: 0, CON: 0, FTH: 0, MAG: 0 },
name: '',
};
raceData() {
return [
{ id: 'human', name: 'Human', trait: '+1 to every save', desc: "Adaptable, short-lived. Everyone's second choice.", frag: "Born in the Lower Ward to parents who didn't stay." },
{ id: 'elf', name: 'Elf', trait: 'Low-light sight · keen senses', desc: 'Long memory, longer grudges. The forests are ash now.', frag: 'You outlived your village; the ash took the rest.' },
{ id: 'dwarf', name: 'Dwarf', trait: 'Poison resist · darkvision', desc: 'Stubborn as the stone they bury their dead in.', frag: 'The deep holds bred you, then buried your kin.' },
{ id: 'beastfolk', name: 'Beastfolk', trait: 'Natural claws · keen scent', desc: 'Feared in the cities, taxed twice at every gate.', frag: 'They muzzled your kind at the gates; you bit anyway.' },
];
}
classData() {
return [
{ id: 'sellsword', name: 'Sellsword', trait: 'STR', primary: 'STR', desc: 'Blade for hire, conscience negotiable.', frag: 'your blade for hire and your conscience up for auction.' },
{ id: 'cutpurse', name: 'Cutpurse', trait: 'DEX', primary: 'DEX', desc: 'Quick fingers, quicker exits.', frag: 'quick fingers, quicker exits, and a knife for the rest.' },
{ id: 'hedgemage', name: 'Hedge-Mage', trait: 'MAG', primary: 'MAG', desc: 'Self-taught hexes that leave you retching.', frag: 'self-taught hexes that leave you retching in the dirt.' },
{ id: 'bonesetter', name: 'Bonesetter', trait: 'FTH', primary: 'FTH', desc: 'You set bones and beg gods for coin.', frag: "a healer of sorts — the smell of rot never washes out." },
{ id: 'trapper', name: 'Trapper', trait: 'DEX', primary: 'DEX', desc: 'Snares, patience, a nose for bad meat.', frag: 'all snares and patience and a nose for infected meat.' },
];
}
spentTotal() { return this.VIS.reduce((s, k) => s + (this.state.spent[k] || 0), 0); }
renderVals() {
const races = this.raceData().map(r => ({ ...r, sel: r.id === this.state.race, onSelect: () => this.setState({ race: r.id }) }));
const classes = this.classData().map(c => ({ ...c, sel: c.id === this.state.cls, onSelect: () => this.setState({ cls: c.id }) }));
const race = races.find(r => r.sel);
const cls = classes.find(c => c.sel);
const remaining = this.POOL - this.spentTotal();
const primary = cls.primary;
const abil = this.VIS.map(k => {
const base = this.state.rolled[k];
const plus = this.state.spent[k] || 0;
return {
key: k, base, plus, val: base + plus,
isPrimary: k === primary,
plusLabel: plus > 0 ? ('+' + plus) : '',
onInc: () => this.setState(s => { if ((this.POOL - this.spentTotal()) <= 0) return {}; return { spent: { ...s.spent, [k]: (s.spent[k] || 0) + 1 } }; }),
};
});
const nm = (this.state.name || '').trim();
return {
races, classes,
raceName: race.name, className: cls.name,
displayName: nm || 'Nameless',
originText: `${race.frag} Now you carry a ${cls.name.toLowerCase()}'s work — ${cls.frag}`,
abil, remaining,
remainingColor: remaining > 0 ? '#8f6a2a' : '#5a4326',
reroll: () => this.setState({ rolled: this.freshRoll(), spent: { STR: 0, DEX: 0, CON: 0, FTH: 0, MAG: 0 } }),
setName: (e) => this.setState({ name: e.target.value }),
};
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,221 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ============ LEFT: PORTRAIT / VITALS ============ -->
<div style="width:560px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);display:flex;flex-direction:column">
<div style="position:absolute;inset:0;background:radial-gradient(120% 70% at 50% 38%,rgba(0,0,0,0) 32%,rgba(0,0,0,.62) 100%)"></div>
<div style="position:relative;padding:28px 34px 0;font:13px ui-monospace,monospace;letter-spacing:.18em;color:#8a7f6c">CHARACTER</div>
<!-- model -->
<div style="position:relative;flex:1;display:flex;align-items:center;justify-content:center">
<div style="width:230px;height:460px;border:1px dashed #5a5348;border-radius:16px;background:repeating-linear-gradient(135deg,#2c2620 0 14px,#282219 14px 28px);display:flex;align-items:center;justify-content:center;text-align:center;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.12em">PORTRAIT</div>
</div>
<!-- nameplate + xp + vitals + conditions -->
<div style="position:relative;padding:0 34px 34px;display:flex;flex-direction:column;gap:16px">
<div>
<div style="font:600 36px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1">Vexcca the Nameless</div>
<div style="font:14px ui-monospace,monospace;letter-spacing:.12em;color:#a9843f;margin-top:4px">HUMAN · SELLSWORD · LEVEL 4</div>
</div>
<!-- xp -->
<div>
<div style="display:flex;justify-content:space-between;font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c"><span>EXPERIENCE</span><span>3,450 / 5,000</span></div>
<div style="height:9px;border-radius:5px;background:#2a251f;border:1px solid #4a4238;overflow:hidden;margin-top:4px"><div style="width:69%;height:100%;background:#a9843f"></div></div>
</div>
<!-- vitals -->
<div style="display:flex;gap:10px">
<div style="flex:1"><div style="font:10px ui-monospace,monospace;color:#caa39c;margin-bottom:3px">HP 42 / 60</div><div style="height:11px;border-radius:6px;background:#2a251f;border:1px solid #4a4238;overflow:hidden"><div style="width:70%;height:100%;background:linear-gradient(90deg,#b8524a,#8f3a34)"></div></div></div>
<div style="flex:1"><div style="font:10px ui-monospace,monospace;color:#9caccb;margin-bottom:3px">MP 18 / 30</div><div style="height:11px;border-radius:6px;background:#2a251f;border:1px solid #4a4238;overflow:hidden"><div style="width:60%;height:100%;background:linear-gradient(90deg,#4f7ab8,#243f6f)"></div></div></div>
</div>
<!-- condition badges -->
<div style="display:flex;flex-wrap:wrap;gap:8px">
<span style="font:11px ui-monospace,monospace;letter-spacing:.04em;color:#e6b0aa;background:rgba(143,58,52,.24);border:1px solid #8f3a34;border-radius:6px;padding:5px 10px">⚠ Infected bite — left arm</span>
<span style="font:11px ui-monospace,monospace;letter-spacing:.04em;color:#e6c9a0;background:rgba(165,119,42,.22);border:1px solid #a5772a;border-radius:6px;padding:5px 10px">Hungover</span>
</div>
</div>
</div>
<!-- ============ RIGHT: SHEET ============ -->
<div style="width:1360px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:30px 40px;display:flex;flex-direction:column;gap:20px">
<!-- header -->
<div style="display:flex;align-items:flex-end;justify-content:space-between;border-bottom:2px solid #bfa878;padding-bottom:14px">
<div>
<div style="font:600 30px 'Architects Daughter',cursive;letter-spacing:.04em;color:#5a4326">Character Sheet</div>
<div style="font:12px ui-monospace,monospace;color:#8a7748;letter-spacing:.08em;margin-top:2px">WHAT YOU ARE, WRITTEN DOWN — AND WHAT'S WRONG WITH YOU</div>
</div>
<div style="display:flex;gap:20px">
<div style="text-align:center"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">PROF</div><div style="font:600 22px 'Architects Daughter',cursive;color:#5a4326">+2</div></div>
<div style="text-align:center"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">HIT DIE</div><div style="font:600 22px 'Architects Daughter',cursive;color:#5a4326">d10</div></div>
<div style="font:11px ui-monospace,monospace;color:#a99464;border:1px solid #cbb684;border-radius:6px;padding:6px 10px;cursor:pointer;align-self:center" style-hover="background:#e6d9b8">ESC ✕</div>
</div>
</div>
<!-- body: 3 columns -->
<div style="display:flex;gap:26px;flex:1;min-height:0">
<!-- COL A: attributes + saves -->
<div style="width:300px;flex:none;display:flex;flex-direction:column;gap:20px">
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">ATTRIBUTES</div>
<div style="display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ stats }}" as="a" hint-placeholder-count="5">
<div style="display:flex;align-items:center;gap:14px;background:#f1e8d2;border:1px solid #bfa878;border-radius:10px;padding:10px 16px">
<div style="font:12px ui-monospace,monospace;letter-spacing:.14em;color:#8a7748;width:38px">{{ a.key }}</div>
<div style="font:600 30px 'Architects Daughter',cursive;color:#3a2f1c;width:56px">{{ a.mod }}</div>
<div style="margin-left:auto;font:13px ui-monospace,monospace;color:#a9885a">score {{ a.val }}</div>
</div>
</sc-for>
</div>
</div>
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">SAVING THROWS</div>
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:10px;padding:6px 16px">
<sc-for list="{{ saves }}" as="s" hint-placeholder-count="5">
<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid #e0d3ac">
<span style="font:15px ui-monospace,monospace;color:{{ s.dotColor }}">{{ s.dot }}</span>
<span style="font:14px system-ui;color:#3a2f1c">{{ s.key }}</span>
<span style="margin-left:auto;font:600 15px ui-monospace,monospace;color:#5a4326">{{ s.val }}</span>
</div>
</sc-for>
</div>
</div>
</div>
<!-- COL B: combat + skills -->
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:20px">
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">IN A FIGHT</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px">
<sc-for list="{{ combat }}" as="c" hint-placeholder-count="8">
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:10px;padding:12px 8px;text-align:center">
<div style="font:9px ui-monospace,monospace;letter-spacing:.08em;color:#8a7748">{{ c.label }}</div>
<div style="font:600 26px 'Architects Daughter',cursive;color:#3a2f1c;margin-top:3px">{{ c.val }}</div>
</div>
</sc-for>
</div>
</div>
<div style="flex:1;min-height:0;display:flex;flex-direction:column">
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">SKILLS</div>
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:10px;padding:4px 16px;column-count:2;column-gap:26px">
<sc-for list="{{ skills }}" as="k" hint-placeholder-count="9">
<div style="display:flex;align-items:center;gap:9px;padding:7px 0;break-inside:avoid">
<span style="font:15px ui-monospace,monospace;color:{{ k.dotColor }}">{{ k.dot }}</span>
<span style="font:14px system-ui;color:#3a2f1c">{{ k.name }}</span>
<span style="font:10px ui-monospace,monospace;color:#a9885a">{{ k.abil }}</span>
<span style="margin-left:auto;font:600 14px ui-monospace,monospace;color:#5a4326">{{ k.val }}</span>
</div>
</sc-for>
</div>
</div>
</div>
<!-- COL C: talents + afflictions + bio -->
<div style="width:400px;flex:none;display:flex;flex-direction:column;gap:20px;min-height:0">
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">TALENTS</div>
<div style="display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ talents }}" as="t" hint-placeholder-count="3">
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:10px;padding:11px 15px">
<div style="font:600 15px system-ui;color:#3a2f1c">{{ t.name }}</div>
<div style="font:13px/1.45 Georgia,serif;color:#6a5a3a;margin-top:3px">{{ t.desc }}</div>
</div>
</sc-for>
</div>
</div>
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8f3a34;margin-bottom:11px">AFFLICTIONS</div>
<div style="display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ afflictions }}" as="x" hint-placeholder-count="3">
<div style="background:#f1e8d2;border:1px solid {{ x.color }};border-left:4px solid {{ x.color }};border-radius:10px;padding:11px 15px">
<div style="font:600 15px system-ui;color:{{ x.color }}">{{ x.name }}</div>
<div style="font:13px/1.45 Georgia,serif;color:#6a5a3a;margin-top:3px">{{ x.desc }}</div>
</div>
</sc-for>
</div>
</div>
<div style="flex:1;min-height:0">
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:11px">THE DM'S NOTES</div>
<div style="background:rgba(231,220,196,.6);border:1px dashed #a99464;border-radius:10px;padding:14px 16px;font:15px/1.55 Georgia,serif;font-style:italic;color:#5a4a2c">They call you Nameless because you burned the papers that said otherwise. Four contracts, three scars, and one debt that keeps finding you no matter which city you bleed in.</div>
</div>
</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 {
MODS = { STR: 2, DEX: 1, CON: 1, FTH: -1, MAG: -1 }; // LCK exists but is never shown
PROF = 2;
sign(n) { return (n >= 0 ? '+' : '') + n; }
renderVals() {
const scores = { STR: 15, DEX: 13, CON: 12, FTH: 9, MAG: 8 };
const stats = Object.keys(this.MODS).map(k => ({ key: k, val: scores[k], mod: this.sign(this.MODS[k]) }));
const saveProf = { STR: true, CON: true };
const saves = Object.keys(this.MODS).map(k => {
const v = this.MODS[k] + (saveProf[k] ? this.PROF : 0);
return { key: k, val: this.sign(v), dot: saveProf[k] ? '●' : '○', dotColor: saveProf[k] ? '#8f6a2a' : '#c9b78a' };
});
const skillDefs = [
{ name: 'Athletics', abil: 'STR', prof: true },
{ name: 'Intimidation', abil: 'STR', prof: true },
{ name: 'Stealth', abil: 'DEX', prof: true },
{ name: 'Sleight of Hand', abil: 'DEX', prof: false },
{ name: 'Acrobatics', abil: 'DEX', prof: false },
{ name: 'Endurance', abil: 'CON', prof: true },
{ name: 'Perception', abil: 'FTH', prof: false },
{ name: 'Faith-Lore', abil: 'FTH', prof: false },
{ name: 'Sorcery', abil: 'MAG', prof: false },
];
const skills = skillDefs.map(s => {
const v = this.MODS[s.abil] + (s.prof ? this.PROF : 0);
return { name: s.name, abil: s.abil, val: this.sign(v), dot: s.prof ? '●' : '○', dotColor: s.prof ? '#8f6a2a' : '#c9b78a' };
});
const combat = [
{ label: 'ARMOR', val: '14' },
{ label: 'INITIATIVE', val: '+1' },
{ label: 'SPEED', val: "30'" },
{ label: 'CARRY', val: '60' },
{ label: 'MELEE', val: '+4' },
{ label: 'RANGED', val: '+3' },
{ label: 'SPELL DC', val: '11' },
{ label: 'DEATH SVS', val: '0/0' },
];
const talents = [
{ name: 'Second Wind', desc: 'Once per rest, bite down and claw back 1d10+4 HP.' },
{ name: 'Reckless Swing', desc: 'Swing with advantage — but everything hits you easier until your next turn.' },
{ name: "Mercenary's Nose", desc: 'You can smell a contract going bad before the coin changes hands.' },
];
const afflictions = [
{ name: 'Infected Bite — left arm', desc: 'CON save each dawn or your max HP drops by 1 until it heals or takes you.', color: '#8f3a34' },
{ name: 'Hungover', desc: '1 to attack rolls and CON checks until midday. You did this to yourself.', color: '#a5772a' },
{ name: 'Old Knee', desc: 'Speed 5 in cold or wet weather. It always knows before you do.', color: '#a5772a' },
];
return { stats, saves, skills, combat, talents, afflictions };
}
}
</script>
</body>
</html>

434
mockups/Combat HUD.dc.html Normal file
View File

@@ -0,0 +1,434 @@
<!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>

208
mockups/Dialogue.dc.html Normal file
View File

@@ -0,0 +1,208 @@
<!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}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#201a14 0 18px,#1a150f 18px 36px)">
<!-- dimmed scene -->
<div style="position:absolute;inset:0;background:radial-gradient(110% 90% at 50% 40%,rgba(0,0,0,.35) 40%,rgba(0,0,0,.82) 100%)"></div>
<div style="position:absolute;top:36px;left:50%;transform:translateX(-50%);color:#4a3f30;font:12px ui-monospace,monospace;letter-spacing:.18em;opacity:.6">SCENE · GALLOWSMOOR · THE BACK ALLEY, RAINING</div>
<div style="position:absolute;inset:0;display:flex">
<!-- ===== LEFT: NPC ===== -->
<div style="width:640px;flex:none;position:relative;display:flex;flex-direction:column;justify-content:flex-end;padding:56px 52px">
<!-- portrait -->
<div style="position:absolute;left:52px;top:96px;right:52px;bottom:300px;border:1px dashed #4a4238;border-radius:16px;background:repeating-linear-gradient(135deg,#2c2620 0 16px,#282219 16px 32px);display:flex;align-items:center;justify-content:center;color:#5a5040;font:12px ui-monospace,monospace;letter-spacing:.14em">NPC PORTRAIT</div>
<!-- info -->
<div style="position:relative">
<div style="font:600 46px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1">Grud Ninefingers</div>
<div style="font:12px ui-monospace,monospace;letter-spacing:.14em;color:#a9843f;margin-top:6px">BEASTFOLK · FENCE · KNOWS EVERYONE</div>
<!-- disposition -->
<div style="margin-top:22px;max-width:360px">
<div style="display:flex;justify-content:space-between;font:11px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c"><span>DISPOSITION</span><span style="color:{{ dispColor }}">{{ dispLabel }}</span></div>
<div style="height:10px;border-radius:6px;background:#2a251f;border:1px solid #4a4238;overflow:hidden;margin-top:5px"><div style="width:{{ dispPct }};height:100%;background:{{ dispColor }};transition:width .3s,background .3s"></div></div>
</div>
</div>
</div>
<!-- ===== RIGHT: CONVERSATION ===== -->
<div style="flex:1;min-width:0;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:56px 64px;display:flex;flex-direction:column;position:relative">
<sc-if value="{{ hasBanner }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;top:44px;right:56px;transform:rotate(-10deg);border:4px solid #8f3a34;color:#8f3a34;font:800 30px ui-monospace,monospace;letter-spacing:.12em;padding:6px 18px;border-radius:8px;opacity:.72">{{ banner }}</div>
</sc-if>
<!-- speaker -->
<div style="font:12px ui-monospace,monospace;letter-spacing:.18em;color:#8a7748">{{ speaker }}</div>
<!-- narration beat -->
<sc-if value="{{ hasNarration }}" hint-placeholder-val="{{ true }}">
<div style="font:18px/1.55 Georgia,serif;font-style:italic;color:#7a684a;margin-top:16px;max-width:900px">{{ narration }}</div>
</sc-if>
<!-- spoken line -->
<sc-if value="{{ hasLine }}" hint-placeholder-val="{{ true }}">
<div style="font:34px/1.4 Georgia,serif;color:#3a2a1a;margin-top:18px;max-width:960px">&ldquo;{{ line }}&rdquo;</div>
</sc-if>
<div style="height:2px;background:#bfa878;margin:36px 0 26px;max-width:960px"></div>
<!-- responses -->
<div style="font:12px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:16px">YOUR RESPONSE</div>
<div style="display:flex;flex-direction:column;gap:10px;max-width:1000px">
<sc-for list="{{ choices }}" as="c" hint-placeholder-count="4">
<div onClick="{{ c.onSelect }}" style="display:flex;align-items:center;gap:16px;background:{{ c.bg }};border:1px solid {{ c.border }};border-radius:11px;padding:15px 18px;cursor:{{ c.cursor }};opacity:{{ c.opacity }}" style-hover="border-color:#8f7a4a">
<span style="font:600 15px ui-monospace,monospace;color:#a9885a;width:16px">{{ c.num }}</span>
<sc-if value="{{ c.hasTag }}" hint-placeholder-val="{{ false }}">
<span style="font:600 10px ui-monospace,monospace;letter-spacing:.08em;color:{{ c.tagFg }};background:{{ c.tagBg }};border:1px solid {{ c.tagFg }};border-radius:5px;padding:4px 9px;white-space:nowrap">{{ c.tag }}</span>
</sc-if>
<span style="flex:1;min-width:0;font:17px system-ui;color:#3a2f1c">{{ c.text }}</span>
<sc-if value="{{ c.hasNote }}" hint-placeholder-val="{{ false }}">
<span style="font:11px ui-monospace,monospace;color:#a08a5a;white-space:nowrap">🔒 {{ c.note }}</span>
</sc-if>
</div>
</sc-for>
</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 {
TAG = {
red: { fg: '#8f3a34', bg: 'rgba(143,58,52,.1)' },
gold: { fg: '#8f6a2a', bg: 'rgba(143,106,42,.12)' },
grey: { fg: '#8a8378', bg: 'rgba(138,131,120,.12)' },
};
nodes() {
return {
start: {
narration: "Grud Ninefingers doesn't get up. Rain runs off the awning between you.",
line: 'You again. And bleeding on my stoop.',
choices: [
{ text: "I've got coin. I need the salve — the real fevergrass.", tag: 'BARTER', to: 'deal' },
{ text: "You'll help me, or I bleed on more than your stoop.", tag: 'INTIMIDATE · STR', tt: 'red', to: 'threat', dd: -8 },
{ text: 'The Bonewright sent me.', tag: 'LIE', tt: 'grey', disabled: true, note: 'Not yet known to the Bonewright' },
{ text: "For the Ninth Saint's sake, Grud.", tag: 'FAITH', tt: 'gold', to: 'faith', dd: 4 },
{ text: '(Leave.)', to: 'leave' },
],
},
deal: {
narration: 'He leans forward at last. The coin-smell interests him more than the blood-smell.',
line: 'Real fevergrass costs. Forty silver, and I forget your face by morning.',
choices: [
{ text: 'Pay the forty.', tag: 'PAY ◈40', tt: 'gold', to: 'paid', dd: 6 },
{ text: 'Twenty. You still owe me for Saltcrol.', tag: 'HAGGLE · DEX', tt: 'red', to: 'haggle' },
{ text: '(Back.)', to: 'start' },
],
},
threat: {
narration: 'His claws spread flat on the counter. Slow. Deliberate.',
line: 'Big words. From a man with rot climbing his arm.',
choices: [
{ text: 'Step closer. Let him smell it.', tag: 'PRESS · STR', tt: 'red', to: 'knife', dd: -10 },
{ text: '...Fine. Coin, then.', to: 'deal', dd: 2 },
],
},
faith: {
narration: 'Something crosses his face. Not kindness. Older than that.',
line: "The Saint didn't save the Saint. But a debt's a debt. Thirty, then.",
choices: [
{ text: 'Pay the thirty.', tag: 'PAY ◈30', tt: 'gold', to: 'paid', dd: 8 },
{ text: '(Back.)', to: 'start' },
],
},
haggle: {
narration: 'A long pause. He remembers Saltcrol. So do you.',
line: '...Thirty. And you never say that name in my alley again.',
choices: [
{ text: 'Done.', tag: 'PAY ◈30', tt: 'gold', to: 'paid', dd: 3 },
{ text: 'Forty it is.', to: 'paid', dd: 1 },
],
},
knife: {
narration: 'Steel scrapes leather. The knife is out before you finish the step.',
line: 'Last chance to walk, sellsword.',
choices: [
{ text: '(Draw your blade.)', tag: 'COMBAT', tt: 'red', to: 'combat' },
{ text: '(Back off, hands up.)', to: 'threat', dd: 2 },
],
},
paid: {
narration: 'He slides a wrapped bundle across. It reeks of pondweed and salvation.',
line: 'Pleasure doing. Try not to die in my alley — bad for trade.',
choices: [{ text: '(Take the salve and go.)', to: 'leave' }],
},
leave: {
end: true,
narration: 'You step back into the rain. The arm still burns, but the coin, or the courage, is spent.',
line: '',
choices: [{ text: '(Start the conversation over.)', to: 'start' }],
},
combat: {
end: true, banner: 'COMBAT',
narration: 'No more talking. The alley narrows to him, the knife, and the space between.',
line: '',
choices: [{ text: '(Start the conversation over.)', to: 'start' }],
},
};
}
state = { node: 'start', disp: 30 };
choose(ch) {
if (ch.disabled) return;
this.setState(s => ({
node: ch.to,
disp: Math.max(0, Math.min(100, s.disp + (ch.dd || 0))),
}));
}
renderVals() {
const n = this.nodes()[this.state.node];
const choices = n.choices.map((c, i) => {
const tag = this.TAG[c.tt] || null;
return {
num: i + 1,
text: c.text,
hasTag: !!c.tag, tag: c.tag || '',
tagFg: tag ? tag.fg : '#8a7748', tagBg: tag ? tag.bg : 'rgba(138,131,120,.12)',
hasNote: !!c.disabled, note: c.note || '',
cursor: c.disabled ? 'default' : 'pointer',
opacity: c.disabled ? '0.5' : '1',
bg: c.disabled ? '#e8ddc2' : '#f1e8d2',
border: c.disabled ? '#d3c39a' : '#bfa878',
onSelect: () => this.choose(c),
};
});
const d = this.state.disp;
let dispLabel, dispColor;
if (d < 20) { dispLabel = 'HOSTILE'; dispColor = '#a5442e'; }
else if (d < 45) { dispLabel = 'WARY'; dispColor = '#a5772a'; }
else if (d < 70) { dispLabel = 'NEUTRAL'; dispColor = '#8a7748'; }
else { dispLabel = 'WARMING'; dispColor = '#5f8a4f'; }
return {
speaker: n.end ? 'THE DM' : 'GRUD NINEFINGERS',
hasNarration: !!n.narration, narration: n.narration,
hasLine: !!n.line, line: n.line,
hasBanner: !!n.banner, banner: n.banner || '',
choices,
dispPct: d + '%', dispLabel, dispColor,
};
}
}
</script>
</body>
</html>

292
mockups/Inventory.dc.html Normal file
View File

@@ -0,0 +1,292 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ============ LEFT: PAPER DOLL ============ -->
<div style="width:620px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);display:flex;flex-direction:column">
<div style="position:absolute;inset:0;background:radial-gradient(120% 70% at 50% 40%,rgba(0,0,0,0) 35%,rgba(0,0,0,.6) 100%)"></div>
<!-- header -->
<div style="position:relative;padding:28px 34px 0">
<div style="font:13px ui-monospace,monospace;letter-spacing:.18em;color:#8a7f6c">EQUIPMENT</div>
<div style="font:600 30px 'Architects Daughter',cursive;color:#e8d9b6;margin-top:4px">Vexcca the Nameless</div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.12em;color:#a9843f;margin-top:2px">HUMAN · SELLSWORD · LVL 4</div>
</div>
<!-- paper doll -->
<div style="position:relative;flex:1;display:flex;align-items:center;justify-content:center">
<div style="position:relative;width:620px;height:430px">
<!-- model -->
<div style="position:absolute;left:50%;top:60px;transform:translateX(-50%);width:150px;height:310px;border:1px dashed #5a5348;border-radius:14px;background:repeating-linear-gradient(135deg,#2c2620 0 14px,#282219 14px 28px);display:flex;align-items:center;justify-content:center;text-align:center;color:#6b6152;font:11px ui-monospace,monospace;letter-spacing:.1em;padding:0 14px">PAPER<br>DOLL</div>
<!-- equipment slots -->
<sc-for list="{{ equip }}" as="s" hint-placeholder-count="12">
<div style="position:absolute;{{ s.posStyle }}">
<div onClick="{{ s.onSelect }}" style="width:62px;height:62px;border-radius:10px;border:1px solid #4a4238;background:rgba(18,15,11,.72);position:relative;display:flex;align-items:center;justify-content:center;cursor:pointer" style-hover="border-color:#a9843f">
<sc-if value="{{ s.filled }}" hint-placeholder-val="{{ true }}">
<span style="font:600 11px ui-monospace,monospace;color:#c9bda3">{{ s.glyph }}</span>
<div style="position:absolute;left:6px;right:6px;bottom:5px;height:3px;border-radius:2px;background:{{ s.rarityColor }}"></div>
</sc-if>
<sc-if value="{{ s.empty }}" hint-placeholder-val="{{ false }}">
<span style="font:8px ui-monospace,monospace;letter-spacing:.08em;color:#5a5348">{{ s.label }}</span>
</sc-if>
<sc-if value="{{ s.sel }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-2px;border:2.5px solid #a9843f;border-radius:11px;pointer-events:none"></div>
</sc-if>
</div>
</div>
</sc-for>
</div>
</div>
<!-- derived stats -->
<div style="position:relative;display:flex;gap:10px;padding:0 34px 34px">
<div style="flex:1;background:rgba(18,15,11,.7);border:1px solid #3a352e;border-radius:10px;padding:11px 12px"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c">ARMOR</div><div style="font:600 22px 'Architects Daughter',cursive;color:#e8d9b6">14</div></div>
<div style="flex:1;background:rgba(18,15,11,.7);border:1px solid #3a352e;border-radius:10px;padding:11px 12px"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c">DAMAGE</div><div style="font:600 22px 'Architects Daughter',cursive;color:#e8d9b6">1217</div></div>
<div style="flex:1;background:rgba(18,15,11,.7);border:1px solid #3a352e;border-radius:10px;padding:11px 12px"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#caa39c">HP</div><div style="font:600 22px 'Architects Daughter',cursive;color:#e8d9b6">42/60</div></div>
<div style="flex:1;background:rgba(18,15,11,.7);border:1px solid #3a352e;border-radius:10px;padding:11px 12px"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#9caccb">MP</div><div style="font:600 22px 'Architects Daughter',cursive;color:#e8d9b6">18/30</div></div>
</div>
</div>
<!-- ============ RIGHT: SATCHEL ============ -->
<div style="width:1300px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:30px 40px;display:flex;flex-direction:column;gap:18px">
<!-- header -->
<div style="display:flex;align-items:flex-end;justify-content:space-between;border-bottom:2px solid #bfa878;padding-bottom:14px">
<div>
<div style="font:600 30px 'Architects Daughter',cursive;letter-spacing:.04em;color:#5a4326">The Satchel</div>
<div style="font:12px ui-monospace,monospace;color:#8a7748;letter-spacing:.08em;margin-top:2px">EVERYTHING YOU'VE STOLEN, LOOTED OR REFUSED TO DROP</div>
</div>
<div style="display:flex;align-items:flex-end;gap:22px">
<div style="text-align:right"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">GOLD</div><div style="font:600 22px ui-monospace,monospace;color:#8f6a2a">◈ 1,240</div></div>
<div style="text-align:right;width:200px">
<div style="display:flex;justify-content:space-between;font:10px ui-monospace,monospace;letter-spacing:.08em;color:#8a7748"><span>ENCUMBRANCE</span><span style="color:{{ weightColor }}">{{ weightText }}</span></div>
<div style="height:9px;border-radius:5px;background:#d3c39a;border:1px solid #bfa878;overflow:hidden;margin-top:4px"><div style="width:{{ weightPct }};height:100%;background:{{ weightColor }}"></div></div>
</div>
<div style="font:11px ui-monospace,monospace;color:#a99464;border:1px solid #cbb684;border-radius:6px;padding:6px 10px;cursor:pointer" style-hover="background:#e6d9b8">ESC ✕</div>
</div>
</div>
<!-- tabs -->
<div style="display:flex;gap:8px">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="5">
<div onClick="{{ t.onSelect }}" style="font:12px ui-monospace,monospace;letter-spacing:.08em;padding:7px 14px;border-radius:7px;border:1px solid {{ t.border }};background:{{ t.bg }};color:{{ t.fg }};cursor:pointer">{{ t.label }}</div>
</sc-for>
</div>
<!-- grid -->
<div style="display:grid;grid-template-columns:repeat(12,1fr);gap:8px">
<sc-for list="{{ cells }}" as="c" hint-placeholder-count="60">
<div onClick="{{ c.onSelect }}" style="aspect-ratio:1;border-radius:8px;border:1px solid #cbb684;background:{{ c.bg }};position:relative;cursor:pointer;display:flex;align-items:center;justify-content:center" style-hover="border-color:#8f7a4a">
<sc-if value="{{ c.filled }}" hint-placeholder-val="{{ false }}">
<span style="font:600 12px ui-monospace,monospace;color:#5a4326">{{ c.glyph }}</span>
<div style="position:absolute;left:5px;right:5px;bottom:4px;height:3px;border-radius:2px;background:{{ c.rarityColor }}"></div>
<span style="position:absolute;top:3px;right:5px;font:9px ui-monospace,monospace;color:#8a7748">{{ c.qtyLabel }}</span>
</sc-if>
<sc-if value="{{ c.sel }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-2px;border:2.5px solid #8f3a34;border-radius:9px;pointer-events:none"></div>
</sc-if>
</div>
</sc-for>
</div>
<!-- detail -->
<div style="margin-top:auto;min-height:196px;background:#f1e8d2;border:1px solid #bfa878;border-radius:12px;padding:20px 24px">
<sc-if value="{{ hasSel }}" hint-placeholder-val="{{ true }}">
<div style="display:flex;gap:22px;height:100%">
<!-- icon -->
<div style="width:96px;height:96px;border-radius:12px;border:2px solid {{ selRarityColor }};background:#e6d9b8;position:relative;display:flex;align-items:center;justify-content:center;flex:none">
<span style="font:600 16px ui-monospace,monospace;color:#5a4326">{{ selGlyph }}</span>
<div style="position:absolute;left:8px;right:8px;bottom:7px;height:4px;border-radius:2px;background:{{ selRarityColor }}"></div>
</div>
<!-- body -->
<div style="flex:1;min-width:0">
<div style="font:600 24px 'Architects Daughter',cursive;color:{{ selRarityColor }};line-height:1.1">{{ selName }}</div>
<div style="font:11px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748;margin-top:3px">{{ selType }}</div>
<div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:5px 18px">
<sc-for list="{{ selStats }}" as="line" hint-placeholder-count="2">
<div style="font:14px system-ui;color:#3a2f1c">▸ {{ line }}</div>
</sc-for>
</div>
<div style="margin-top:12px;font:14px/1.5 Georgia,serif;font-style:italic;color:#7a684a">"{{ selFlavor }}"</div>
</div>
<!-- side: value + actions -->
<div style="width:180px;flex:none;display:flex;flex-direction:column;gap:8px;border-left:1px solid #cbb684;padding-left:20px">
<div style="display:flex;justify-content:space-between;font:12px ui-monospace,monospace;color:#8a7748"><span>WEIGHT</span><span style="color:#5a4326">{{ selWeight }}</span></div>
<div style="display:flex;justify-content:space-between;font:12px ui-monospace,monospace;color:#8a7748"><span>VALUE</span><span style="color:#8f6a2a">◈ {{ selValue }}</span></div>
<div style="margin-top:auto;display:flex;flex-direction:column;gap:7px">
<sc-if value="{{ selCanEquip }}" hint-placeholder-val="{{ false }}">
<div style="text-align:center;padding:9px 0;border:1.5px solid #8f3a34;border-radius:8px;background:linear-gradient(180deg,#a5423a,#7a2f29);color:#f4e6cf;font:600 13px system-ui;letter-spacing:.04em;cursor:pointer" style-hover="background:#b8524a">EQUIP</div>
</sc-if>
<sc-if value="{{ selCanUse }}" hint-placeholder-val="{{ false }}">
<div style="text-align:center;padding:9px 0;border:1.5px solid #8f3a34;border-radius:8px;background:linear-gradient(180deg,#a5423a,#7a2f29);color:#f4e6cf;font:600 13px system-ui;letter-spacing:.04em;cursor:pointer" style-hover="background:#b8524a">USE</div>
</sc-if>
<div style="text-align:center;padding:9px 0;border:1px solid #a99464;border-radius:8px;background:#e6d9b8;color:#5a4326;font:600 13px system-ui;letter-spacing:.04em;cursor:pointer" style-hover="background:#d9c9a2">DROP</div>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{ noSel }}" hint-placeholder-val="{{ false }}">
<div style="height:100%;display:flex;align-items:center;justify-content:center;font:15px 'Architects Daughter',cursive;color:#a99464">Select something. Turn it over in your hands.</div>
</sc-if>
</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 {
RARITY = { common: '#8a8378', uncommon: '#5f8a4f', rare: '#4f7ab8', epic: '#8a5fb8', legendary: '#c8963f' };
COLS = 12; ROWS = 5;
MAXW = 60;
items() {
return {
// equipped
falchion: { name: 'Notched Falchion', cat: 'weapon', slot: 'mainhand', rarity: 'uncommon', glyph: 'FAL', weight: 4.5, value: 85, qty: 1, stats: ['1217 slashing', '+1 to hit vs. unarmored'], flavor: 'The nicks in the edge are from bone, not practice.' },
buckler: { name: 'Dwarf-iron Buckler', cat: 'armor', slot: 'offhand', rarity: 'uncommon', glyph: 'BKL', weight: 5, value: 70, qty: 1, stats: ['+4 block', 'Shield bash: 1d4'], flavor: 'Dented inward. Whatever hit it came off worse.' },
hood: { name: "Cutpurse's Hood", cat: 'armor', slot: 'head', rarity: 'uncommon', glyph: 'HD', weight: 1, value: 55, qty: 1, stats: ['+2 armor', '+1 DEX'], flavor: 'Keeps the rain and the witnesses off.' },
jerkin: { name: 'Boiled Leather Jerkin', cat: 'armor', slot: 'chest', rarity: 'common', glyph: 'JRK', weight: 8, value: 40, qty: 1, stats: ['+3 armor', '1 to sneak'], flavor: 'Stinks of the tanner who died in it.' },
amulet: { name: 'Amulet of the Ninth Saint', cat: 'misc', slot: 'amulet', rarity: 'rare', glyph: 'AMU', weight: 0.2, value: 320, qty: 1, stats: ['+2 FTH', 'Once a day: reroll a death save'], flavor: 'The saint was hanged. The amulet remembers.' },
ringdrowned: { name: 'Ring of the Drowned', cat: 'misc', slot: 'ring1', rarity: 'rare', glyph: 'RNG', weight: 0.1, value: 280, qty: 1, stats: ['+1 MAG', 'Breathe water, briefly'], flavor: 'Cold, always. It has not dried in years.' },
boots: { name: 'Split Marching Boots', cat: 'armor', slot: 'boots', rarity: 'common', glyph: 'BT', weight: 2, value: 15, qty: 1, stats: ['+1 armor'], flavor: 'One sole flaps. You walk quiet anyway.' },
// backpack
shiv: { name: 'Rusted Shiv', cat: 'weapon', slot: 'mainhand', rarity: 'common', glyph: 'SHV', weight: 2, value: 5, qty: 1, stats: ['47 slashing', 'Snaps on a natural 1'], flavor: 'A door hinge with ambitions.' },
cap: { name: 'Padded Cap', cat: 'armor', slot: 'head', rarity: 'common', glyph: 'CAP', weight: 1, value: 8, qty: 1, stats: ['+1 armor'], flavor: 'Sweat-stiff. Previous owner not consulted.' },
salve: { name: 'Fevergrass Salve', cat: 'consumable', rarity: 'common', glyph: 'SLV', weight: 0.3, value: 20, qty: 3, stats: ['Clears Infection (1)', 'Heals 2d4 over time'], flavor: 'Tastes of pond. Works anyway.' },
bile: { name: 'Vial of Black Bile', cat: 'consumable', rarity: 'uncommon', glyph: 'BILE', weight: 0.4, value: 45, qty: 2, stats: ['Throw: 2d6 acid', 'Fouls the ground it hits'], flavor: 'Do not uncork it near your face.' },
ration: { name: 'Stale Ration', cat: 'consumable', rarity: 'common', glyph: 'RTN', weight: 0.5, value: 2, qty: 4, stats: ['Removes Hungry', 'Small chance of the runs'], flavor: 'Hard enough to break a tooth. Some have.' },
picks: { name: "Tinker's Lockpicks", cat: 'tool', rarity: 'uncommon', glyph: 'PICK', weight: 0.2, value: 60, qty: 1, stats: ['+2 to pick locks', 'Breaks on a fumble'], flavor: 'Six left. There were twelve.' },
bones: { name: 'Human Finger-bones', cat: 'misc', rarity: 'common', glyph: 'BNE', weight: 0.1, value: 0, qty: 9, stats: ["Someone's tally"], flavor: 'Not yours. You keep counting them anyway.' },
signet: { name: 'Cracked Signet', cat: 'misc', rarity: 'uncommon', glyph: 'SIG', weight: 0.1, value: 120, qty: 1, stats: ['Quest item'], flavor: 'A noble house that no longer pays its debts.' },
};
}
slotList() {
return [
{ key: 'head', label: 'HEAD', pos: 'left:30px;top:12px' },
{ key: 'amulet', label: 'NECK', pos: 'left:30px;top:80px' },
{ key: 'chest', label: 'CHEST', pos: 'left:30px;top:148px' },
{ key: 'cloak', label: 'CLOAK', pos: 'left:30px;top:216px' },
{ key: 'gloves', label: 'HANDS', pos: 'left:30px;top:284px' },
{ key: 'belt', label: 'BELT', pos: 'left:30px;top:352px' },
{ key: 'mainhand', label: 'MAIN', pos: 'left:528px;top:12px' },
{ key: 'offhand', label: 'OFF', pos: 'left:528px;top:80px' },
{ key: 'ring1', label: 'RING', pos: 'left:528px;top:148px' },
{ key: 'ring2', label: 'RING', pos: 'left:528px;top:216px' },
{ key: 'legs', label: 'LEGS', pos: 'left:528px;top:284px' },
{ key: 'boots', label: 'FEET', pos: 'left:528px;top:352px' },
];
}
equipped() {
return { head: 'hood', amulet: 'amulet', chest: 'jerkin', cloak: null, gloves: null, belt: null, mainhand: 'falchion', offhand: 'buckler', ring1: 'ringdrowned', ring2: null, legs: null, boots: 'boots' };
}
backpack() { return ['shiv', 'cap', 'salve', 'bile', 'ration', 'picks', 'bones', 'signet']; }
state = { sel: 'falchion', tab: 'all' };
tabDefs() {
return [
{ id: 'all', label: 'ALL', test: () => true },
{ id: 'weapon', label: 'WEAPONS', test: (it) => it.cat === 'weapon' },
{ id: 'armor', label: 'ARMOR', test: (it) => it.cat === 'armor' },
{ id: 'consumable', label: 'CONSUMABLES', test: (it) => it.cat === 'consumable' },
{ id: 'misc', label: 'MISC', test: (it) => it.cat === 'misc' || it.cat === 'tool' },
];
}
rc(r) { return this.RARITY[r] || '#8a8378'; }
renderVals() {
const ITEMS = this.items();
const eq = this.equipped();
const sel = this.state.sel;
// equipment slots
const equip = this.slotList().map(s => {
const id = eq[s.key];
const it = id ? ITEMS[id] : null;
return {
key: s.key, label: s.label, posStyle: s.pos,
filled: !!it, empty: !it,
glyph: it ? it.glyph : '',
rarityColor: it ? this.rc(it.rarity) : '#4a4238',
sel: !!id && id === sel,
onSelect: () => { if (id) this.setState({ sel: id }); },
};
});
// tabs
const tab = this.state.tab;
const tabs = this.tabDefs().map(t => ({
label: t.label,
onSelect: () => this.setState({ tab: t.id }),
bg: t.id === tab ? '#8f3a34' : '#efe6cf',
fg: t.id === tab ? '#f1e8d2' : '#6a5a3a',
border: t.id === tab ? '#8f3a34' : '#cbb684',
}));
const activeTest = this.tabDefs().find(t => t.id === tab).test;
// grid cells
const visible = this.backpack().filter(id => activeTest(ITEMS[id]));
const total = this.COLS * this.ROWS;
const cells = [];
for (let i = 0; i < total; i++) {
const id = visible[i];
const it = id ? ITEMS[id] : null;
cells.push({
filled: !!it,
glyph: it ? it.glyph : '',
qtyLabel: it && it.qty > 1 ? ('×' + it.qty) : '',
rarityColor: it ? this.rc(it.rarity) : '#cbb684',
bg: it ? '#f4ecd6' : '#e4d6b6',
sel: !!id && id === sel,
onSelect: () => { if (id) this.setState({ sel: id }); },
});
}
// encumbrance
let w = 0;
Object.values(eq).forEach(id => { if (id) w += ITEMS[id].weight; });
this.backpack().forEach(id => { const it = ITEMS[id]; w += it.weight * it.qty; });
w = Math.round(w * 10) / 10;
const pct = Math.min(100, Math.round((w / this.MAXW) * 100));
// detail
const it = ITEMS[sel];
const typeLabel = it ? this.typeOf(it) : '';
return {
equip, tabs, cells,
weightText: w + ' / ' + this.MAXW,
weightPct: pct + '%',
weightColor: pct > 90 ? '#8f3a34' : (pct > 70 ? '#a5772a' : '#5f8a4f'),
hasSel: !!it, noSel: !it,
selName: it ? it.name : '',
selType: typeLabel,
selGlyph: it ? it.glyph : '',
selRarityColor: it ? this.rc(it.rarity) : '#5a4326',
selStats: it ? it.stats : [],
selFlavor: it ? it.flavor : '',
selWeight: it ? it.weight : '',
selValue: it ? it.value : '',
selCanEquip: !!(it && it.slot),
selCanUse: !!(it && it.cat === 'consumable'),
};
}
typeOf(it) {
const capt = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const cat = it.cat === 'tool' ? 'Tool' : (it.cat === 'misc' ? 'Trinket' : capt(it.cat));
if (it.slot) return cat + ' · ' + capt(it.slot);
return cat;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,363 @@
<!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>
<meta name="design_doc_mode" content="canvas">
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&display=swap" rel="stylesheet">
<style>
body{margin:0;background:#efece6;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
.dv-turn{padding:44px 48px 40px;border-bottom:1px solid rgba(0,0,0,.08);scroll-margin-top:16px}
.dv-thd{display:flex;align-items:baseline;gap:10px;margin:0 0 6px}
.dv-tid{font:600 10px ui-monospace,Menlo,monospace;padding:3px 7px;background:#1a1a1a;color:#fff;border-radius:4px;text-decoration:none}
.dv-tname{font:600 14px/1.3 system-ui,sans-serif;color:#1a1a1a}
.dv-sub{margin:0 0 28px;font:13px/1.55 system-ui,sans-serif;color:rgba(0,0,0,.52);max-width:820px}
.dv-opts{display:flex;flex-wrap:wrap;gap:44px;align-items:flex-start}
.dv-opt{flex:none;display:flex;flex-direction:column;gap:12px;scroll-margin-top:16px}
.dv-oid{font:600 10.5px ui-monospace,Menlo,monospace;padding:3px 7px;background:rgba(0,0,0,.08);color:#1a1a1a;border-radius:5px;text-decoration:none}
.dv-olabel{display:flex;align-items:baseline;gap:10px;font:400 12.5px/1.45 system-ui,sans-serif;color:rgba(0,0,0,.58);max-width:900px}
.dv-card{background:#0f0d0b;border:1px solid rgba(0,0,0,.35);border-radius:10px;box-shadow:0 6px 22px rgba(0,0,0,.18);overflow:hidden}
.dv-opt:target .dv-oid{background:#8f3a34;color:#fff}
.dv-next{margin:30px 0 0;font:12.5px/1.7 system-ui,sans-serif;color:rgba(0,0,0,.5);max-width:860px}
</style>
</helmet>
<section class="dv-turn" id="t2">
<div class="dv-thd"><a class="dv-tid" href="#t2">2</a><span class="dv-tname">1b built out — full 1920×1080</span></div>
<p class="dv-sub"><a class="dv-oid" href="#1b">1b</a> at real scale. Added a <strong>slide-in system dock</strong> on the right edge of the world (Inventory / Character / Quests / Map / Spellbook / Party) — click the tab to slide it in and out. The bottom row is now labeled as <strong>quick-select consumables</strong> pulled from your inventory, with an <strong>End Turn</strong> control for the turn-based loop, and the narration book gains a free-text action line for that tabletop feel.</p>
<div class="dv-opts">
<div class="dv-opt" id="2a">
<div class="dv-olabel"><a class="dv-oid" href="#2a">2a</a><span><strong>The full main window.</strong> Try the dock tab (right edge of the world). Everything is placeholder art — real isometric render, item icons and portraits drop into the marked slots.</span></div>
<div class="dv-card" style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ================= LEFT: WORLD ================= -->
<div style="width:1180px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px)">
<div style="position:absolute;inset:0;background:radial-gradient(120% 80% at 50% 40%,rgba(0,0,0,0) 40%,rgba(0,0,0,.55) 100%)"></div>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:#6b6152;font:16px ui-monospace,monospace;letter-spacing:.18em">ISOMETRIC WORLD VIEW<span style="font:18px 'Architects Daughter',cursive;letter-spacing:.02em;color:#8a7f6c">party &amp; enemies on the tile grid · real render here</span></div>
<!-- turn order -->
<div style="position:absolute;top:28px;left:28px;background:rgba(18,15,12,.82);border:1px solid rgba(200,180,140,.2);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:11px">
<div style="font:12px ui-monospace,monospace;letter-spacing:.16em;color:#9a8f78">TURN ORDER</div>
<div style="display:flex;gap:12px">
<div style="position:relative"><div style="width:60px;height:60px;border-radius:50%;border:3px solid #a9843f;background:#3a332a;color:#c9bda3;font:600 15px system-ui;display:flex;align-items:center;justify-content:center;box-shadow:0 0 14px rgba(169,132,63,.5)">EL</div><div style="position:absolute;top:-4px;right:-4px;width:20px;height:20px;border-radius:50%;background:#a9843f;color:#1a1512;font:700 10px ui-monospace;display:flex;align-items:center;justify-content:center">17</div></div>
<div style="position:relative"><div style="width:60px;height:60px;border-radius:50%;border:2px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 15px system-ui;display:flex;align-items:center;justify-content:center">DW</div><div style="position:absolute;top:-4px;right:-4px;width:20px;height:20px;border-radius:50%;background:#3a352e;color:#c9bda3;font:700 10px ui-monospace;display:flex;align-items:center;justify-content:center">12</div></div>
<div style="position:relative"><div style="width:60px;height:60px;border-radius:50%;border:2px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 15px system-ui;display:flex;align-items:center;justify-content:center">HU</div><div style="position:absolute;top:-4px;right:-4px;width:20px;height:20px;border-radius:50%;background:#3a352e;color:#c9bda3;font:700 10px ui-monospace;display:flex;align-items:center;justify-content:center">09</div></div>
<div style="position:relative"><div style="width:60px;height:60px;border-radius:50%;border:2px dashed #6b3b38;background:#2a1c1a;color:#b8524a;font:600 20px system-ui;display:flex;align-items:center;justify-content:center"></div></div>
</div>
</div>
<!-- minimap -->
<div style="position:absolute;top:28px;right:28px;width:200px;height:200px;background:rgba(18,15,12,.82);border:1px solid rgba(200,180,140,.24);border-radius:12px;display:flex;align-items:center;justify-content:center;color:#7a7060;font:12px ui-monospace,monospace;letter-spacing:.14em">MINIMAP</div>
<!-- slide-in system dock -->
<div style="position:absolute;right:44px;top:50%;transition:transform .32s cubic-bezier(.4,0,.2,1),opacity .32s;transform:{{ dockShift }};opacity:{{ dockOpacity }};display:flex;flex-direction:column;gap:10px;background:rgba(14,11,9,.9);border:1px solid #a9843f;border-radius:12px;padding:12px">
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">I</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">Inventory</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">C</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">Character</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">J</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">Quest Log</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">K</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">Spellbook</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">M</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">World Map</span></div>
<div style="display:flex;align-items:center;gap:12px"><div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.75);display:flex;align-items:center;justify-content:center;font:700 15px ui-monospace;color:#c9bda3">P</div><span style="font:600 14px system-ui;color:#c9bda3;letter-spacing:.04em">Party</span></div>
</div>
<!-- dock handle -->
<div onClick="{{ toggleDock }}" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:26px;height:104px;background:rgba(14,11,9,.92);border:1px solid #a9843f;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#a9843f;font:700 22px ui-monospace">{{ handleArrow }}</div>
<!-- bottom command bar -->
<div style="position:absolute;left:0;right:0;bottom:0;height:150px;background:linear-gradient(180deg,rgba(10,8,6,0),rgba(10,8,6,.94) 40px);display:flex;align-items:flex-end;gap:24px;padding:0 28px 22px;box-sizing:border-box">
<!-- stats -->
<div style="display:flex;flex-direction:column;gap:8px;width:230px">
<div><div style="font:11px ui-monospace,monospace;color:#caa39c;margin-bottom:3px">HP 42 / 60</div><div style="height:12px;border-radius:6px;background:#2a251f;border:1px solid #4a4238;overflow:hidden"><div style="width:70%;height:100%;background:linear-gradient(90deg,#b8524a,#8f3a34)"></div></div></div>
<div><div style="font:11px ui-monospace,monospace;color:#9caccb;margin-bottom:3px">MP 18 / 30</div><div style="height:12px;border-radius:6px;background:#2a251f;border:1px solid #4a4238;overflow:hidden"><div style="width:60%;height:100%;background:linear-gradient(90deg,#4f7ab8,#243f6f)"></div></div></div>
<div style="display:flex;align-items:center;gap:8px;margin-top:2px"><span style="font:11px ui-monospace,monospace;color:#c9a86a">GOLD</span><span style="font:600 17px ui-monospace,monospace;color:#d9b46a">◈ 1,240 gp</span></div>
</div>
<!-- quick-select consumables -->
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:7px">
<div style="font:11px 'Architects Daughter',cursive;letter-spacing:.06em;color:#8a7f6c">QUICK-SELECT · consumables from inventory</div>
<div style="display:flex;gap:9px">
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">1</span>Salve</div>
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">2</span>Draught</div>
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">3</span>Bomb</div>
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">4</span>Antidote</div>
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">5</span>Ration</div>
<div style="width:62px;height:62px;border:1px solid #6b6152;border-radius:8px;background:rgba(40,34,28,.72);position:relative;display:flex;align-items:flex-end;justify-content:center;padding-bottom:5px;font:9px system-ui;color:#9a8f78"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#a9843f">6</span>Torch</div>
<div style="width:62px;height:62px;border:1px dashed #4a4238;border-radius:8px;background:rgba(30,26,22,.5);position:relative"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#5a5348">7</span></div>
<div style="width:62px;height:62px;border:1px dashed #4a4238;border-radius:8px;background:rgba(30,26,22,.5);position:relative"><span style="position:absolute;top:3px;left:6px;font:10px ui-monospace,monospace;color:#5a5348">8</span></div>
</div>
</div>
<!-- end turn -->
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div style="padding:16px 26px;border:1.5px solid #a9843f;border-radius:10px;background:linear-gradient(180deg,#3a2f1f,#2a2015);color:#e8d9b6;font:600 16px system-ui;letter-spacing:.06em;box-shadow:0 0 14px rgba(169,132,63,.28);cursor:pointer">END TURN</div>
<span style="font:10px ui-monospace,monospace;color:#7a7060">↵ enter</span>
</div>
</div>
</div>
<!-- ================= RIGHT: NARRATION BOOK ================= -->
<div style="width:740px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:34px 40px;display:flex;flex-direction:column">
<div style="display:flex;align-items:baseline;justify-content:space-between;border-bottom:2px solid #bfa878;padding-bottom:14px;margin-bottom:22px">
<span style="font:600 24px 'Architects Daughter',cursive;letter-spacing:.06em;color:#5a4326">THE DUNGEON MASTER</span>
<span style="font:13px ui-monospace,monospace;color:#8a7748;letter-spacing:.1em">ROUND 4 · THE LOWER WARD</span>
</div>
<div style="font:20px/1.65 Georgia,serif;color:#4a3f2c;flex:1;overflow:hidden">
<p style="margin:0 0 18px">The alley stinks of piss and rot. Rain beads on the cobbles and runs black into the gutter. Your left arm has gone hot to the touch — the bite you took in the sewers three nights ago is turning, and the fever with it.</p>
<p style="margin:0 0 18px">The beastfolk fence weighs your coin purse in one clawed hand, unhurried. His good eye drifts from the silver to the soft flesh of your throat, and back again.</p>
<p style="margin:0 0 18px;color:#6a5a3a;font-style:italic">"Thin," he says. "For what you're asking."</p>
<p style="margin:0;padding:14px 16px;background:#d9c9a2;border-left:4px solid #8f3a34;color:#5a3230;font-style:italic;font-size:18px">✗ Sleight of Hand (14) failed. His grip closes hard around your wrist.</p>
</div>
<div style="border-top:2px solid #bfa878;padding-top:20px;margin-top:20px">
<div style="font:600 18px 'Architects Daughter',cursive;color:#5a4326;margin-bottom:14px">▸ Your turn. What do you do?</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div style="border:1px solid #8f7a4a;border-radius:8px;padding:13px 16px;font:16px system-ui;color:#4a3f2c;background:#f1e8d2;cursor:pointer" style-hover="background:#e6d9b8;border-color:#5a4326"><strong style="color:#8f6a2a">1</strong> · Pay the fence double and keep your hand where he can see it</div>
<div style="border:1px solid #8f7a4a;border-radius:8px;padding:13px 16px;font:16px system-ui;color:#4a3f2c;background:#f1e8d2;cursor:pointer" style-hover="background:#e6d9b8;border-color:#5a4326"><strong style="color:#8f6a2a">2</strong> · Draw your blade — he's closer than his guards</div>
<div style="border:1px solid #8f7a4a;border-radius:8px;padding:13px 16px;font:16px system-ui;color:#4a3f2c;background:#f1e8d2;cursor:pointer" style-hover="background:#e6d9b8;border-color:#5a4326"><strong style="color:#8f6a2a">3</strong> · Lie — tell him the rest is coming with your dwarf</div>
</div>
<div style="margin-top:16px;display:flex;align-items:center;gap:12px;border:1px dashed #a99464;border-radius:8px;padding:12px 16px;background:rgba(255,255,255,.35)">
<span style="font:14px ui-monospace,monospace;color:#8a7748"></span>
<span style="font:16px Georgia,serif;font-style:italic;color:#8a7c5a">…or describe your own action</span>
</div>
</div>
</div>
</div>
</div>
</div>
<p class="dv-next">Try next: "the dock should be icons, not text labels" · "add a combat-log tab to the narration book" · "now sketch the character-creation screen" · "sketch the inventory screen the I button opens" · "make the book parchment darker / grittier"</p>
</section>
<section class="dv-turn" id="t1">
<div class="dv-thd"><a class="dv-tid" href="#t1">1</a><span class="dv-tname">Main window — 4 layout directions</span></div>
<p class="dv-sub">1920×1080 in-game HUD. Common ingredients across all four: an isometric tile view, a turn-by-turn <strong>Dungeon Master narration feed</strong>, HP / MP / stamina, a keyed action hotbar, and inventory / currency quick-slots. What changes is <strong>where the narration lives</strong> and <strong>how ornate the frame is</strong> — the two axes that matter most for a DnD-table feel. Rough sketches; ignore exact colors.</p>
<div class="dv-opts">
<!-- ============ 1a : IMMERSIVE ORB BAR ============ -->
<div class="dv-opt" id="1a">
<div class="dv-olabel"><a class="dv-oid" href="#1a">1a</a><span><strong>Immersive orb bar (Diablo 2 / Grim Dawn).</strong> Full-bleed world, everything floats over it. Narration is a translucent log you can collapse. Most game-first, least reading-focused.</span></div>
<div class="dv-card" style="width:900px;height:506px;position:relative;background:repeating-linear-gradient(135deg,#26221c 0 12px,#221e18 12px 24px)">
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.14em">ISOMETRIC WORLD VIEW<span style="font:11px 'Architects Daughter',cursive;letter-spacing:.02em;color:#8a7f6c">party &amp; enemies on the tile grid</span></div>
<!-- turn order top-left -->
<div style="position:absolute;top:16px;left:16px;background:rgba(18,15,12,.8);border:1px solid rgba(200,180,140,.18);border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:6px">
<div style="font:10px ui-monospace,monospace;letter-spacing:.14em;color:#9a8f78">TURN ORDER</div>
<div style="display:flex;gap:7px">
<div style="width:38px;height:38px;border-radius:50%;border:2px solid #a9843f;background:#3a332a;color:#c9bda3;font:600 10px system-ui;display:flex;align-items:center;justify-content:center">EL</div>
<div style="width:38px;height:38px;border-radius:50%;border:1px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 10px system-ui;display:flex;align-items:center;justify-content:center">DW</div>
<div style="width:38px;height:38px;border-radius:50%;border:1px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 10px system-ui;display:flex;align-items:center;justify-content:center">HU</div>
<div style="width:38px;height:38px;border-radius:50%;border:1px dashed #6b3b38;background:#2a1c1a;color:#b8524a;font:600 10px system-ui;display:flex;align-items:center;justify-content:center"></div>
</div>
</div>
<!-- minimap top-right -->
<div style="position:absolute;top:16px;right:16px;width:124px;height:124px;background:rgba(18,15,12,.8);border:1px solid rgba(200,180,140,.2);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#7a7060;font:10px ui-monospace,monospace;letter-spacing:.1em">MINIMAP</div>
<!-- narration log floating bottom-left -->
<div style="position:absolute;left:16px;bottom:150px;width:300px;background:rgba(14,11,9,.82);border:1px solid rgba(200,180,140,.16);border-radius:8px;padding:11px 13px">
<div style="font:10px ui-monospace,monospace;letter-spacing:.14em;color:#a9843f;margin-bottom:6px">⌂ DUNGEON MASTER ▾</div>
<div style="font:12.5px/1.5 Georgia,serif;color:#c9bda3">The alley stinks of piss and rot. Your left arm's gone hot — the bite is turning. The fence weighs your coin, then your throat.</div>
<div style="margin-top:8px;font:11px 'Architects Daughter',cursive;color:#7a7060">␣ continue · click to expand log</div>
</div>
<!-- bottom bar -->
<div style="position:absolute;left:0;right:0;bottom:0;height:132px;background:linear-gradient(180deg,rgba(10,8,6,0),rgba(10,8,6,.92) 42px);display:flex;align-items:flex-end;justify-content:space-between;padding:0 26px 16px;box-sizing:border-box">
<!-- HP orb -->
<div style="text-align:center"><div style="width:88px;height:88px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#b8524a,#6f2620);border:2px solid #1a1512"></div><div style="font:10px ui-monospace,monospace;color:#c9a; margin-top:4px;color:#caa39c">HP 42/60</div></div>
<!-- center: stamina + hotbar -->
<div style="display:flex;flex-direction:column;align-items:center;gap:7px;flex:1;padding:0 20px">
<div style="width:260px;height:7px;border-radius:4px;background:#2a251f;border:1px solid #4a4238;overflow:hidden"><div style="width:64%;height:100%;background:#8f9a4f"></div></div>
<div style="display:flex;gap:7px">
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">1</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">2</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">3</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">4</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">5</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">6</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">7</span></div>
<div style="width:44px;height:44px;border:1px solid #6b6152;border-radius:6px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">8</span></div>
</div>
</div>
<!-- right: quick slots + MP orb -->
<div style="display:flex;align-items:flex-end;gap:14px">
<div style="display:flex;flex-direction:column;gap:6px;align-items:center">
<div style="display:flex;gap:6px">
<div style="width:34px;height:34px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:34px;height:34px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:34px;height:34px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
</div>
<div style="font:10px ui-monospace,monospace;color:#a9843f">◈ 1,240 gp</div>
</div>
<div style="text-align:center"><div style="width:88px;height:88px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#4f7ab8,#243f6f);border:2px solid #1a1512"></div><div style="font:10px ui-monospace,monospace;margin-top:4px;color:#9caccb">MP 18/30</div></div>
</div>
</div>
</div>
</div>
<!-- ============ 1b : TABLETOP SPLIT ============ -->
<div class="dv-opt" id="1b">
<div class="dv-olabel"><a class="dv-oid" href="#1b">1b</a><span><strong>Tabletop split — the DM's screen.</strong> World on the left, a permanent narration column on the right like an open book. Best for reading-heavy, story-first turns. Leans hardest into the DnD-table feel.</span></div>
<div class="dv-card" style="width:900px;height:506px;position:relative;display:flex">
<!-- LEFT: world -->
<div style="width:534px;height:506px;position:relative;background:repeating-linear-gradient(135deg,#26221c 0 12px,#221e18 12px 24px)">
<div style="position:absolute;inset:0 0 96px 0;display:flex;align-items:center;justify-content:center;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.14em">ISOMETRIC WORLD VIEW</div>
<!-- turn order -->
<div style="position:absolute;top:14px;left:14px;background:rgba(18,15,12,.8);border:1px solid rgba(200,180,140,.18);border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:6px">
<div style="font:10px ui-monospace,monospace;letter-spacing:.14em;color:#9a8f78">TURN ORDER</div>
<div style="display:flex;gap:7px">
<div style="width:34px;height:34px;border-radius:50%;border:2px solid #a9843f;background:#3a332a;color:#c9bda3;font:600 9px system-ui;display:flex;align-items:center;justify-content:center">EL</div>
<div style="width:34px;height:34px;border-radius:50%;border:1px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 9px system-ui;display:flex;align-items:center;justify-content:center">DW</div>
<div style="width:34px;height:34px;border-radius:50%;border:1px solid #5a5348;background:#2a251f;color:#9a8f78;font:600 9px system-ui;display:flex;align-items:center;justify-content:center">HU</div>
<div style="width:34px;height:34px;border-radius:50%;border:1px dashed #6b3b38;background:#2a1c1a;color:#b8524a;font:600 9px system-ui;display:flex;align-items:center;justify-content:center"></div>
</div>
</div>
<!-- minimap -->
<div style="position:absolute;top:14px;right:14px;width:104px;height:104px;background:rgba(18,15,12,.8);border:1px solid rgba(200,180,140,.2);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#7a7060;font:9px ui-monospace,monospace;letter-spacing:.1em">MINIMAP</div>
<!-- bottom bar within world -->
<div style="position:absolute;left:0;right:0;bottom:0;height:96px;background:rgba(10,8,6,.92);display:flex;align-items:center;gap:16px;padding:0 16px;box-sizing:border-box">
<div style="display:flex;flex-direction:column;gap:5px;width:150px">
<div><div style="font:8px ui-monospace,monospace;color:#caa39c;margin-bottom:2px">HP 42/60</div><div style="height:8px;border-radius:4px;background:#2a251f;overflow:hidden"><div style="width:70%;height:100%;background:#b8524a"></div></div></div>
<div><div style="font:8px ui-monospace,monospace;color:#9caccb;margin-bottom:2px">MP 18/30</div><div style="height:8px;border-radius:4px;background:#2a251f;overflow:hidden"><div style="width:60%;height:100%;background:#4f7ab8"></div></div></div>
<div style="display:flex;align-items:center;gap:6px;margin-top:2px"><span style="font:8px ui-monospace,monospace;color:#c9a86a">GOLD</span><span style="font:600 12px ui-monospace,monospace;color:#d9b46a">◈ 1,240 gp</span></div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;flex:1;justify-content:center">
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7)"></div>
</div>
</div>
</div>
<!-- RIGHT: narration book -->
<div style="width:366px;height:506px;background:#e7dcc4;border-left:3px solid #1a1512;box-sizing:border-box;padding:20px 22px;display:flex;flex-direction:column">
<div style="display:flex;align-items:baseline;justify-content:space-between;border-bottom:1.5px solid #bfa878;padding-bottom:8px;margin-bottom:12px">
<span style="font:600 13px 'Architects Daughter',cursive;letter-spacing:.06em;color:#5a4326">THE DUNGEON MASTER</span>
<span style="font:9px ui-monospace,monospace;color:#8a7748">ROUND 4</span>
</div>
<div style="font:14px/1.6 Georgia,serif;color:#4a3f2c;flex:1">
<p style="margin:0 0 12px">The alley stinks of piss and rot. Rain beads on the cobbles. Your left arm has gone hot to the touch — the bite from the sewers is turning.</p>
<p style="margin:0 0 12px">The beastfolk fence weighs your coin in one clawed hand, then looks at your throat.</p>
<p style="margin:0;padding:8px 10px;background:#d9c9a2;border-left:3px solid #8f3a34;color:#5a3230;font-style:italic">✗ Sleight of Hand failed. His grip closes on your wrist.</p>
</div>
<div style="border-top:1.5px solid #bfa878;padding-top:12px;margin-top:12px">
<div style="font:600 12px 'Architects Daughter',cursive;color:#5a4326;margin-bottom:8px">▸ Your turn. What do you do?</div>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:12px system-ui;color:#4a3f2c;background:#efe6cf">1 · Pay the fence double</div>
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:12px system-ui;color:#4a3f2c;background:#efe6cf">2 · Draw your blade</div>
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:12px system-ui;color:#4a3f2c;background:#efe6cf">3 · Lie about the coin</div>
</div>
</div>
</div>
</div>
</div>
<!-- ============ 1c : NARRATION-FORWARD BOTTOM BOX ============ -->
<div class="dv-opt" id="1c">
<div class="dv-olabel"><a class="dv-oid" href="#1c">1c</a><span><strong>Cinematic narration box.</strong> World up top, a wide DM text box across the bottom with inline choices — classic JRPG/CRPG framing. Balances world and story; hotbar tray straddles the seam.</span></div>
<div class="dv-card" style="width:900px;height:506px;position:relative;background:#0f0d0b">
<!-- world top -->
<div style="position:absolute;left:0;right:0;top:0;height:320px;background:repeating-linear-gradient(135deg,#26221c 0 12px,#221e18 12px 24px)">
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.14em">ISOMETRIC WORLD VIEW</div>
<!-- portrait + bars top-left -->
<div style="position:absolute;top:14px;left:14px;display:flex;gap:10px;background:rgba(14,11,9,.8);border:1px solid rgba(200,180,140,.16);border-radius:8px;padding:8px 10px">
<div style="width:42px;height:42px;border-radius:50%;border:2px solid #a9843f;background:#3a332a;color:#c9bda3;font:600 10px system-ui;display:flex;align-items:center;justify-content:center">EL</div>
<div style="display:flex;flex-direction:column;gap:4px;width:120px;justify-content:center">
<div style="height:7px;border-radius:4px;background:#2a251f;overflow:hidden"><div style="width:70%;height:100%;background:#b8524a"></div></div>
<div style="height:7px;border-radius:4px;background:#2a251f;overflow:hidden"><div style="width:60%;height:100%;background:#4f7ab8"></div></div>
<div style="height:7px;border-radius:4px;background:#2a251f;overflow:hidden"><div style="width:64%;height:100%;background:#8f9a4f"></div></div>
</div>
</div>
<div style="position:absolute;top:14px;right:14px;width:104px;height:104px;background:rgba(18,15,12,.8);border:1px solid rgba(200,180,140,.2);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#7a7060;font:9px ui-monospace,monospace;letter-spacing:.1em">MINIMAP</div>
</div>
<!-- hotbar tray straddling seam -->
<div style="position:absolute;left:50%;transform:translateX(-50%);top:294px;display:flex;gap:7px;background:rgba(10,8,6,.95);border:1px solid rgba(200,180,140,.18);border-radius:8px;padding:7px 9px">
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">1</span></div>
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">2</span></div>
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">3</span></div>
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">4</span></div>
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">5</span></div>
<div style="width:42px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:9px ui-monospace,monospace;color:#8a7f6c">6</span></div>
<div style="width:1px;background:#4a4238;margin:2px 3px"></div>
<div style="width:34px;height:42px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);display:flex;align-items:flex-end;justify-content:center;padding-bottom:3px;font:8px ui-monospace,monospace;color:#a9843f"></div>
</div>
<!-- narration box bottom -->
<div style="position:absolute;left:0;right:0;bottom:0;height:186px;background:#e7dcc4;border-top:3px solid #1a1512;box-sizing:border-box;padding:16px 22px;display:flex;gap:22px">
<div style="flex:1">
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:8px"><span style="font:600 12px 'Architects Daughter',cursive;letter-spacing:.06em;color:#5a4326">THE DUNGEON MASTER</span><span style="font:9px ui-monospace,monospace;color:#8a7748">ROUND 4</span></div>
<p style="margin:0;font:14px/1.55 Georgia,serif;color:#4a3f2c">The alley stinks of piss and rot. Your left arm's gone hot — the bite is turning. The beastfolk fence weighs your coin, then your throat. <span style="color:#8f3a34;font-style:italic">Your Sleight of Hand failed; his grip closes on your wrist.</span></p>
</div>
<div style="width:210px;display:flex;flex-direction:column;gap:6px;justify-content:center">
<div style="font:600 11px 'Architects Daughter',cursive;color:#5a4326">▸ What do you do?</div>
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:11.5px system-ui;color:#4a3f2c;background:#efe6cf">1 · Pay double</div>
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:11.5px system-ui;color:#4a3f2c;background:#efe6cf">2 · Draw your blade</div>
<div style="border:1px solid #8f7a4a;border-radius:5px;padding:6px 9px;font:11.5px system-ui;color:#4a3f2c;background:#efe6cf">3 · Lie about the coin</div>
</div>
</div>
</div>
</div>
<!-- ============ 1d : ORNATE GOTHIC FRAME ============ -->
<div class="dv-opt" id="1d">
<div class="dv-olabel"><a class="dv-oid" href="#1d">1d</a><span><strong>Ornate gothic frame.</strong> A carved border wraps the whole screen; stats and map sit in corner cartouches, narration in a footer ribbon. Most decorative — the "dark fantasy" end of the range.</span></div>
<div class="dv-card" style="width:900px;height:506px;position:relative;background:repeating-linear-gradient(135deg,#26221c 0 12px,#221e18 12px 24px)">
<!-- world label -->
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#6b6152;font:12px ui-monospace,monospace;letter-spacing:.14em">ISOMETRIC WORLD VIEW</div>
<!-- ornate frame -->
<div style="position:absolute;inset:0;border:10px solid #1a1512;box-sizing:border-box;pointer-events:none"></div>
<div style="position:absolute;inset:10px;border:1.5px solid #a9843f;box-sizing:border-box;pointer-events:none;opacity:.55"></div>
<!-- corner diamonds -->
<div style="position:absolute;top:4px;left:4px;width:16px;height:16px;background:#a9843f;transform:rotate(45deg)"></div>
<div style="position:absolute;top:4px;right:4px;width:16px;height:16px;background:#a9843f;transform:rotate(45deg)"></div>
<div style="position:absolute;bottom:4px;left:4px;width:16px;height:16px;background:#a9843f;transform:rotate(45deg)"></div>
<div style="position:absolute;bottom:4px;right:4px;width:16px;height:16px;background:#a9843f;transform:rotate(45deg)"></div>
<!-- top-left cartouche: portrait + orbs -->
<div style="position:absolute;top:22px;left:22px;display:flex;align-items:center;gap:10px;background:rgba(14,11,9,.85);border:1.5px solid #a9843f;border-radius:10px;padding:9px 12px">
<div style="width:46px;height:46px;border-radius:50%;border:2px solid #a9843f;background:#3a332a;color:#c9bda3;font:600 11px system-ui;display:flex;align-items:center;justify-content:center">EL</div>
<div style="display:flex;gap:8px">
<div style="text-align:center"><div style="width:26px;height:26px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#b8524a,#6f2620);border:1.5px solid #1a1512"></div><div style="font:7px ui-monospace,monospace;color:#caa39c;margin-top:2px">HP</div></div>
<div style="text-align:center"><div style="width:26px;height:26px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#4f7ab8,#243f6f);border:1.5px solid #1a1512"></div><div style="font:7px ui-monospace,monospace;color:#9caccb;margin-top:2px">MP</div></div>
<div style="text-align:center"><div style="width:26px;height:26px;border-radius:50%;background:radial-gradient(circle at 38% 32%,#9aa85a,#4f5926);border:1.5px solid #1a1512"></div><div style="font:7px ui-monospace,monospace;color:#b6bd8e;margin-top:2px">ST</div></div>
</div>
</div>
<!-- top-right cartouche: minimap -->
<div style="position:absolute;top:22px;right:22px;width:112px;height:112px;background:rgba(14,11,9,.85);border:1.5px solid #a9843f;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#7a7060;font:9px ui-monospace,monospace;letter-spacing:.1em">MINIMAP</div>
<!-- footer narration ribbon -->
<div style="position:absolute;left:26px;right:26px;bottom:74px;background:rgba(231,220,196,.96);border:1.5px solid #1a1512;border-radius:6px;padding:10px 16px;display:flex;align-items:center;gap:14px">
<span style="font:600 11px 'Architects Daughter',cursive;letter-spacing:.06em;color:#5a4326;white-space:nowrap">THE DM ▸</span>
<span style="font:13px/1.45 Georgia,serif;color:#4a3f2c">The alley stinks of piss and rot; your arm's gone hot with fever. The fence weighs your coin, then your throat. <span style="color:#8f3a34;font-style:italic">Sleight of Hand failed.</span></span>
</div>
<!-- ornate hotbar tray bottom center -->
<div style="position:absolute;left:50%;transform:translateX(-50%);bottom:20px;display:flex;align-items:center;gap:8px">
<div style="width:12px;height:12px;background:#a9843f;transform:rotate(45deg)"></div>
<div style="display:flex;gap:6px;background:rgba(14,11,9,.9);border:1.5px solid #a9843f;border-radius:8px;padding:6px 8px">
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">1</span></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">2</span></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">3</span></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">4</span></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">5</span></div>
<div style="width:40px;height:40px;border:1px solid #6b6152;border-radius:5px;background:rgba(40,34,28,.7);position:relative"><span style="position:absolute;top:2px;left:4px;font:8px ui-monospace,monospace;color:#8a7f6c">6</span></div>
</div>
<div style="width:12px;height:12px;background:#a9843f;transform:rotate(45deg)"></div>
</div>
</div>
</div>
</div>
<p class="dv-next">Try next: "expand <a class="dv-oid" href="#1b">1b</a> to full 1920×1080 detail" · "combine <a class="dv-oid" href="#1a">1a</a>'s orbs with <a class="dv-oid" href="#1b">1b</a>'s narration column" · "sketch the character-creation screen" · "sketch the inventory / character hub tabs" · "make <a class="dv-oid" href="#1d">1d</a> less ornate"</p>
</section>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
state = { dockOpen: true };
renderVals() {
const open = this.state.dockOpen;
return {
dockShift: open ? 'translateY(-50%) translateX(0)' : 'translateY(-50%) translateX(230px)',
dockOpacity: open ? '1' : '0',
handleArrow: open ? '' : '',
toggleDock: () => this.setState(s => ({ dockOpen: !s.dockOpen })),
};
}
}
</script>
</body>
</html>

204
mockups/Pause Menu.dc.html Normal file
View File

@@ -0,0 +1,204 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
input[type=range]{-webkit-appearance:none;appearance:none;width:230px;height:6px;border-radius:4px;background:#cbb684;outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:#8f6a2a;border:2px solid #5a4326;cursor:pointer}
input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#8f6a2a;border:2px solid #5a4326;cursor:pointer}
select{font-family:ui-monospace,monospace;font-size:13px;color:#3a2f1c;background:#f6efdd;border:1px solid #a99464;border-radius:8px;padding:9px 12px;outline:none;cursor:pointer;min-width:200px}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex;align-items:center;justify-content:center;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px)">
<!-- dim -->
<div style="position:absolute;inset:0;background:rgba(8,6,4,.72);backdrop-filter:blur(2px)"></div>
<!-- panel -->
<div style="position:relative;width:1560px;height:920px;display:flex;border:8px solid #1a1512;border-radius:16px;box-shadow:0 30px 80px rgba(0,0,0,.6);overflow:hidden">
<div style="position:absolute;inset:8px;border:1.5px solid #a9843f;border-radius:9px;pointer-events:none;opacity:.5;z-index:2"></div>
<!-- ===== LEFT RAIL ===== -->
<div style="width:400px;flex:none;background:repeating-linear-gradient(135deg,#211d17 0 16px,#1d1914 16px 32px);box-sizing:border-box;padding:44px 38px;display:flex;flex-direction:column;position:relative">
<div style="position:absolute;inset:0;background:radial-gradient(120% 50% at 50% 0%,rgba(0,0,0,0) 50%,rgba(0,0,0,.5) 100%);pointer-events:none"></div>
<div style="position:relative">
<div style="font:13px ui-monospace,monospace;letter-spacing:.24em;color:#8a7f6c">THE WORLD WAITS</div>
<div style="font:600 54px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1;margin-top:6px">Paused</div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.06em;color:#a9843f;margin-top:10px">CHAPTER II · THE LOWER WARD</div>
</div>
<div style="position:relative;display:flex;flex-direction:column;gap:12px;margin-top:44px">
<div style="padding:16px 20px;border:1.5px solid #8f3a34;border-radius:10px;background:linear-gradient(180deg,#a5423a,#7a2f29);color:#f4e6cf;font:600 18px system-ui;letter-spacing:.04em;cursor:pointer" style-hover="background:linear-gradient(180deg,#b8524a,#8f3a34)">▸ Resume</div>
<div style="padding:15px 20px;border:1px solid #4a4238;border-radius:10px;background:rgba(30,26,20,.7);color:#d8ccb2;font:600 16px system-ui;cursor:pointer" style-hover="border-color:#a9843f">Save Game</div>
<div style="padding:15px 20px;border:1px solid #4a4238;border-radius:10px;background:rgba(30,26,20,.7);color:#d8ccb2;font:600 16px system-ui;cursor:pointer" style-hover="border-color:#a9843f">Load Game</div>
<div style="padding:15px 20px;border:1px solid #4a4238;border-radius:10px;background:rgba(30,26,20,.7);color:#caa39c;font:600 16px system-ui;cursor:pointer" style-hover="border-color:#8f3a34">Quit to Title</div>
</div>
<div style="position:relative;margin-top:auto;font:12px ui-monospace,monospace;line-height:1.9;color:#7a7060">
<div>PLAYTIME · <span style="color:#c9bda3">14:22:07</span></div>
<div>AUTOSAVED · <span style="color:#c9bda3">6 min ago</span></div>
<div style="color:#8f6a5a;margin-top:6px;font-family:'Architects Daughter',cursive;font-size:13px">Permadeath is on. Be careful out there.</div>
</div>
</div>
<!-- ===== RIGHT: SETTINGS ===== -->
<div style="flex:1;min-width:0;background:linear-gradient(180deg,#ece2ca,#e4d8bd);box-sizing:border-box;padding:38px 46px;display:flex;flex-direction:column;gap:22px">
<div style="display:flex;align-items:flex-end;justify-content:space-between;border-bottom:2px solid #bfa878;padding-bottom:16px">
<div style="font:600 30px 'Architects Daughter',cursive;letter-spacing:.04em;color:#5a4326">Settings</div>
<div style="font:11px ui-monospace,monospace;color:#a99464;border:1px solid #cbb684;border-radius:6px;padding:6px 10px;cursor:pointer" style-hover="background:#e6d9b8">RESTORE DEFAULTS</div>
</div>
<!-- tabs -->
<div style="display:flex;gap:8px">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="4">
<div onClick="{{ t.onSelect }}" style="font:12px ui-monospace,monospace;letter-spacing:.1em;padding:9px 18px;border-radius:8px;border:1px solid {{ t.border }};background:{{ t.bg }};color:{{ t.fg }};cursor:pointer">{{ t.label }}</div>
</sc-for>
</div>
<!-- controls -->
<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column">
<sc-for list="{{ controls }}" as="c" hint-placeholder-count="6">
<div style="display:flex;align-items:center;gap:20px;padding:16px 4px;border-bottom:1px solid #d9cba6">
<div style="flex:1;min-width:0">
<div style="font:600 17px system-ui;color:#3a2f1c">{{ c.label }}</div>
<sc-if value="{{ c.hasDesc }}" hint-placeholder-val="{{ false }}"><div style="font:13px/1.4 Georgia,serif;font-style:italic;color:#7a684a;margin-top:2px">{{ c.desc }}</div></sc-if>
</div>
<div style="flex:none;display:flex;align-items:center;gap:14px">
<sc-if value="{{ c.isSlider }}" hint-placeholder-val="{{ false }}">
<span style="font:600 14px ui-monospace,monospace;color:#8f6a2a;width:52px;text-align:right">{{ c.valueLabel }}</span>
<input type="range" min="{{ c.min }}" max="{{ c.max }}" step="1" value="{{ c.def }}" onInput="{{ c.onChange }}" />
</sc-if>
<sc-if value="{{ c.isToggle }}" hint-placeholder-val="{{ false }}">
<div onClick="{{ c.onToggle }}" style="width:50px;height:28px;border-radius:15px;background:{{ c.trackBg }};border:1px solid #8a7748;position:relative;cursor:pointer;transition:background .15s"><div style="position:absolute;top:2px;left:2px;width:22px;height:22px;border-radius:50%;background:#f6efdd;box-shadow:0 1px 2px rgba(0,0,0,.3);transform:{{ c.knobShift }};transition:transform .15s"></div></div>
</sc-if>
<sc-if value="{{ c.isSelect }}" hint-placeholder-val="{{ false }}">
<select onChange="{{ c.onChange }}" value="{{ c.current }}">
<sc-for list="{{ c.options }}" as="o" hint-placeholder-count="3">
<option value="{{ o.value }}">{{ o.label }}</option>
</sc-for>
</select>
</sc-if>
<sc-if value="{{ c.isKeybind }}" hint-placeholder-val="{{ false }}">
<div style="display:flex;gap:6px">
<sc-for list="{{ c.keys }}" as="k" hint-placeholder-count="1">
<span style="font:600 13px ui-monospace,monospace;color:#4a3f2c;background:#f6efdd;border:1px solid #a99464;border-bottom-width:3px;border-radius:6px;padding:6px 11px;min-width:14px;text-align:center">{{ k }}</span>
</sc-for>
</div>
</sc-if>
</div>
</div>
</sc-for>
</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 {
state = {
tab: 'gameplay',
s: {
master: 80, music: 60, sfx: 75, ambience: 50, subtitles: true,
windowMode: 'fullscreen', resolution: '1920x1080', brightness: 55, motionBlur: false, vsync: true, damageFlash: true,
difficulty: 'bastard', permadeath: true, tutorial: true, gore: true, textSpeed: 70, autosave: 'camp',
mouseSens: 50, invertY: false,
},
};
set(key, val) { this.setState(st => ({ s: { ...st.s, [key]: val } })); }
slider(key, label, desc) {
return {
isSlider: true, label, desc: desc || '', hasDesc: !!desc,
min: 0, max: 100, def: this.state.s[key],
valueLabel: this.state.s[key] + '%',
onChange: (e) => this.set(key, +e.target.value),
};
}
toggle(key, label, desc) {
const on = this.state.s[key];
return {
isToggle: true, label, desc: desc || '', hasDesc: !!desc,
trackBg: on ? '#8f6a2a' : '#c9b78a',
knobShift: on ? 'translateX(22px)' : 'translateX(0)',
onToggle: () => this.set(key, !on),
};
}
select(key, label, options, desc) {
return {
isSelect: true, label, desc: desc || '', hasDesc: !!desc,
current: this.state.s[key],
options: options.map(o => ({ value: o[0], label: o[1] })),
onChange: (e) => this.set(key, e.target.value),
};
}
keybind(label, keys) { return { isKeybind: true, label, hasDesc: false, keys }; }
renderVals() {
const tab = this.state.tab;
const tabs = [
{ id: 'gameplay', label: 'GAMEPLAY' },
{ id: 'video', label: 'VIDEO' },
{ id: 'audio', label: 'AUDIO' },
{ id: 'controls', label: 'CONTROLS' },
].map(t => ({
label: t.label,
onSelect: () => this.setState({ tab: t.id }),
bg: t.id === tab ? '#8f3a34' : '#efe6cf',
fg: t.id === tab ? '#f1e8d2' : '#6a5a3a',
border: t.id === tab ? '#8f3a34' : '#cbb684',
}));
let controls = [];
if (tab === 'gameplay') {
controls = [
this.select('difficulty', 'Difficulty', [['cutthroat', 'Cutthroat — story'], ['bastard', 'Bastard — normal'], ['nightmare', 'Nightmare — hard'], ['ironblood', 'Ironblood — brutal']]),
this.toggle('permadeath', 'Permadeath', 'When you die, the save dies with you.'),
this.toggle('gore', 'Gore & Wounds', 'Blood, infection, and the state bodies get into.'),
this.toggle('tutorial', 'Tutorial Hints', 'The DM explains the rules as you go.'),
this.slider('textSpeed', 'Narration Speed', 'How fast the DM writes the world.'),
this.select('autosave', 'Autosave', [['scene', 'Every scene'], ['camp', 'At camp only'], ['manual', 'Manual only']]),
];
} else if (tab === 'video') {
controls = [
this.select('windowMode', 'Window Mode', [['fullscreen', 'Fullscreen'], ['borderless', 'Borderless'], ['windowed', 'Windowed']]),
this.select('resolution', 'Resolution', [['2560x1440', '2560 × 1440'], ['1920x1080', '1920 × 1080'], ['1600x900', '1600 × 900'], ['1280x720', '1280 × 720']]),
this.slider('brightness', 'Brightness', 'The Margreave is meant to be dark. A little.'),
this.toggle('vsync', 'VSync', ''),
this.toggle('motionBlur', 'Motion Blur', ''),
this.toggle('damageFlash', 'Damage Flash', 'The screen reddens when you take a hit.'),
];
} else if (tab === 'audio') {
controls = [
this.slider('master', 'Master Volume', ''),
this.slider('music', 'Music', ''),
this.slider('sfx', 'Effects', ''),
this.slider('ambience', 'Ambience', 'Rain, rats, and distant screaming.'),
this.toggle('subtitles', 'Subtitles', 'Show the DM\u2019s narration as text.'),
];
} else {
controls = [
this.keybind('Move', ['W', 'A', 'S', 'D']),
this.keybind('Interact', ['E']),
this.keybind('Attack', ['LMB']),
this.keybind('End Turn', ['↵']),
this.keybind('Inventory', ['I']),
this.keybind('Character', ['C']),
this.keybind('Quest Log', ['J']),
this.keybind('World Map', ['M']),
this.slider('mouseSens', 'Mouse Sensitivity', ''),
this.toggle('invertY', 'Invert Y-Axis', ''),
];
}
return { tabs, controls };
}
}
</script>
</body>
</html>

264
mockups/Quest Log.dc.html Normal file
View File

@@ -0,0 +1,264 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ============ LEFT: QUEST LIST ============ -->
<div style="width:620px;height:1080px;position:relative;overflow:hidden;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);display:flex;flex-direction:column">
<div style="position:absolute;inset:0;background:radial-gradient(120% 60% at 50% 10%,rgba(0,0,0,0) 45%,rgba(0,0,0,.45) 100%);pointer-events:none"></div>
<!-- header -->
<div style="position:relative;padding:28px 32px 0">
<div style="font:13px ui-monospace,monospace;letter-spacing:.2em;color:#8a7f6c">JOURNAL</div>
<div style="font:600 34px 'Architects Daughter',cursive;color:#e8d9b6;margin-top:4px;line-height:1">Deeds &amp; Debts</div>
</div>
<!-- tabs -->
<div style="position:relative;display:flex;gap:8px;padding:20px 32px 16px">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="3">
<div onClick="{{ t.onSelect }}" style="font:12px ui-monospace,monospace;letter-spacing:.06em;padding:8px 14px;border-radius:8px;border:1px solid {{ t.border }};background:{{ t.bg }};color:{{ t.fg }};cursor:pointer">{{ t.label }}</div>
</sc-for>
</div>
<!-- list -->
<div style="position:relative;flex:1;overflow-y:auto;padding:0 24px 24px;display:flex;flex-direction:column;gap:10px">
<sc-for list="{{ list }}" as="q" hint-placeholder-count="4">
<div onClick="{{ q.onSelect }}" style="position:relative;background:{{ q.bg }};border:1px solid {{ q.border }};border-left:4px solid {{ q.typeColor }};border-radius:10px;padding:14px 16px;cursor:pointer;display:flex;flex-direction:column;gap:6px" style-hover="border-color:#a9843f">
<div style="display:flex;align-items:center;gap:9px">
<span style="font:600 17px system-ui;color:#e8d9b6;flex:1;min-width:0">{{ q.title }}</span>
<sc-if value="{{ q.tracked }}" hint-placeholder-val="{{ false }}"><span style="color:#c8963f;font-size:15px"></span></sc-if>
</div>
<div style="display:flex;align-items:center;gap:10px;font:11px ui-monospace,monospace;color:#9a8f78">
<span style="color:{{ q.typeColor }};letter-spacing:.06em">{{ q.typeLabel }}</span>
<span>·</span>
<span>{{ q.loc }}</span>
<span style="margin-left:auto;color:#c9bda3">{{ q.progressText }}</span>
</div>
</div>
</sc-for>
</div>
</div>
<!-- ============ RIGHT: QUEST DETAIL (open page) ============ -->
<div style="width:1300px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);border-left:4px solid #1a1512;box-sizing:border-box;padding:44px 56px;display:flex;flex-direction:column;gap:24px;position:relative;overflow:hidden">
<!-- status stamp -->
<sc-if value="{{ isCompleted }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;top:70px;right:60px;transform:rotate(-11deg);border:4px solid #5f8a4f;color:#5f8a4f;font:800 30px ui-monospace,monospace;letter-spacing:.1em;padding:8px 20px;border-radius:8px;opacity:.72">COMPLETED</div>
</sc-if>
<sc-if value="{{ isFailed }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;top:70px;right:60px;transform:rotate(-11deg);border:4px solid #8f3a34;color:#8f3a34;font:800 34px ui-monospace,monospace;letter-spacing:.12em;padding:8px 22px;border-radius:8px;opacity:.72">FAILED</div>
</sc-if>
<!-- title block -->
<div style="border-bottom:2px solid #bfa878;padding-bottom:20px">
<div style="font:12px ui-monospace,monospace;letter-spacing:.16em;color:{{ selTypeColor }}">{{ selTypeLabel }}</div>
<div style="font:600 46px 'Architects Daughter',cursive;color:#4a3520;line-height:1.05;margin-top:6px">{{ selTitle }}</div>
<div style="display:flex;gap:22px;margin-top:14px;font:13px ui-monospace,monospace;color:#8a7748">
<span>GIVEN BY · <span style="color:#5a4326">{{ selGiver }}</span></span>
<span>WHERE · <span style="color:#5a4326">{{ selLoc }}</span></span>
</div>
</div>
<!-- description -->
<div style="font:19px/1.65 Georgia,serif;color:#4a3f2c;max-width:820px">{{ selDesc }}</div>
<!-- objectives -->
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:14px">OBJECTIVES</div>
<div style="display:flex;flex-direction:column;gap:11px">
<sc-for list="{{ objectives }}" as="o" hint-placeholder-count="4">
<div style="display:flex;align-items:center;gap:13px">
<span style="font:19px;color:{{ o.boxColor }};width:22px">{{ o.box }}</span>
<span style="font:17px system-ui;color:{{ o.textColor }};{{ o.strike }}">{{ o.text }}</span>
<sc-if value="{{ o.current }}" hint-placeholder-val="{{ false }}"><span style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8f3a34;border:1px solid #8f3a34;border-radius:5px;padding:2px 7px">CURRENT</span></sc-if>
</div>
</sc-for>
</div>
</div>
<!-- rewards + actions -->
<div style="margin-top:auto;display:flex;align-items:flex-end;justify-content:space-between;border-top:2px solid #bfa878;padding-top:22px">
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.16em;color:#8a7748;margin-bottom:12px">ON COMPLETION</div>
<div style="display:flex;gap:12px;align-items:center">
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:9px;padding:10px 16px;text-align:center"><div style="font:9px ui-monospace,monospace;color:#8a7748">GOLD</div><div style="font:600 20px ui-monospace,monospace;color:#8f6a2a">◈ {{ rewardGold }}</div></div>
<div style="background:#f1e8d2;border:1px solid #bfa878;border-radius:9px;padding:10px 16px;text-align:center"><div style="font:9px ui-monospace,monospace;color:#8a7748">XP</div><div style="font:600 20px ui-monospace,monospace;color:#5a4326">{{ rewardXp }}</div></div>
<sc-for list="{{ rewardItems }}" as="ri" hint-placeholder-count="1">
<div style="background:#f1e8d2;border:1px solid #c8963f;border-radius:9px;padding:10px 16px;text-align:center"><div style="font:9px ui-monospace,monospace;color:#8a7748">ITEM</div><div style="font:600 15px 'Architects Daughter',cursive;color:#8f6a2a">{{ ri }}</div></div>
</sc-for>
</div>
</div>
<sc-if value="{{ isActive }}" hint-placeholder-val="{{ true }}">
<div style="display:flex;gap:10px">
<div onClick="{{ onTrack }}" style="padding:14px 22px;border:1.5px solid {{ trackBorder }};border-radius:10px;background:{{ trackBg }};color:{{ trackFg }};font:600 15px system-ui;letter-spacing:.04em;cursor:pointer">{{ trackLabel }}</div>
<div style="padding:14px 22px;border:1px solid #a99464;border-radius:10px;background:#e6d9b8;color:#7a5a3a;font:600 15px system-ui;letter-spacing:.04em;cursor:pointer" style-hover="background:#d9c9a2">Abandon</div>
</div>
</sc-if>
</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 {
TYPE = {
main: { label: 'MAIN QUEST', color: '#c8963f' },
side: { label: 'SIDE QUEST', color: '#7a9a5a' },
bounty: { label: 'BOUNTY', color: '#a5442e' },
};
questData() {
return [
{
id: 'femur', title: "The Saint's Femur", type: 'main', status: 'active',
giver: 'The Bonewright', loc: 'Barrow of the Ninth',
desc: 'The Bonewright wants the hanged saint\u2019s left femur, and does not care to say why. The grave-wards are failing, and something in the dark is glad of it.',
objectives: [
{ text: 'Reach the Barrow of the Ninth', done: true },
{ text: 'Break 3 of the 5 grave-wards', done: true },
{ text: "Recover the hanged saint's femur", done: false },
{ text: 'Return to the Bonewright — alive', done: false },
],
gold: 250, xp: 400, items: ['Ward-Charm'],
},
{
id: 'crying', title: "Nobody's Crying", type: 'side', status: 'active',
giver: 'A nailed-up notice', loc: 'The Weeping Mire',
desc: 'The town\u2019s tax-collector went into the Mire and never came out. The reward is small. The relief in town is not.',
objectives: [
{ text: 'Find the missing tax-collector', done: false },
{ text: 'Learn what dragged him under', done: false },
],
gold: 80, xp: 150, items: [],
},
{
id: 'bell', title: 'Make the Bell Stop', type: 'side', status: 'active',
giver: 'Overheard in Saltcrol', loc: 'Ashfell',
desc: 'A bell still rings in the burned city, though no one living should be there to ring it. It rings at dusk. People have started to count on it, which is worse.',
objectives: [
{ text: 'Reach the bell tower in Ashfell', done: false },
{ text: 'Silence whatever rings it', done: false },
],
gold: 120, xp: 200, items: [],
},
{
id: 'spider', title: 'The Red Spider', type: 'bounty', status: 'active',
giver: 'Harbor-master, Saltcrol', loc: 'Redwidow Camp',
desc: 'The Redwidows brand a red spider on everyone they rob. The harbor-master wants their branding iron brought back. He did not ask for it clean.',
objectives: [
{ text: 'Identify the Redwidow leader', done: true },
{ text: 'Bring back their branding iron', done: false },
],
gold: 300, xp: 250, items: ['Harbor Writ'],
},
{
id: 'debt', title: 'A Debt in Saltcrol', type: 'main', status: 'completed',
giver: 'Mother Cael', loc: 'Saltcrol Harbor',
desc: 'You paid off the debt that followed you out of the last city. Mostly in coin. Partly in blood.',
objectives: [
{ text: 'Deliver the strongbox to Mother Cael', done: true },
{ text: 'Survive the double-cross', done: true },
],
gold: 200, xp: 350, items: [],
},
{
id: 'rats', title: 'Rats, Obviously', type: 'side', status: 'completed',
giver: 'The Gutted Cod tavern', loc: 'Gallowsmoor',
desc: 'It was rats. It is always rats. These ones were the size of dogs, but still.',
objectives: [{ text: 'Clear the cellar', done: true }],
gold: 25, xp: 60, items: [],
},
{
id: 'pilgrim', title: 'Escort the Pilgrim', type: 'side', status: 'failed',
giver: 'The Ninth Order', loc: 'The Gibbet Road',
desc: 'The pilgrim died on the Gibbet Road. You lived. The Order remembers which of you it paid for.',
objectives: [
{ text: 'Escort the pilgrim to the barrow', done: false },
{ text: 'Keep the pilgrim alive', done: false },
],
gold: 0, xp: 0, items: [],
},
];
}
state = { sel: 'femur', tab: 'active', tracked: 'femur' };
renderVals() {
const quests = this.questData();
const byId = {}; quests.forEach(q => byId[q.id] = q);
const tabDefs = [
{ id: 'active', label: 'ACTIVE' },
{ id: 'completed', label: 'COMPLETED' },
{ id: 'failed', label: 'FAILED' },
];
const tab = this.state.tab;
const tabs = tabDefs.map(t => {
const n = quests.filter(q => q.status === t.id).length;
return {
label: t.label + ' (' + n + ')',
onSelect: () => { const first = quests.find(q => q.status === t.id); this.setState({ tab: t.id, sel: first ? first.id : this.state.sel }); },
bg: t.id === tab ? '#8f3a34' : 'rgba(18,15,11,.6)',
fg: t.id === tab ? '#f4e6cf' : '#9a8f78',
border: t.id === tab ? '#8f3a34' : '#3a352e',
};
});
const list = quests.filter(q => q.status === tab).map(q => {
const doneN = q.objectives.filter(o => o.done).length;
const ty = this.TYPE[q.type];
return {
title: q.title, typeLabel: ty.label, typeColor: ty.color, loc: q.loc,
progressText: q.status === 'active' ? (doneN + '/' + q.objectives.length) : (q.status === 'completed' ? '✓' : '✗'),
tracked: q.id === this.state.tracked,
onSelect: () => this.setState({ sel: q.id }),
bg: q.id === this.state.sel ? 'rgba(143,58,52,.18)' : 'rgba(18,15,11,.55)',
border: q.id === this.state.sel ? '#a9843f' : '#3a352e',
};
});
const s = byId[this.state.sel];
const ty = this.TYPE[s.type];
let currentSet = false;
const objectives = s.objectives.map(o => {
const isCurrent = s.status === 'active' && !o.done && !currentSet;
if (isCurrent) currentSet = true;
return {
text: o.text,
box: o.done ? '☑' : '☐',
boxColor: o.done ? '#5f8a4f' : (isCurrent ? '#8f3a34' : '#a99464'),
textColor: o.done ? '#8a7c5a' : '#3a2f1c',
strike: o.done ? 'text-decoration:line-through' : '',
current: isCurrent,
};
});
const tracked = s.id === this.state.tracked;
return {
tabs, list,
selTitle: s.title, selTypeLabel: ty.label, selTypeColor: ty.color,
selGiver: s.giver, selLoc: s.loc, selDesc: s.desc,
objectives,
rewardGold: s.gold, rewardXp: s.xp, rewardItems: s.items,
isActive: s.status === 'active', isCompleted: s.status === 'completed', isFailed: s.status === 'failed',
onTrack: () => this.setState({ tracked: tracked ? null : s.id }),
trackLabel: tracked ? '★ Tracked' : '☆ Track',
trackBg: tracked ? 'linear-gradient(180deg,#c8963f,#a5772a)' : '#e6d9b8',
trackFg: tracked ? '#2a2015' : '#7a5a3a',
trackBorder: tracked ? '#a5772a' : '#a99464',
};
}
}
</script>
</body>
</html>

210
mockups/README.md Normal file
View File

@@ -0,0 +1,210 @@
# Handoff: Code of Conquest — Game UI
## Overview
**Code of Conquest** is a single-player, **top-down / isometric, turn-based RPG** in a gritty high-fantasy world ("The Margreave"). The party is the player character plus **AI-controlled companions** (NPCs); the story is delivered turn-by-turn by a **"Dungeon Master" (DM)** narrator, in the spirit of a tabletop D&D session. Target platform: **Godot 4.7**. Target render resolution: **1920×1080** (16:9).
This bundle contains **11 UI screens** designed as a single, consistent visual system. They are meant to be translated into **Godot 4.7 scenes** (Control-node UI).
**Tone — read this first.** The whole game is written and styled to be **"gritty, not grim."** The reference is Batman/DC comics, *not* Warhammer and *not* Care Bears. People get hangovers, wounds get infected, people curse and bleed — but it is not nihilistic. All placeholder copy in these files reflects that tone and can be reused or rewritten in the same voice. Recurring tagline: *"The world has real problems, and it does not care about you."*
---
## About the Design Files
The files here are **design references authored in HTML/CSS/JS** — high-fidelity prototypes showing the intended look, layout, and behavior. **They are not production code to ship.** The task is to **recreate each screen as a Godot 4.7 scene** using Godot's own UI system (Control nodes, Theme resources, `TextureRect`/`NinePatchRect`, `Label`/`RichTextLabel`, `Container` nodes) and to port the interaction logic to **GDScript**. Where these mocks use placeholder boxes (dashed rectangles labeled "PORTRAIT", "KEY ART", "ISOMETRIC WORLD VIEW", etc.), those are **art slots** to be filled with real game art.
### How the files are built (and how to view them)
Each screen is a **`.dc.html` component**: an HTML file with a template plus a small JavaScript logic class (a React-like `Component` with `state` + a `renderVals()` method). They depend on the runtime **`support.js`** included in this folder. To preview a screen, serve this folder over a local static server (e.g. `python3 -m http.server`) and open the `.dc.html` file — opening via `file://` may not load `support.js`.
**You do not need to run them to implement them.** Each `.dc.html` is readable as plain source:
- The **template** section = the markup + inline styles (all styling is inline; there are no external stylesheets). This gives you exact layout, sizes, and colors.
- The **`class Component extends DCLogic` / `renderVals()`** section = the **state model and interaction logic** (initiative order, point-buy math, shop economy, dialogue tree, etc.). Port this logic to GDScript.
### Fidelity
**High-fidelity.** Colors, typography, spacing, and interactions are final-intent. Recreate the UI faithfully, then swap placeholder art boxes for real assets. One exception: **`Main Window + Wireframes.dc.html`** contains *both* the hi-fi main HUD (top, labeled option "2a") *and* four earlier low-fi wireframe explorations (below it) kept for context — build from **2a**.
---
## Global Design System
### Canvas
- Every screen is a fixed **1920×1080** stage. Design for a 16:9 reference resolution and anchor/scale from there.
- Overlays (Pause Menu) dim + blur the scene behind them.
### The core layout motif — "Dark Bay + Parchment"
Almost every screen is a **two-panel split**:
- **Dark "bay"** (left on most screens; right on Dialogue/Shop): a textured near-black panel for portraits, the world/battlefield, a merchant, or an NPC. Built from a **diagonal stripe** `repeating-linear-gradient(135deg, #26221c 0, #26221c 16px, #221e18 16px, #221e18 32px)` plus a **radial vignette** overlay `radial-gradient(120% 70% at 50% 38%, transparent 3245%, rgba(0,0,0,.5.62) 100%)`.
- **Parchment "sheet/journal/page"** (the content side): vertical gradient `linear-gradient(180deg, #ece2ca, #e4d8bd)`, with content cards in `#f1e8d2` bordered `#bfa878`.
- The two panels are divided by a **4px `#1a1512` border** (`border-left`).
Keep this split and these two textures consistent — it's the game's UI identity.
### Color tokens (exact hex)
**Backgrounds / dark**
- Near-black stage: `#0a0806`, `#0f0d0b`
- Dark bay stripe: `#26221c` / `#221e18` (also seen `#201a14`/`#1a150f`, `#211d17`/`#1d1914`)
- Dark translucent panels: `rgba(18,15,11,.7)`, `rgba(14,11,9,.85)`, `rgba(10,8,6,.9)`
- Dark borders (light→dark): `#6b6152`, `#5a5348`, `#4a4238`, `#3a352e`, `#1a1512`
**Parchment**
- Sheet gradient: `#ece2ca``#e4d8bd`; log/page variants `#e7dcc4``#ddd0b2`, `#ece2ca``#e4d8bd`
- Cards: `#f1e8d2`; darker insets `#e6d9b8`, `#e4d6b6`, `#d9c9a2`, `#d3c39a`
- Parchment borders: `#bfa878`, `#cbb684`, `#a99464`, `#8f7a4a`
- Ink text: headings `#3a2f1c` / `#4a3520`, body `#4a3f2c`, muted `#6a5a3a` / `#7a684a`, labels `#5a4326`
- Parchment gold-ink accents: `#8f6a2a`, `#8a7748`, `#a9885a`
**Text on dark**
- Bone/cream (primary): `#e8d9b6`, `#e8ddc8`, `#f0e6d0`, `#f4e6cf`
- Secondary: `#c9bda3`, `#d8ccb2`
- Muted mono: `#9a8f78`, `#8a7f6c`, `#7a7060`, `#5a5040`
**Accent hues (all deliberately muted / desaturated for grit)**
- **Blood red** (primary CTA, danger, selection, enemies): `#8f3a34` (core), `#a5423a`, `#7a2f29`, `#b8524a`, `#a5442e`, `#6f2620`
- **Gold / brass** (highlights, currency, primary-stat, "current turn"): `#a9843f`, `#c8963f`, `#8f6a2a`, `#a5772a`, `#c9a45a`, `#d9b46a`
- **Steel blue-teal** (allies, mana): `#5f8a9a`, `#4f7ab8`, `#243f6f`, `#9caccb`
- **Green** (stamina, success, "cleared"): `#5f8a4f`, `#8f9a4f`, `#7a9a5a`, `#2f7a4a`
**Stat bars**
- HP: `linear-gradient(90deg, #b8524a, #8f3a34)` on track `#2a251f` border `#4a4238`
- MP: `linear-gradient(90deg, #4f7ab8, #243f6f)`
- Stamina: `#8f9a4f`
**Rarity (items)** — colored 3px bar at the bottom of each item tile + used for item-name text:
- common `#8a8378` · uncommon `#5f8a4f` · rare `#4f7ab8` · epic `#8a5fb8` · legendary `#c8963f`
### Typography
Three families, used consistently:
1. **Georgia** (serif) — headings, the logo (bold up to 116px), and all **narrative/DM/flavor body text** (often *italic*). This is the "prose" voice.
2. **Architects Daughter** (Google Font, handwritten) — accent display type: nameplates, big stat numbers, section flourishes, DM-header labels, hand-inked feel. Load it, or substitute a comparable hand-drawn font in Godot.
3. **Monospace** (`ui-monospace`/system mono) — all **UI chrome**: labels, keybinds, numeric readouts, tags/chips. Usually **UPPERCASE** with `letter-spacing` `0.060.24em`.
- Plain **system-ui sans** is used for button labels and item names.
### Reusable patterns (build these as shared Godot components/Theme styles)
- **Item tile**: rounded square, 2px rarity-colored border on filled slots, a short mono glyph/abbreviation centered (placeholder for an icon), a 3px rarity bar near the bottom, optional `×N` stack count. Empty slots are dashed/recessed.
- **Selection states**: red (`#8f3a34`) ring + a small mono tag (`CHOSEN` / `◎ TARGET`) for the player's pick; **gold** (`#8f6a2a`/`#c8963f`) ring + `PRIMARY` tag for system-highlighted things.
- **Tag/chip**: mono uppercase, ~1011px, colored text + matching 1px border + faint tinted background; semantic color (red = aggressive/danger, gold = pay/faith/primary, grey = neutral/locked).
- **Tabs**: pill buttons; active = solid `#8f3a34` bg with cream text; inactive = `#efe6cf`/dark with muted text + `#cbb684` border.
- **Primary button (CTA)**: `linear-gradient(180deg, #a5423a, #7a2f29)`, `#8f3a34` border, cream text, `box-shadow: 0 4px 16px rgba(143,58,52,.35)`; hover brightens to `#b8524a`/`#8f3a34`.
- **Danger/"FAILED"/"COMBAT" stamps**: 4px colored border, heavy mono uppercase, rotated ~ -6° to -11°, ~72% opacity.
- **Art-slot placeholder**: dashed `#5a5348` or `#4a4238` border, a stripe fill, and a centered mono caption naming what art goes there.
---
## Screens / Views
> For each screen: read the `.dc.html` template for exact pixel values and the `Component` class for the state model. Summaries below cover purpose, layout, key components, interactions, and state.
### 1. Title Screen — `Title Screen.dc.html`
- **Purpose:** Main menu / first impression.
- **Layout:** Full-bleed atmospheric background; left-aligned content column (`padding: 0 120px`) vertically centered.
- **Components:** Background = warm-to-black radial `radial-gradient(120% 90% at 70% 20%, #2a2018, #160f0a 45%, #0a0806)` + a faint diagonal-stripe key-art placeholder + an off-screen fire glow (`radial-gradient` bottom, animated opacity) + heavy vignette + **drifting ember particles** (10 small colored dots, `@keyframes ember` rising 460px over 711s with fade). Logo: kicker "A DARK FANTASY RPG" (mono, `letter-spacing:.42em`, `#a9843f`), title **"CODE OF CONQUEST"** (Georgia 800, 116px, `#e8ddc8`, two lines), a gold rule, and the tagline (Architects Daughter, 20px, `#b0a488`). Menu list below.
- **Interactions / state:** `state.sel` (index). Menu items: New Game, Continue (with save-info subline "CHAPTER II · VEXCCA · 14:22:07"), Load Game, Settings, Credits, Quit. A gold ◆ marker shows on the selected item (opacity toggle); selected item text brightens to `#f0e6d0`; hover brightens any item. Footer: version "v0.4.1 · BUILT IN GODOT 4.7 · © 2026" (left) and a `[↑ ↓] CHOOSE [↵] ENTER` hint (right).
### 2. Character Creation — `Character Creation.dc.html`
- **Purpose:** Build a character before play.
- **Layout:** Dark portrait bay (left, 660px) + parchment character sheet (right, 1260px).
- **Left (bay):** "WHO WILL YOU BE?" label, a tall **model art slot** with rotate arrows, a live **nameplate** (`displayName` in Architects Daughter + "`RACE · CLASS`"), and a **"THE DM SIZES YOU UP"** parchment inset showing a live gritty **origin blurb**.
- **Right (sheet):** Header; **RACE** (4 selectable cards: Human / Elf / Dwarf / Beastfolk, each with a gritty one-liner + trait); **CALLING** (5 cards: Sellsword / Cutpurse / Hedge-Mage / Bonesetter / Trapper, each tagged with its primary stat); **ABILITY SCORES**; footer with a **NAME** text field + "ENTER THE WORLD" CTA.
- **Stats (important, game-specific):** Six stats exist — **STR · DEX · CON · FTH (Faith) · MAG (Magic) · LCK (Luck)** — but only **five are visible**. **LCK is hidden from the player entirely**: it is rolled silently in the background (kept in state, never rendered — no number, no bar, no row); the player only ever "feels" it through opening-scene prose. Do not surface LCK anywhere in the UI.
- Each visible stat is **rolled to a base via 3d6** (die/formula is flagged `TUNABLE` in the logic — treat as configurable). The rolled value is a **hard floor**.
- There is a shared **+3 spend-up pool**, **additive only**: a `+`-only stepper per row, a "points left" counter, **no minus button**. A green `+N` badge shows points added to a row.
- The selected **class's primary stat** is highlighted (gold ring + `PRIMARY` tag): Sellsword→STR, Cutpurse→DEX, Hedge-Mage→MAG, Bonesetter→FTH, Trapper→DEX.
- A **⟳ re-roll** re-rolls all floors (including hidden LCK) and clears spent points.
- **State:** `race`, `cls`, `rolled{STR..LCK}`, `spent{5 visible}`, `name`. Selecting race/class updates highlight + the origin blurb + nameplate. See `renderVals()` for the point-buy guards.
### 3. Character Sheet — `Character Sheet.dc.html`
- **Purpose:** Persistent character overview (opened in-game with **C**).
- **Layout:** Dark portrait bay (left, 560px) + parchment sheet (right, 1360px, three columns).
- **Left:** portrait slot, nameplate, **XP bar**, **HP/MP** vitals, and prominent **condition badges** (e.g. "⚠ Infected bite — left arm", "Hungover").
- **Right, col A:** **ATTRIBUTES** (modifier-forward: big signed modifier + small score; 5 visible stats, LCK still hidden) and **SAVING THROWS** (proficiency ● / ○ dots + signed value).
- **Right, col B:** **"IN A FIGHT"** derived-stat grid (Armor, Initiative, Speed, Carry, Melee, Ranged, Spell DC, Death Saves) and a two-column **SKILLS** list (proficiency dot + ability tag + value).
- **Right, col C:** **TALENTS** (name + description cards), **AFFLICTIONS** (gritty, color-coded by severity red `#8f3a34` / amber `#a5772a`), and a **"THE DM'S NOTES"** bio (italic, dashed-border parchment).
- **State:** mostly static display; `renderVals()` computes ability modifiers `floor((score-10)/2)` and skill/save values from a `+2` proficiency bonus. Example character: Vexcca, human Sellsword, lvl 4.
### 4. Inventory — `Inventory.dc.html`
- **Purpose:** Equipment + carried items (opened with **I**).
- **Layout:** Dark paper-doll (left, 620px) + parchment "Satchel" (right, 1300px).
- **Left:** a central model slot with **12 equipment slots** positioned around it (head, amulet, chest, cloak, gloves, belt, mainhand, offhand, ring×2, legs, boots); filled slots show a rarity bar; a derived stat strip (Armor / Damage / HP / MP).
- **Right:** header with **gold** and an **encumbrance bar** (fills amber→red via `weightColor`), **category tabs** (All / Weapons / Armor / Consumables / Misc) that filter live, a **12×5 item grid**, and an **item detail card** at the bottom.
- **Interactions / state:** `sel` (selected item id), `tab`. Clicking any grid item or equipped slot selects it → detail card updates (rarity-colored name, type, stat lines, flavor, weight/value, and context actions Equip / Use / Drop shown conditionally). Tabs filter the grid. Item data (name, cat, rarity, glyph, weight, value, stats, flavor) is in the `items()` map — reuse it as seed data.
### 5. World Map — `World Map.dc.html`
- **Purpose:** Region travel (opened with **M**).
- **Layout:** Parchment map (left, flex) + dark location-detail panel (right, 560px).
- **Map:** header ("The Margreave" + subtitle), a **legend**, the map surface (radial parchment; **terrain illustration is an art slot**), a **compass** and **scale bar**. On it: **location markers** — settlements = circles, dungeons/ruins = diamonds, hostile = red, uncharted = grey **fog "?" markers**; the current town **pulses**. **Roads** are dashed SVG `<line>`s between locations; a **red planned-route line** runs from the current location to the selected one.
- **Detail panel:** selected location's type + status tag, a **danger rating (☠)**, gritty description, any **quests here**, and travel time + a "SET OUT ▸" CTA (or "You are here" at the current node).
- **Interactions / state:** `sel` (location id). Clicking a *discovered* marker selects it (uncharted markers are non-interactive). Locations carry `x,y` (0100 %), `status` (current/hostile/discovered/undiscovered), `danger` (04), `desc`, `quests[]`. Travel time is computed from map distance. Marker/road projection math is in `renderVals()`.
### 6. Quest Log — `Quest Log.dc.html`
- **Purpose:** Track quests (opened with **J**).
- **Layout:** Dark journal (left, 620px) + parchment "open page" (right, 1300px).
- **Left:** **tabs with counts** (Active / Completed / Failed) and a scrollable quest list — each entry shows type color (main = gold, side = green, bounty = red), location, progress (`2/4`), and a ★ on the tracked quest.
- **Right:** selected quest's type/giver/location, a DM-voice brief, an **objectives checklist** (done items struck through; the active one flagged `CURRENT`), and an **"ON COMPLETION"** rewards row (gold / XP / item). **Track (★/☆)** + Abandon for active quests; completed/failed quests get a rotated **COMPLETED / FAILED stamp**.
- **State:** `sel`, `tab`, `tracked`. Switching tab auto-selects the first quest in it. Quest data (type, status, giver, loc, desc, objectives[{text,done}], rewards) is in `questData()`.
### 7. Shop — `Shop.dc.html`
- **Purpose:** Buy/sell with a fence.
- **Layout:** Parchment wares (left, 1180px) + dark merchant panel (right, 740px).
- **Left:** header with **your purse** + **the merchant's coin**, **Buy/Sell tabs**, and item rows (rarity tile, name/type, stock-or-owned qty, price).
- **Right:** merchant "Grud Ninefingers" (portrait slot + a **reaction line** that changes as you deal), the selected item's detail, the deal price, and a BUY/SELL CTA.
- **Interactions / state (a real economy loop):** `mode` (buy/sell), `sel`, `gold`, `merchantGold`, `stock{}`, `owned{}`, `msg`. **Buy** deducts your gold, moves one unit from stock→owned, adds to merchant coin. **Sell** pays you ~40% of value, moves item owned→stock, deducts merchant coin. The CTA **disables with a gritty reason** when you're broke, the merchant is tapped out, an item is sold out, or worthless (value 0). Buy price = item value; sell price = `round(value*0.4)`.
### 8. Pause Menu / Settings — `Pause Menu.dc.html`
- **Purpose:** In-game pause + settings (overlay).
- **Layout:** The scene is **dimmed + blurred** (`rgba(8,6,4,.72)` + `backdrop-filter: blur(2px)`); a centered **1560×920 ornate-framed panel** (8px `#1a1512` border + inner 1.5px `#a9843f` hairline). Left rail (dark, 400px) + right settings (parchment).
- **Left rail:** "Paused", chapter, action buttons (Resume [primary] / Save Game / Load Game / Quit to Title), and footer status (playtime, last autosave, a permadeath warning).
- **Right:** **SETTINGS** with four tabs — **Gameplay / Video / Audio / Controls** — each rendering a list of controls.
- **Controls (fully interactive):** custom **toggle switches**, native **range sliders** (styled — thumb `#8f6a2a`, track `#cbb684`), native **selects**, and **keybind rows** (key-cap chips). Gameplay: difficulty select, permadeath/gore/tutorial toggles, narration-speed slider, autosave select. Video: window mode/resolution selects, brightness slider, VSync/motion-blur/damage-flash toggles. Audio: master/music/effects/ambience sliders + subtitles toggle. Controls: keybinds (WASD, E, LMB, ↵, I, C, J, M) + mouse-sensitivity slider + invert-Y toggle. Note the keybinds here are the canonical in-game key map.
- **State:** `tab` + a `s{}` settings object. Controls are generated per tab in `renderVals()` (`slider`/`toggle`/`select`/`keybind` helpers) — a good template for a data-driven settings menu in Godot.
### 9. Dialogue / Conversation — `Dialogue.dc.html`
- **Purpose:** Branching NPC conversation.
- **Layout:** Dimmed scene; NPC side (left, 640px) + parchment conversation (right, flex).
- **Left:** NPC portrait slot, nameplate (Grud the fence), and a **live disposition meter** (Hostile → Wary → Neutral → Warming, color-shifting bar).
- **Right:** speaker label, a DM-voice **narration beat** (italic), the NPC's **spoken line** (Georgia 34px, in “curly quotes”), and a numbered **response list**.
- **Choices:** each response can carry a **skill/approach tag chip** (`INTIMIDATE · STR`, `HAGGLE · DEX`, `FAITH`, `PAY ◈40` — colored red/gold/grey) and can be **gated/disabled** (🔒 with a requirement note).
- **Interactions / state:** `node` (current dialogue node), `disp` (0100 disposition). Selecting a response navigates a real **dialogue tree** (`nodes()`), applies a disposition delta (`dd`), and can dead-end into a **COMBAT** stamp or a clean exit; terminal nodes offer to restart. Port `nodes()` as your dialogue-graph data.
### 10. Combat HUD / Turn Tracker — `Combat HUD.dc.html`
- **Purpose:** The turn-based fight screen. **The player controls only their own character; the DM resolves the AI companions' and enemies' turns.**
- **Layout (flex column):** top **turn tracker** ribbon → middle row (left party+log, center battlefield, right enemies) → bottom command bar.
- **Turn tracker:** every combatant in **initiative order** with mini HP; the active unit is lifted + pulsing (`@keyframes cturn`); downed units marked ✕; color-coded (gold = you, teal = AI party, red = enemies) + a **ROUND** counter.
- **Center — isometric battle board:** a **7×6 diamond-tile grid** (CSS `clip-path` diamonds, iso projection `x=(c-r)*44, y=(c+r)*23`) with **unit tokens** on tiles (colored disc + initial + ground shadow + HP bar). Live with state: active unit's tile outlines **gold** and its token pulses; on your turn reachable tiles tint **teal (move range, Manhattan ≤2)**; the targeted enemy's tile outlines red and its token gets a **dashed red reticle**; dead units grey out; a VICTORY/DEFEAT stamp overlays on end. This is the placeholder for the real isometric battlefield — token positions come from `POS()`.
- **Left:** "YOUR PARTY · AI" companion cards (HP) + a parchment **DM combat log** (newest first).
- **Right:** **ENEMIES** list — click to **target** (selected gets a red border + `◎ TARGET`).
- **Bottom:** active-hero panel (portrait, HP, **action-economy pips**: Action ● / Bonus ● / Move) + **ability bar** (Falchion Slash, Reckless Swing, Shield Bash, Second Wind, Bile Bomb — each tagged ACTION/BONUS with a keybind) + the **turn-action button** (End Turn on your turn; "RESOLVE {name} ▸" on an AI turn).
- **Interactions / state:** `hp{}` per unit, `turnPos` (index into initiative order), `round`, `target`, `used{action,bonus}`, `over` (null/victory/defeat), `log[]`. On your turn: select an enemy, then use abilities (each spends its Action/Bonus, rolls damage from the ability's range, applies to target or heals self, logs a DM line); **End Turn** advances initiative (resetting your economy when it returns to you). On an AI turn: **Resolve** plays out that unit's action (enemies hit your side, allies hit enemies) with a narrated log line, then advances. Combat ends at all-enemies-down (victory) or player-down (defeat). Unit stats (init, max HP, AC, glyph) are in `base()`; abilities in `abilityDefs()`. **This state machine is the spec for your GDScript turn manager.**
### 11. Main Window + Wireframes — `Main Window + Wireframes.dc.html`
- **Purpose:** The core exploration HUD (the "tabletop split" main game screen). **Build from option "2a" at the top** — the four items below it are earlier low-fi wireframes kept only for context.
- **2a layout:** isometric world view (left, ~1180px) + a permanent **narration "book"** (right, ~740px) where the DM tells the story turn-by-turn with inline response choices.
- **World side:** a **turn-order rail** (initiative portraits, active ringed gold, downed ✕), a **minimap**, a **slide-in system dock** (Inventory / Character / Quest Log / Spellbook / World Map / Party — toggled by a gold tab, animates in/out), and a bottom command bar: **HP/MP bars + gold**, a **quick-select consumables** row (drawn from inventory), and an **End Turn** button.
- **Book side (parchment):** "THE DUNGEON MASTER" header, narration paragraphs, an action-result callout, numbered response choices, and a free-text **"describe your own action"** line (reinforcing the tabletop feel).
- **State:** `dockOpen` (the system dock slide toggle). The rest is layout.
---
## Godot 4.7 Translation Notes
- **Resolution:** author scenes at 1920×1080; set the project's stretch mode to `canvas_items` with a 1920×1080 reference and `keep` aspect (or `expand`) so the fixed layouts scale cleanly.
- **Palette + type → Theme:** put the color tokens and the three font families into a shared **`Theme`** resource with named `StyleBox`es for the recurring pieces (parchment card, dark panel, primary button, tab active/inactive, item tile, tag/chip). Every screen pulls from it — that's what keeps the look consistent.
- **Panels/containers:** the two-panel splits map to `HBoxContainer`/`HSplitContainer`; stat/skill/ability grids to `GridContainer`; lists to `VBoxContainer` (+ `ScrollContainer`). Use `MarginContainer` for the paddings noted in the templates.
- **Textures:** parchment gradient, dark stripe, and vignette are best baked as texture assets (or `NinePatchRect` + a shader) rather than reproduced as many gradient nodes. Art slots (portraits, key art, terrain, isometric world/battlefield) become `TextureRect`/`SubViewport`.
- **Narrative text:** use `RichTextLabel` for DM narration / quest briefs / dialogue lines (supports the italic + inline colored callouts).
- **Logic → GDScript:** the `Component` classes are the behavior spec. The highest-value ports: the **combat turn manager** (Combat HUD), the **dialogue graph** (Dialogue `nodes()`), the **shop economy** (Shop), and **point-buy character creation** (with the hidden-LCK rule). Reuse the seed data maps (items, quests, locations, units, abilities) as starting `Resource`s / dictionaries.
- **Fonts:** "Architects Daughter" (Google Fonts, OFL) and Georgia — bundle equivalents; if Georgia is unavailable, substitute a comparable serif and keep weights/sizes.
## Assets
No real art is included — all imagery is **placeholder art slots** (dashed boxes with captions naming what belongs there): character portraits/models, world/battlefield isometric renders, the world-map terrain illustration, key art, item/ability icons (currently short mono glyphs like `FAL`, `SLV`, `RNG`), and NPC/merchant mugshots. Icons are represented by 24-letter abbreviations to be replaced with real icon assets. Fonts: Architects Daughter (Google Fonts) + Georgia (system serif).
## Files
- `Title Screen.dc.html`
- `Character Creation.dc.html`
- `Character Sheet.dc.html`
- `Inventory.dc.html`
- `World Map.dc.html`
- `Quest Log.dc.html`
- `Shop.dc.html`
- `Pause Menu.dc.html`
- `Dialogue.dc.html`
- `Combat HUD.dc.html`
- `Main Window + Wireframes.dc.html` (build from option **2a**)
- `support.js` — runtime required only to *preview* the `.dc.html` files in a browser; not needed for the Godot implementation.

247
mockups/Shop.dc.html Normal file
View File

@@ -0,0 +1,247 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex">
<!-- ============ LEFT: WARES ============ -->
<div style="width:1180px;height:1080px;background:linear-gradient(180deg,#ece2ca,#e4d8bd);box-sizing:border-box;padding:30px 40px;display:flex;flex-direction:column;gap:18px">
<!-- header -->
<div style="display:flex;align-items:flex-end;justify-content:space-between;border-bottom:2px solid #bfa878;padding-bottom:14px">
<div>
<div style="font:600 30px 'Architects Daughter',cursive;letter-spacing:.04em;color:#5a4326">The Crooked Coin</div>
<div style="font:12px ui-monospace,monospace;color:#8a7748;letter-spacing:.08em;margin-top:2px">NO REFUNDS · NO QUESTIONS · NO CITY GUARD</div>
</div>
<div style="display:flex;gap:22px;align-items:flex-end">
<div style="text-align:right"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">YOUR PURSE</div><div style="font:600 24px ui-monospace,monospace;color:#8f6a2a">◈ {{ gold }}</div></div>
<div style="text-align:right"><div style="font:10px ui-monospace,monospace;letter-spacing:.1em;color:#8a7748">GRUD'S COIN</div><div style="font:600 24px ui-monospace,monospace;color:#7a684a">◈ {{ merchantGold }}</div></div>
</div>
</div>
<!-- buy/sell tabs -->
<div style="display:flex;gap:8px">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="2">
<div onClick="{{ t.onSelect }}" style="font:13px ui-monospace,monospace;letter-spacing:.1em;padding:9px 22px;border-radius:8px;border:1px solid {{ t.border }};background:{{ t.bg }};color:{{ t.fg }};cursor:pointer">{{ t.label }}</div>
</sc-for>
</div>
<!-- item rows -->
<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:9px">
<sc-for list="{{ rows }}" as="r" hint-placeholder-count="8">
<div onClick="{{ r.onSelect }}" style="display:flex;align-items:center;gap:16px;background:{{ r.bg }};border:1px solid {{ r.border }};border-radius:11px;padding:12px 16px;cursor:{{ r.cursor }};opacity:{{ r.opacity }}" style-hover="border-color:#8f7a4a">
<div style="width:52px;height:52px;flex:none;border-radius:9px;border:2px solid {{ r.rarityColor }};background:#e6d9b8;position:relative;display:flex;align-items:center;justify-content:center">
<span style="font:600 11px ui-monospace,monospace;color:#5a4326">{{ r.glyph }}</span>
<div style="position:absolute;left:6px;right:6px;bottom:5px;height:3px;border-radius:2px;background:{{ r.rarityColor }}"></div>
</div>
<div style="flex:1;min-width:0">
<div style="font:600 18px system-ui;color:#3a2f1c">{{ r.name }}</div>
<div style="font:11px ui-monospace,monospace;color:#8a7748;letter-spacing:.04em;margin-top:2px">{{ r.typeLabel }}</div>
</div>
<div style="font:12px ui-monospace,monospace;color:#a9885a;width:120px;text-align:right">{{ r.qtyText }}</div>
<div style="font:600 19px ui-monospace,monospace;color:#8f6a2a;width:90px;text-align:right">◈ {{ r.price }}</div>
</div>
</sc-for>
</div>
</div>
<!-- ============ RIGHT: MERCHANT + DEAL ============ -->
<div style="width:740px;flex:none;height:1080px;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);border-left:4px solid #1a1512;box-sizing:border-box;padding:34px 38px;display:flex;flex-direction:column;gap:20px;position:relative">
<div style="position:absolute;inset:0;background:radial-gradient(120% 60% at 50% 15%,rgba(0,0,0,0) 40%,rgba(0,0,0,.5) 100%);pointer-events:none"></div>
<!-- merchant -->
<div style="position:relative;display:flex;gap:18px;align-items:center">
<div style="width:92px;height:92px;flex:none;border-radius:12px;border:1px dashed #5a5348;background:repeating-linear-gradient(135deg,#2c2620 0 12px,#282219 12px 24px);display:flex;align-items:center;justify-content:center;font:10px ui-monospace,monospace;color:#6b6152;text-align:center">MUG<br>SHOT</div>
<div>
<div style="font:600 30px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1">Grud Ninefingers</div>
<div style="font:12px ui-monospace,monospace;letter-spacing:.1em;color:#a9843f;margin-top:3px">BEASTFOLK FENCE · GALLOWSMOOR</div>
</div>
</div>
<div style="position:relative;font:16px/1.55 Georgia,serif;font-style:italic;color:#c9bda3;background:rgba(18,15,11,.5);border-left:3px solid #a9843f;border-radius:8px;padding:13px 16px;min-height:58px">{{ merchantMsg }}</div>
<div style="position:relative;height:1px;background:#3a352e"></div>
<!-- deal detail -->
<div style="position:relative;flex:1;display:flex;flex-direction:column;gap:18px">
<div style="display:flex;gap:18px">
<div style="width:96px;height:96px;flex:none;border-radius:12px;border:2px solid {{ selRarityColor }};background:rgba(40,34,28,.7);position:relative;display:flex;align-items:center;justify-content:center">
<span style="font:600 15px ui-monospace,monospace;color:#c9bda3">{{ selGlyph }}</span>
<div style="position:absolute;left:8px;right:8px;bottom:7px;height:4px;border-radius:2px;background:{{ selRarityColor }}"></div>
</div>
<div style="flex:1;min-width:0">
<div style="font:600 26px 'Architects Daughter',cursive;color:{{ selRarityColor }};line-height:1.05">{{ selName }}</div>
<div style="font:11px ui-monospace,monospace;letter-spacing:.1em;color:#8a7f6c;margin-top:4px">{{ selType }}</div>
<div style="margin-top:12px;display:flex;flex-direction:column;gap:5px">
<sc-for list="{{ selStats }}" as="line" hint-placeholder-count="2">
<div style="font:14px system-ui;color:#d8ccb2">▸ {{ line }}</div>
</sc-for>
</div>
</div>
</div>
<div style="font:15px/1.55 Georgia,serif;font-style:italic;color:#8a7f6c">"{{ selFlavor }}"</div>
</div>
<!-- price + action -->
<div style="position:relative;border-top:1px solid #3a352e;padding-top:20px">
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:14px">
<span style="font:12px ui-monospace,monospace;letter-spacing:.12em;color:#8a7f6c">{{ priceLabel }}</span>
<span style="font:600 30px ui-monospace,monospace;color:#c8963f">◈ {{ dealPrice }}</span>
</div>
<div onClick="{{ onAction }}" style="text-align:center;padding:18px 0;border:1.5px solid {{ actionBorder }};border-radius:11px;background:{{ actionBg }};color:{{ actionFg }};font:600 19px system-ui;letter-spacing:.06em;cursor:{{ actionCursor }}">{{ actionLabel }}</div>
<div style="text-align:center;margin-top:9px;font:11px 'Architects Daughter',cursive;color:#7a7060">{{ actionNote }}</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 {
RARITY = { common: '#8a8378', uncommon: '#5f8a4f', rare: '#4f7ab8', epic: '#8a5fb8', legendary: '#c8963f' };
items() {
return {
salve: { name: 'Fevergrass Salve', cat: 'consumable', rarity: 'common', glyph: 'SLV', value: 20, stats: ['Clears Infection (1)', 'Heals 2d4 over time'], flavor: 'Tastes of pond. Works anyway.' },
antitox: { name: 'Antitoxin', cat: 'consumable', rarity: 'common', glyph: 'ATX', value: 35, stats: ['Advantage vs. poison for 1 hour'], flavor: 'Grud swears it is not just vinegar.' },
caltrops: { name: 'Iron Caltrops', cat: 'consumable', rarity: 'common', glyph: 'CAL', value: 15, stats: ['Scatter: 1d4 and stops a charge'], flavor: 'Rusty. The tetanus is a feature.' },
oil: { name: 'Oil of Sharpness', cat: 'consumable', rarity: 'uncommon', glyph: 'OIL', value: 60, stats: ['+1 weapon damage for 10 min'], flavor: 'Smells of the forge and something worse.' },
ash: { name: 'Grave-Ash', cat: 'misc', rarity: 'common', glyph: 'ASH', value: 8, stats: ['Spell reagent'], flavor: 'Someone was cremated for this. Cheaply.' },
falchion: { name: 'Notched Falchion', cat: 'weapon', slot: 'mainhand', rarity: 'uncommon', glyph: 'FAL', value: 85, stats: ['1217 slashing', '+1 vs. unarmored'], flavor: 'The nicks are from bone, not practice.' },
chain: { name: 'Chain Shirt', cat: 'armor', slot: 'chest', rarity: 'uncommon', glyph: 'CHN', value: 140, stats: ['+5 armor', 'Heavy on the shoulders'], flavor: 'Rings missing over the heart. Ask the last owner.' },
map: { name: "Smuggler's Map", cat: 'misc', rarity: 'rare', glyph: 'MAP', value: 220, stats: ['Reveals a hidden route out'], flavor: 'The ink runs when it rains. Memorize it fast.' },
shiv: { name: 'Rusted Shiv', cat: 'weapon', slot: 'mainhand', rarity: 'common', glyph: 'SHV', value: 5, stats: ['47 slashing'], flavor: 'A door hinge with ambitions.' },
bones: { name: 'Human Finger-bones', cat: 'misc', rarity: 'common', glyph: 'BNE', value: 0, stats: ["Someone's tally"], flavor: 'Grud will not touch these. Nobody will.' },
signet: { name: 'Cracked Signet', cat: 'misc', rarity: 'uncommon', glyph: 'SIG', value: 120, stats: ['A fallen noble house'], flavor: 'Worth more to the wrong people.' },
ration: { name: 'Stale Ration', cat: 'consumable', rarity: 'common', glyph: 'RTN', value: 2, stats: ['Removes Hungry'], flavor: 'Hard enough to break a tooth.' },
pelt: { name: 'Wolf Pelt', cat: 'misc', rarity: 'common', glyph: 'PLT', value: 12, stats: ['Trade good'], flavor: 'Still smells of wet dog and worse.' },
tooth: { name: 'Silver Tooth', cat: 'misc', rarity: 'uncommon', glyph: 'TTH', value: 40, stats: ['Trade good'], flavor: 'Pried loose. Do not ask from whom.' },
};
}
state = {
mode: 'buy',
sel: 'salve',
gold: 1240,
merchantGold: 340,
stock: { salve: 5, antitox: 3, caltrops: 4, oil: 2, ash: 8, falchion: 1, chain: 1, map: 1 },
owned: { shiv: 1, bones: 9, signet: 1, ration: 4, pelt: 3, tooth: 2 },
msg: 'Grud Ninefingers eyes your coat for bulges. "Show me silver, or show yourself out."',
};
rc(r) { return this.RARITY[r] || '#8a8378'; }
buyPrice(id) { return this.items()[id].value; }
sellPrice(id) { return Math.round(this.items()[id].value * 0.4); }
typeOf(it) {
const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
const c = it.cat === 'misc' ? 'Trinket' : cap(it.cat);
return it.slot ? c + ' · ' + cap(it.slot) : c;
}
buy() {
const id = this.state.sel;
const p = this.buyPrice(id);
if ((this.state.stock[id] || 0) <= 0) return;
if (this.state.gold < p) { this.setState({ msg: 'Grud laughs, wet and low. "Come back with coin, not lint."' }); return; }
this.setState(s => ({
gold: s.gold - p, merchantGold: s.merchantGold + p,
stock: { ...s.stock, [id]: s.stock[id] - 1 },
owned: { ...s.owned, [id]: (s.owned[id] || 0) + 1 },
msg: '"Mm." Grud bites the coin, then pockets it. The goods are yours.',
}));
}
sell() {
const id = this.state.sel;
const p = this.sellPrice(id);
if ((this.state.owned[id] || 0) <= 0) return;
if (p <= 0) { this.setState({ msg: 'Grud shoves it back across the counter. "I fence goods, not grief."' }); return; }
if (this.state.merchantGold < p) { this.setState({ msg: '"Grud\u2019s tapped out. Come back when I\u2019ve robbed someone."' }); return; }
this.setState(s => ({
gold: s.gold + p, merchantGold: s.merchantGold - p,
owned: { ...s.owned, [id]: s.owned[id] - 1 },
stock: { ...s.stock, [id]: (s.stock[id] || 0) + 1 },
msg: 'Grud sniffs it, names a number lower than you hoped, and pays it anyway.',
}));
}
renderVals() {
const ITEMS = this.items();
const mode = this.state.mode;
const buying = mode === 'buy';
const tabs = [
{ id: 'buy', label: 'BUY' },
{ id: 'sell', label: 'SELL' },
].map(t => ({
label: t.label,
onSelect: () => {
const src = t.id === 'buy' ? this.state.stock : this.state.owned;
const first = Object.keys(src).find(k => src[k] > 0) || this.state.sel;
this.setState({ mode: t.id, sel: first });
},
bg: t.id === mode ? '#8f3a34' : '#efe6cf',
fg: t.id === mode ? '#f1e8d2' : '#6a5a3a',
border: t.id === mode ? '#8f3a34' : '#cbb684',
}));
const src = buying ? this.state.stock : this.state.owned;
const ids = Object.keys(src);
const rows = ids.map(id => {
const it = ITEMS[id];
const qty = src[id] || 0;
const soldout = qty <= 0;
return {
glyph: it.glyph, name: it.name, typeLabel: this.typeOf(it),
rarityColor: this.rc(it.rarity),
qtyText: buying ? (soldout ? 'sold out' : '×' + qty + ' in stock') : '×' + qty + ' yours',
price: buying ? this.buyPrice(id) : this.sellPrice(id),
onSelect: () => { if (!soldout) this.setState({ sel: id }); },
cursor: soldout ? 'default' : 'pointer',
opacity: soldout ? '0.45' : '1',
bg: id === this.state.sel ? 'rgba(143,58,52,.12)' : '#f1e8d2',
border: id === this.state.sel ? '#8f3a34' : '#bfa878',
};
});
const id = this.state.sel;
const it = ITEMS[id];
const price = buying ? this.buyPrice(id) : this.sellPrice(id);
const stockQty = this.state.stock[id] || 0;
const ownQty = this.state.owned[id] || 0;
let enabled, note;
if (buying) {
enabled = stockQty > 0 && this.state.gold >= price;
note = stockQty <= 0 ? 'Grud is out of these.' : (this.state.gold < price ? 'Not enough silver in your purse.' : 'Grud will take your coin.');
} else {
enabled = ownQty > 0 && price > 0 && this.state.merchantGold >= price;
note = ownQty <= 0 ? "You've none left to sell." : (price <= 0 ? "Grud won't pay a copper for it." : (this.state.merchantGold < price ? "Grud can't cover that right now." : 'Grud will buy it off you.'));
}
return {
gold: this.state.gold.toLocaleString(),
merchantGold: this.state.merchantGold.toLocaleString(),
tabs, rows,
merchantMsg: this.state.msg,
selGlyph: it.glyph, selName: it.name, selType: this.typeOf(it),
selRarityColor: this.rc(it.rarity), selStats: it.stats, selFlavor: it.flavor,
priceLabel: buying ? 'GRUD ASKS' : 'GRUD OFFERS',
dealPrice: price,
actionLabel: buying ? 'BUY' : 'SELL',
onAction: buying ? () => this.buy() : () => this.sell(),
actionBg: enabled ? 'linear-gradient(180deg,#a5423a,#7a2f29)' : '#2a251f',
actionFg: enabled ? '#f4e6cf' : '#6b6152',
actionBorder: enabled ? '#8f3a34' : '#3a352e',
actionCursor: enabled ? 'pointer' : 'default',
actionNote: note,
};
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,99 @@
<!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 ember{0%{transform:translateY(0) scale(1);opacity:0}12%{opacity:.85}100%{transform:translateY(-460px) scale(.3);opacity:0}}
@keyframes glow{0%,100%{opacity:.5}50%{opacity:.8}}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;overflow:hidden;background:radial-gradient(120% 90% at 70% 20%,#2a2018 0%,#160f0a 45%,#0a0806 100%)">
<!-- key-art placeholder -->
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,#181310 0 22px,#141009 22px 44px);opacity:.5"></div>
<div style="position:absolute;right:8%;top:44%;transform:translateY(-50%);text-align:center;color:#4a3f30;font:13px ui-monospace,monospace;letter-spacing:.18em;opacity:.6">KEY ART DROPS IN HERE<br><span style="font:13px 'Architects Daughter',cursive;letter-spacing:.02em">a lone figure · the gallows · rain</span></div>
<!-- warm glow from below (off-screen fire) -->
<div style="position:absolute;left:0;right:0;bottom:0;height:420px;background:radial-gradient(80% 100% at 30% 100%,rgba(143,58,52,.28),rgba(169,132,63,.12) 40%,transparent 70%);animation:glow 6s ease-in-out infinite;pointer-events:none"></div>
<!-- vignette -->
<div style="position:absolute;inset:0;background:radial-gradient(110% 90% at 50% 40%,transparent 45%,rgba(0,0,0,.72) 100%);pointer-events:none"></div>
<!-- embers -->
<div style="position:absolute;left:0;right:0;bottom:0;height:600px;pointer-events:none">
<div style="position:absolute;bottom:0;left:12%;width:4px;height:4px;border-radius:50%;background:#c8963f;animation:ember 7s linear infinite;animation-delay:0s"></div>
<div style="position:absolute;bottom:0;left:20%;width:3px;height:3px;border-radius:50%;background:#a5442e;animation:ember 9s linear infinite;animation-delay:1.4s"></div>
<div style="position:absolute;bottom:0;left:28%;width:5px;height:5px;border-radius:50%;background:#c8963f;animation:ember 8s linear infinite;animation-delay:2.8s"></div>
<div style="position:absolute;bottom:0;left:35%;width:3px;height:3px;border-radius:50%;background:#8f6a2a;animation:ember 10s linear infinite;animation-delay:0.7s"></div>
<div style="position:absolute;bottom:0;left:44%;width:4px;height:4px;border-radius:50%;background:#c8963f;animation:ember 7.5s linear infinite;animation-delay:3.6s"></div>
<div style="position:absolute;bottom:0;left:52%;width:3px;height:3px;border-radius:50%;background:#a5442e;animation:ember 9.5s linear infinite;animation-delay:2.1s"></div>
<div style="position:absolute;bottom:0;left:61%;width:5px;height:5px;border-radius:50%;background:#c8963f;animation:ember 8.5s linear infinite;animation-delay:4.4s"></div>
<div style="position:absolute;bottom:0;left:70%;width:3px;height:3px;border-radius:50%;background:#8f6a2a;animation:ember 11s linear infinite;animation-delay:1.1s"></div>
<div style="position:absolute;bottom:0;left:78%;width:4px;height:4px;border-radius:50%;background:#c8963f;animation:ember 7.8s linear infinite;animation-delay:3.1s"></div>
<div style="position:absolute;bottom:0;left:86%;width:3px;height:3px;border-radius:50%;background:#a5442e;animation:ember 9.2s linear infinite;animation-delay:5s"></div>
</div>
<!-- content -->
<div style="position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;padding:0 120px">
<!-- title -->
<div style="position:relative">
<div style="font:13px ui-monospace,monospace;letter-spacing:.42em;color:#a9843f;margin-bottom:16px">A DARK FANTASY RPG</div>
<div style="font:800 116px Georgia,serif;letter-spacing:.02em;color:#e8ddc8;line-height:.9;text-shadow:0 4px 30px rgba(0,0,0,.8)">CODE OF<br>CONQUEST</div>
<div style="width:220px;height:2px;background:linear-gradient(90deg,#a9843f,transparent);margin:26px 0 18px"></div>
<div style="font:20px 'Architects Daughter',cursive;color:#b0a488;max-width:520px">The world has real problems, and it does not care about you.</div>
</div>
<!-- menu -->
<div style="margin-top:52px;display:flex;flex-direction:column;gap:5px">
<sc-for list="{{ items }}" as="m" hint-placeholder-count="6">
<div onClick="{{ m.onSelect }}" style="display:flex;align-items:center;gap:16px;cursor:pointer;padding:7px 0">
<span style="color:#a9843f;font-size:15px;opacity:{{ m.markOpacity }};transition:opacity .12s"></span>
<div>
<span style="font:600 30px Georgia,serif;letter-spacing:.04em;color:{{ m.color }};transition:color .12s" style-hover="color:#f0e6d0">{{ m.label }}</span>
<sc-if value="{{ m.hasSub }}" hint-placeholder-val="{{ false }}"><div style="font:12px ui-monospace,monospace;letter-spacing:.06em;color:#7a6f58;margin-top:1px">{{ m.sub }}</div></sc-if>
</div>
</div>
</sc-for>
</div>
</div>
<!-- footer -->
<div style="position:absolute;left:120px;bottom:40px;font:12px ui-monospace,monospace;letter-spacing:.06em;color:#5a5040">v0.4.1 · BUILT IN GODOT 4.7 · © 2026</div>
<div style="position:absolute;right:120px;bottom:40px;font:12px ui-monospace,monospace;letter-spacing:.08em;color:#5a5040">[ ↑ ↓ ] CHOOSE &nbsp; [ ↵ ] ENTER</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 {
state = { sel: 0 };
renderVals() {
const defs = [
{ label: 'New Game' },
{ label: 'Continue', sub: 'CHAPTER II · VEXCCA · 14:22:07' },
{ label: 'Load Game' },
{ label: 'Settings' },
{ label: 'Credits' },
{ label: 'Quit' },
];
const items = defs.map((d, i) => ({
label: d.label,
hasSub: !!d.sub, sub: d.sub || '',
markOpacity: i === this.state.sel ? '1' : '0',
color: i === this.state.sel ? '#f0e6d0' : '#9a8f78',
onSelect: () => this.setState({ sel: i }),
}));
return { items };
}
}
</script>
</body>
</html>

221
mockups/World Map.dc.html Normal file
View File

@@ -0,0 +1,221 @@
<!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:#0f0d0b;font-family:system-ui,sans-serif}
a{color:#8f3a34}a:hover{color:#6f2b26}
@keyframes wmpulse{0%{transform:scale(1);opacity:.7}70%{transform:scale(2.4);opacity:0}100%{opacity:0}}
</style>
</helmet>
<div style="width:1920px;height:1080px;position:relative;display:flex;background:#0f0d0b">
<!-- ============ MAP ============ -->
<div style="flex:1;min-width:0;height:1080px;box-sizing:border-box;padding:30px 34px;display:flex;flex-direction:column;gap:18px">
<!-- header -->
<div style="display:flex;align-items:flex-end;justify-content:space-between">
<div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.2em;color:#8a7f6c">WORLD MAP</div>
<div style="font:600 40px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1;margin-top:4px">The Margreave</div>
<div style="font:13px ui-monospace,monospace;letter-spacing:.1em;color:#a9843f;margin-top:5px">A DROWNED, DEBT-RIDDEN CORNER OF THE WORLD</div>
</div>
<!-- legend -->
<div style="display:flex;gap:16px;align-items:center;background:rgba(18,15,11,.7);border:1px solid #3a352e;border-radius:10px;padding:10px 16px">
<div style="display:flex;align-items:center;gap:7px;font:11px ui-monospace,monospace;color:#c9bda3"><span style="width:12px;height:12px;border-radius:50%;background:#c8963f;display:inline-block"></span>Settlement</div>
<div style="display:flex;align-items:center;gap:7px;font:11px ui-monospace,monospace;color:#c9bda3"><span style="width:11px;height:11px;background:#c9a45a;display:inline-block;transform:rotate(45deg)"></span>Dungeon / Ruin</div>
<div style="display:flex;align-items:center;gap:7px;font:11px ui-monospace,monospace;color:#c9bda3"><span style="width:12px;height:12px;border-radius:50%;background:#a5442e;display:inline-block"></span>Hostile</div>
<div style="display:flex;align-items:center;gap:7px;font:11px ui-monospace,monospace;color:#c9bda3"><span style="width:12px;height:12px;border-radius:50%;background:#5a5348;display:inline-block"></span>Uncharted</div>
</div>
</div>
<!-- map surface -->
<div style="position:relative;flex:1;border:2px solid #1a1512;border-radius:14px;overflow:hidden;background:radial-gradient(130% 120% at 40% 30%,#e9dcbd,#d8c8a2 60%,#c9b78a 100%)">
<!-- placeholder terrain note -->
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;color:#b09a6a;font:13px ui-monospace,monospace;letter-spacing:.16em;opacity:.5">HAND-DRAWN REGION MAP ART DROPS IN HERE<br><span style="font:12px 'Architects Daughter',cursive;letter-spacing:.02em">coastline · mires · roads · the mountains that hate you</span></div>
<!-- vignette + paper edge -->
<div style="position:absolute;inset:0;background:radial-gradient(120% 100% at 50% 50%,rgba(0,0,0,0) 55%,rgba(60,40,20,.28) 100%);pointer-events:none"></div>
<!-- roads -->
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none">
<sc-for list="{{ roads }}" as="r" hint-placeholder-count="8">
<line x1="{{ r.x1 }}" y1="{{ r.y1 }}" x2="{{ r.x2 }}" y2="{{ r.y2 }}" stroke="#8a6f45" stroke-width="0.5" stroke-dasharray="1.6 1.4" opacity="0.7"></line>
</sc-for>
<sc-if value="{{ routeShow }}" hint-placeholder-val="{{ false }}">
<line x1="{{ routeX1 }}" y1="{{ routeY1 }}" x2="{{ routeX2 }}" y2="{{ routeY2 }}" stroke="#8f3a34" stroke-width="0.9" stroke-dasharray="2 1.4"></line>
</sc-if>
</svg>
<!-- markers -->
<sc-for list="{{ markers }}" as="m" hint-placeholder-count="9">
<div style="position:absolute;{{ m.posStyle }};transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:5px;cursor:{{ m.cursor }}">
<div onClick="{{ m.onSelect }}" style="position:relative;width:30px;height:30px;display:flex;align-items:center;justify-content:center">
<sc-if value="{{ m.isCurrent }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;width:16px;height:16px;border-radius:50%;background:#8f3a34;animation:wmpulse 2s ease-out infinite"></div>
</sc-if>
<sc-if value="{{ m.showCircle }}" hint-placeholder-val="{{ true }}">
<div style="width:18px;height:18px;border-radius:50%;background:{{ m.color }};border:2px solid #4a3212;box-shadow:0 1px 3px rgba(0,0,0,.4)"></div>
</sc-if>
<sc-if value="{{ m.showDiamond }}" hint-placeholder-val="{{ false }}">
<div style="width:17px;height:17px;background:{{ m.color }};border:2px solid #4a3212;transform:rotate(45deg);box-shadow:0 1px 3px rgba(0,0,0,.4)"></div>
</sc-if>
<sc-if value="{{ m.showFog }}" hint-placeholder-val="{{ false }}">
<div style="width:26px;height:26px;border-radius:50%;background:radial-gradient(circle,#7a6f58,#5a5348);opacity:.75;display:flex;align-items:center;justify-content:center;font:700 13px ui-monospace;color:#e4d6b6">?</div>
</sc-if>
<sc-if value="{{ m.sel }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;inset:-6px;border:2.5px solid #8f3a34;border-radius:50%;pointer-events:none"></div>
</sc-if>
</div>
<div style="font:600 12px 'Architects Daughter',cursive;color:{{ m.labelColor }};white-space:nowrap;text-shadow:0 1px 2px rgba(233,220,189,.9)">{{ m.name }}</div>
</div>
</sc-for>
<!-- compass -->
<div style="position:absolute;bottom:16px;right:18px;width:64px;height:64px;border:1.5px solid #8a6f45;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(233,220,189,.6)">
<div style="font:700 13px ui-monospace,monospace;color:#6a5636">N</div>
<div style="position:absolute;top:5px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:12px solid #8f3a34"></div>
</div>
<!-- scale -->
<div style="position:absolute;bottom:20px;left:20px;display:flex;flex-direction:column;gap:3px"><div style="width:120px;height:5px;border:1px solid #6a5636;background:linear-gradient(90deg,#6a5636 50%,transparent 50%);background-size:40px 100%"></div><div style="font:9px ui-monospace,monospace;color:#6a5636;letter-spacing:.08em">≈ 3 DAYS' WALK</div></div>
</div>
</div>
<!-- ============ LOCATION DETAIL ============ -->
<div style="width:560px;flex:none;height:1080px;background:repeating-linear-gradient(135deg,#26221c 0 16px,#221e18 16px 32px);border-left:4px solid #1a1512;box-sizing:border-box;padding:34px 36px;display:flex;flex-direction:column;gap:20px;position:relative">
<div style="position:absolute;inset:0;background:radial-gradient(120% 60% at 50% 20%,rgba(0,0,0,0) 40%,rgba(0,0,0,.5) 100%);pointer-events:none"></div>
<div style="position:relative;font:13px ui-monospace,monospace;letter-spacing:.18em;color:#8a7f6c">LOCATION</div>
<div style="position:relative">
<div style="font:600 40px 'Architects Daughter',cursive;color:#e8d9b6;line-height:1.05">{{ selName }}</div>
<div style="display:flex;gap:8px;margin-top:12px">
<span style="font:11px ui-monospace,monospace;letter-spacing:.06em;color:#c9bda3;border:1px solid #4a4238;border-radius:6px;padding:5px 10px">{{ selType }}</span>
<span style="font:11px ui-monospace,monospace;letter-spacing:.06em;color:{{ selStatusColor }};border:1px solid {{ selStatusColor }};border-radius:6px;padding:5px 10px">{{ selStatus }}</span>
</div>
</div>
<!-- danger -->
<div style="position:relative;display:flex;align-items:center;gap:12px">
<span style="font:11px ui-monospace,monospace;letter-spacing:.12em;color:#8a7f6c">DANGER</span>
<span style="font:17px;letter-spacing:3px;color:#a5442e">{{ dangerFull }}<span style="color:#4a4238">{{ dangerEmpty }}</span></span>
</div>
<!-- description -->
<div style="position:relative;font:17px/1.6 Georgia,serif;color:#c9bda3">{{ selDesc }}</div>
<!-- quests -->
<sc-if value="{{ hasQuests }}" hint-placeholder-val="{{ false }}">
<div style="position:relative">
<div style="font:12px ui-monospace,monospace;letter-spacing:.14em;color:#a9843f;margin-bottom:10px">QUESTS HERE</div>
<div style="display:flex;flex-direction:column;gap:8px">
<sc-for list="{{ selQuests }}" as="q" hint-placeholder-count="2">
<div style="display:flex;gap:9px;font:15px/1.45 Georgia,serif;color:#d8ccb2;background:rgba(18,15,11,.6);border-left:3px solid #a9843f;border-radius:6px;padding:10px 13px"><span style="color:#a9843f"></span>{{ q }}</div>
</sc-for>
</div>
</div>
</sc-if>
<!-- footer travel -->
<div style="position:relative;margin-top:auto">
<sc-if value="{{ isHere }}" hint-placeholder-val="{{ false }}">
<div style="text-align:center;padding:15px 0;border:1px dashed #a9843f;border-radius:10px;color:#a9843f;font:600 15px 'Architects Daughter',cursive;letter-spacing:.04em">You are here. For now.</div>
</sc-if>
<sc-if value="{{ canTravel }}" hint-placeholder-val="{{ true }}">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;font:12px ui-monospace,monospace;color:#8a7f6c"><span>TRAVEL TIME</span><span style="color:#c9bda3">{{ travelDays }}</span></div>
<div style="text-align:center;padding:16px 0;border:1.5px solid #8f3a34;border-radius:10px;background:linear-gradient(180deg,#a5423a,#7a2f29);color:#f4e6cf;font:600 18px system-ui;letter-spacing:.06em;cursor:pointer;box-shadow:0 4px 16px rgba(143,58,52,.35)" style-hover="background:linear-gradient(180deg,#b8524a,#8f3a34)">SET OUT ▸</div>
</sc-if>
</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 {
locData() {
return [
{ id: 'gallowsmoor', name: 'Gallowsmoor', type: 'town', x: 20, y: 72, status: 'current', danger: 0, desc: 'Your grim little home port. The gallows on the moor gave it a name and a mood. Cheap ale, cheaper loyalty.', quests: [] },
{ id: 'gibbet', name: 'The Gibbet Road', type: 'poi', x: 42, y: 77, status: 'discovered', danger: 1, desc: 'A crossroads hung with cages, most still occupied. The crows here are fat and unafraid of people.', quests: [] },
{ id: 'mire', name: 'The Weeping Mire', type: 'wilds', x: 40, y: 55, status: 'hostile', danger: 3, desc: 'Black water and blacker moods. Something out here has been dragging tax-collectors under. Nobody is crying about it.', quests: ['Find the missing tax-collector (no reward, just answers)'] },
{ id: 'saltcrol', name: 'Saltcrol Harbor', type: 'town', x: 26, y: 35, status: 'discovered', danger: 0, desc: 'A working port that reeks of fish guts and old rope. Half the ships are smuggling, the other half wish they were.', quests: [] },
{ id: 'barrow', name: 'Barrow of the Ninth', type: 'dungeon', x: 56, y: 46, status: 'discovered', danger: 2, desc: 'The tomb of the hanged saint. The wards are failing one by one, and something inside is waking with them.', quests: ["Recover the Saint's femur for the Bonewright", '3 of 5 grave-wards still hold'] },
{ id: 'ashfell', name: 'Ashfell', type: 'ruin', x: 70, y: 66, status: 'discovered', danger: 3, desc: 'A city that burned and never stopped smoldering. A bell still rings somewhere in the smoke. It should not.', quests: ['Whatever is ringing the bell — make it stop'] },
{ id: 'redwidow', name: 'Redwidow Camp', type: 'camp', x: 62, y: 24, status: 'hostile', danger: 2, desc: 'Bandits who brand a red spider on everyone they rob, living or dead. They know your face by now.', quests: [] },
{ id: 'hollowdeep', name: '???', type: 'dungeon', x: 82, y: 40, status: 'undiscovered', danger: 4, desc: 'Uncharted. The maps just say "the deep" and leave the rest blank on purpose.', quests: [] },
{ id: 'edge', name: '???', type: 'poi', x: 88, y: 76, status: 'undiscovered', danger: 4, desc: 'Uncharted.', quests: [] },
];
}
STATUS = {
current: { label: 'YOU ARE HERE', color: '#8f3a34' },
hostile: { label: 'HOSTILE', color: '#a5442e' },
cleared: { label: 'CLEARED', color: '#5f8a4f' },
discovered: { label: 'DISCOVERED', color: '#c9a45a' },
undiscovered: { label: 'UNCHARTED', color: '#8a7c5a' },
};
markerColor(loc) {
if (loc.status === 'current') return '#8f3a34';
if (loc.status === 'hostile') return '#a5442e';
if (loc.status === 'cleared') return '#5f8a4f';
if (loc.status === 'undiscovered') return '#5a5348';
return loc.type === 'town' ? '#c8963f' : '#c9a45a';
}
state = { sel: 'barrow' };
roadPairs() {
return [['gallowsmoor', 'gibbet'], ['gibbet', 'mire'], ['mire', 'saltcrol'], ['mire', 'barrow'], ['barrow', 'ashfell'], ['gibbet', 'ashfell'], ['barrow', 'redwidow'], ['ashfell', 'hollowdeep']];
}
renderVals() {
const locs = this.locData();
const byId = {}; locs.forEach(l => byId[l.id] = l);
const sel = byId[this.state.sel];
const cur = locs.find(l => l.status === 'current');
const isDiamond = (t) => t === 'dungeon' || t === 'ruin';
const markers = locs.map(l => {
const undisc = l.status === 'undiscovered';
const diamond = isDiamond(l.type) && !undisc;
const circle = !diamond && !undisc;
return {
posStyle: `left:${l.x}%;top:${l.y}%`,
name: undisc ? '???' : l.name,
color: this.markerColor(l),
isCurrent: l.status === 'current',
showCircle: circle, showDiamond: diamond, showFog: undisc,
sel: l.id === this.state.sel,
cursor: undisc ? 'default' : 'pointer',
labelColor: l.status === 'current' ? '#8f3a34' : (undisc ? '#8a7c5a' : '#4a3f2c'),
onSelect: () => { if (!undisc) this.setState({ sel: l.id }); },
};
});
const roads = this.roadPairs().map(([a, b]) => ({ x1: byId[a].x, y1: byId[a].y, x2: byId[b].x, y2: byId[b].y }));
// planned route from current to selected
const routeShow = sel && sel.status !== 'current' && sel.status !== 'undiscovered';
const dist = Math.hypot(sel.x - cur.x, sel.y - cur.y);
const days = Math.max(1, Math.round(dist * 0.09));
const st = this.STATUS[sel.status];
const dn = sel.danger;
return {
markers, roads,
routeShow,
routeX1: cur.x, routeY1: cur.y, routeX2: sel.x, routeY2: sel.y,
selName: sel.name,
selType: { town: 'Town', dungeon: 'Dungeon', wilds: 'Wilds', ruin: 'Ruin', camp: 'Bandit Camp', poi: 'Waypoint' }[sel.type],
selStatus: st.label, selStatusColor: st.color,
selDesc: sel.desc,
dangerFull: '☠'.repeat(dn), dangerEmpty: '·'.repeat(4 - dn),
hasQuests: sel.quests.length > 0, selQuests: sel.quests,
isHere: sel.status === 'current',
canTravel: sel.status !== 'current' && sel.status !== 'undiscovered',
travelDays: '≈ ' + days + (days === 1 ? ' day' : ' days') + ' on foot',
};
}
}
</script>
</body>
</html>

1717
mockups/support.js Normal file

File diff suppressed because it is too large Load Diff