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>
222 lines
15 KiB
HTML
222 lines
15 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}
|
|
@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="{"$preview":{"width":1920,"height":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>
|