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

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>