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>