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

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>