Files
code_of_conquest_dnd/mockups/Character Creation.dc.html
Phillip Tarrant 49bc6b22c0 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>
2026-07-10 16:06:23 -05:00

192 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>