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>
248 lines
15 KiB
HTML
248 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}
|
||
</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="{"$preview":{"width":1920,"height":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: ['12–17 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: ['4–7 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>
|