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>
192 lines
13 KiB
HTML
192 lines
13 KiB
HTML
<!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="{"$preview":{"width":1920,"height":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>
|