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

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>