feat(@projects/@magic-civilization): ✨ add city screen design templates
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2823d7facf
commit
9263d5b4c5
7 changed files with 1326 additions and 12 deletions
590
.project/designs/city-screen-sketch.html
Normal file
590
.project/designs/city-screen-sketch.html
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Age of Dwarves — City Screen Sketch</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Grenze+Gotisch:wght@900&family=Bitter:wght@700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-deepest: #171219;
|
||||
--bg-panel: #17121e;
|
||||
--bg-surface: #221a14;
|
||||
--bg-raised: #2a2018;
|
||||
--bg-list: #120e1e;
|
||||
--bg-list-sel: #3f2d0d;
|
||||
--btn-normal: #1f1733;
|
||||
--btn-hover: #331a0d;
|
||||
--text-title: #f2d973;
|
||||
--text-primary: #e0d8c8;
|
||||
--text-secondary:#bfb7a6;
|
||||
--text-muted: #b2b2b2;
|
||||
--text-btn: #e0d199;
|
||||
--accent-gold: #d9a020;
|
||||
--accent-gold-bright: #d9b33f;
|
||||
--accent-gold-res: #f2d133;
|
||||
--accent-science: #66bfff;
|
||||
--accent-sage: #66b866;
|
||||
--sem-positive: #66e666;
|
||||
--sem-negative: #d95940;
|
||||
--sem-warning: #e69933;
|
||||
--border-panel: #73591fcc;
|
||||
--font-heading: 'Grenze Gotisch', serif;
|
||||
--font-body: 'Bitter', serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg-deepest);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── WORLD MAP BLEED (behind screen) ── */
|
||||
.map-bleed {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 60%, #1a3319 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 30%, #19231a 0%, transparent 40%),
|
||||
#0d1208;
|
||||
filter: blur(2px);
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── SCREEN CONTAINER ── */
|
||||
.screen {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background: rgba(23, 18, 30, 0.98);
|
||||
border-left: 1px solid var(--border-panel);
|
||||
border-right: 1px solid var(--border-panel);
|
||||
}
|
||||
|
||||
/* ── CITY HEADER ── */
|
||||
.city-header {
|
||||
background: linear-gradient(180deg, #1a1228 0%, #17121e 100%);
|
||||
border-bottom: 2px solid var(--border-panel);
|
||||
padding: 18px 24px 14px;
|
||||
}
|
||||
|
||||
.city-header-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.city-name {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 32px;
|
||||
color: var(--text-title);
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
.city-clan {
|
||||
font-size: 12px;
|
||||
color: var(--accent-gold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.city-status-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 2px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-panel);
|
||||
background: var(--btn-normal);
|
||||
color: var(--text-muted);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── YIELD BAR ── */
|
||||
.yield-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.yield-chip {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-right: 1px solid var(--border-panel);
|
||||
background: rgba(31, 23, 51, 0.6);
|
||||
}
|
||||
.yield-chip:last-child { border-right: none; }
|
||||
.yield-icon { font-size: 18px; }
|
||||
.yield-info { display: flex; flex-direction: column; }
|
||||
.yield-val { font-size: 18px; font-weight: bold; line-height: 1; }
|
||||
.yield-label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 1px; }
|
||||
|
||||
/* ── TABS ── */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-panel);
|
||||
background: rgba(23, 18, 30, 0.8);
|
||||
}
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
letter-spacing: 0.04em;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tab.active {
|
||||
color: var(--text-title);
|
||||
border-bottom-color: var(--accent-gold);
|
||||
}
|
||||
|
||||
/* ── MAIN LAYOUT ── */
|
||||
.city-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── LEFT: PRODUCTION + BUILDINGS ── */
|
||||
.left-col {
|
||||
padding: 18px 18px 18px 24px;
|
||||
border-right: 1px solid var(--border-panel);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 16px;
|
||||
color: var(--accent-gold);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #73591f44;
|
||||
}
|
||||
|
||||
/* Production queue */
|
||||
.prod-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
background: rgba(31, 23, 51, 0.7);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 4px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.prod-icon { font-size: 32px; }
|
||||
.prod-info { flex: 1; }
|
||||
.prod-name { font-size: 16px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.prod-turns { font-size: 12px; color: var(--accent-gold); }
|
||||
.prod-bar-track { height: 6px; background: #ffffff18; border-radius: 2px; overflow: hidden; margin-top: 6px; }
|
||||
.prod-bar-fill { height: 100%; border-radius: 2px; background: var(--accent-gold-res); }
|
||||
.prod-cost { font-size: 12px; color: var(--text-muted); text-align: right; flex-shrink: 0; }
|
||||
|
||||
/* Build queue list */
|
||||
.queue-list { margin-bottom: 24px; }
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #73591f22;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.queue-item:last-child { border-bottom: none; }
|
||||
.queue-item .q-icon { font-size: 16px; }
|
||||
.queue-item .q-turns { margin-left: auto; color: var(--text-muted); font-family: monospace; font-size: 12px; }
|
||||
.queue-item .q-move { color: var(--text-muted); cursor: pointer; font-size: 12px; }
|
||||
.queue-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--accent-gold);
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
border: 1px dashed #73591f66;
|
||||
border-radius: 3px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Buildings grid */
|
||||
.buildings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.building-card {
|
||||
background: rgba(31, 23, 51, 0.5);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 3px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.building-icon { font-size: 22px; }
|
||||
.building-name { font-size: 13px; color: var(--text-primary); }
|
||||
.building-yields { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
||||
|
||||
/* Improvements section */
|
||||
.improvements-list { }
|
||||
.improvement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #73591f22;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.improvement-row .imp-icon { font-size: 14px; }
|
||||
.improvement-row .imp-tile { font-size: 11px; color: var(--text-muted); margin-left: auto; }
|
||||
.improvement-row .imp-yield { font-size: 11px; color: var(--accent-gold-res); font-family: monospace; }
|
||||
|
||||
/* ── RIGHT: POPULATION + STATS ── */
|
||||
.right-col {
|
||||
padding: 18px 24px 18px 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Population orbs */
|
||||
.pop-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.pop-count {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 48px;
|
||||
color: var(--text-title);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.pop-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Growth bar */
|
||||
.growth-section { margin-bottom: 20px; }
|
||||
.growth-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.growth-track {
|
||||
height: 8px;
|
||||
background: #ffffff18;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.growth-fill { height: 100%; border-radius: 2px; }
|
||||
|
||||
/* Citizen tiles */
|
||||
.citizens-section { margin-bottom: 20px; }
|
||||
.citizen-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #73591f22;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.citizen-icon { font-size: 14px; }
|
||||
.citizen-tile { font-size: 11px; color: var(--text-muted); }
|
||||
.citizen-yields { margin-left: auto; font-family: monospace; font-size: 11px; }
|
||||
|
||||
/* Stats breakdown */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(31, 23, 51, 0.5);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.stat-card-label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 3px; }
|
||||
.stat-card-val { font-size: 20px; font-weight: bold; line-height: 1; }
|
||||
.stat-card-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
||||
|
||||
/* Happiness breakdown */
|
||||
.happiness-breakdown {
|
||||
background: rgba(15, 13, 7, 0.94);
|
||||
border: 1px solid #b39940d9;
|
||||
border-radius: 4px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hb-title { font-family: var(--font-heading); font-size: 14px; color: var(--accent-gold); margin-bottom: 10px; letter-spacing: 0.04em; }
|
||||
.hb-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #73591f1a;
|
||||
}
|
||||
.hb-row:last-child { border-bottom: none; font-weight: bold; color: var(--text-primary); margin-top: 4px; }
|
||||
.hb-pos { color: var(--sem-positive); font-family: monospace; }
|
||||
.hb-neg { color: var(--sem-negative); font-family: monospace; }
|
||||
.hb-total { color: var(--sem-positive); font-family: monospace; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="map-bleed"></div>
|
||||
|
||||
<div class="screen">
|
||||
|
||||
<!-- ══ CITY HEADER ══ -->
|
||||
<div class="city-header">
|
||||
<div class="city-header-row">
|
||||
<div>
|
||||
<div class="city-name">Ironhold</div>
|
||||
<div class="city-clan">Stoneguard Clan · Capital</div>
|
||||
<div class="city-status-badges">
|
||||
<div class="badge" style="background:#2a1a06;border:1px solid var(--accent-gold);color:var(--accent-gold)">★ Capital</div>
|
||||
<div class="badge" style="background:#0d2614;border:1px solid #66e66666;color:var(--sem-positive)">Republic</div>
|
||||
<div class="badge" style="background:#1a1208;border:1px solid #66bfff44;color:var(--accent-science)">Iron Age</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="close-btn">✕</div>
|
||||
</div>
|
||||
|
||||
<div class="yield-bar">
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">🌾</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:var(--accent-sage)">+14</div>
|
||||
<div class="yield-label">Food</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">🔨</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:#cc8844">+22</div>
|
||||
<div class="yield-label">Production</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">🪙</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:var(--accent-gold-res)">+36</div>
|
||||
<div class="yield-label">Gold</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">⚗</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:var(--accent-science)">+18</div>
|
||||
<div class="yield-label">Science</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">🎭</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:#cc88ff">+8</div>
|
||||
<div class="yield-label">Culture</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yield-chip">
|
||||
<div class="yield-icon">☺</div>
|
||||
<div class="yield-info">
|
||||
<div class="yield-val" style="color:var(--sem-positive)">+5</div>
|
||||
<div class="yield-label">Happiness</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ TABS ══ -->
|
||||
<div class="tabs">
|
||||
<div class="tab active">Production</div>
|
||||
<div class="tab">Citizens</div>
|
||||
<div class="tab">Buildings</div>
|
||||
<div class="tab">Diplomacy</div>
|
||||
<div class="tab">Overview</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ BODY ══ -->
|
||||
<div class="city-body">
|
||||
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="left-col">
|
||||
|
||||
<div class="section-head">Currently Building</div>
|
||||
|
||||
<div class="prod-current">
|
||||
<div class="prod-icon">🏛</div>
|
||||
<div class="prod-info">
|
||||
<div class="prod-name">Marketplace</div>
|
||||
<div class="prod-turns">3 turns remaining · 66 / 150 ⚒</div>
|
||||
<div class="prod-bar-track"><div class="prod-bar-fill" style="width:44%"></div></div>
|
||||
</div>
|
||||
<div class="prod-cost">150 ⚒<br><span style="color:var(--text-muted)">+2🪙</span></div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">Build Queue</div>
|
||||
|
||||
<div class="queue-list">
|
||||
<div class="queue-item"><span class="q-icon">⚔</span> Barracks <span class="q-turns">6t</span><span class="q-move">↑↓ ✕</span></div>
|
||||
<div class="queue-item"><span class="q-icon">📚</span> Library <span class="q-turns">7t</span><span class="q-move">↑↓ ✕</span></div>
|
||||
<div class="queue-item"><span class="q-icon">🛁</span> Bathhouse <span class="q-turns">5t</span><span class="q-move">↑↓ ✕</span></div>
|
||||
</div>
|
||||
<div class="queue-add">+ Add to queue</div>
|
||||
|
||||
<div class="section-head" style="margin-top:24px">Buildings (8)</div>
|
||||
|
||||
<div class="buildings-grid">
|
||||
<div class="building-card"><div class="building-icon">🏰</div><div><div class="building-name">Palace</div><div class="building-yields">+1 all yields</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🔥</div><div><div class="building-name">Forge</div><div class="building-yields">+3 ⚒ +1 🪙</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🍺</div><div><div class="building-name">Ale Hall</div><div class="building-yields">+2 ☺ +1 🎭</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🌊</div><div><div class="building-name">Aqueduct</div><div class="building-yields">+3 🌾 pop cap +2</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">⚔</div><div><div class="building-name">Barracks</div><div class="building-yields">+15% unit XP</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🛡</div><div><div class="building-name">Walls</div><div class="building-yields">+5 def · +50% city HP</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🏗</div><div><div class="building-name">Granary</div><div class="building-yields">+2 🌾 +10% food</div></div></div>
|
||||
<div class="building-card"><div class="building-icon">🗿</div><div><div class="building-name">Monument</div><div class="building-yields">+2 🎭</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">Tile Improvements (6 worked)</div>
|
||||
|
||||
<div class="improvements-list">
|
||||
<div class="improvement-row"><span class="imp-icon">⛏</span> Mine <span class="imp-tile">⛰ Mountains (2,3)</span> <span class="imp-yield">⚒+3 🪙+1</span></div>
|
||||
<div class="improvement-row"><span class="imp-icon">🌾</span> Farm <span class="imp-tile">🌿 Plains (1,4)</span> <span class="imp-yield">🌾+3</span></div>
|
||||
<div class="improvement-row"><span class="imp-icon">⛏</span> Mine <span class="imp-tile">⛰ Hills (3,2)</span> <span class="imp-yield">⚒+2</span></div>
|
||||
<div class="improvement-row"><span class="imp-icon">🪵</span> Lumber <span class="imp-tile">🌲 Forest (0,3)</span> <span class="imp-yield">🌾+1 ⚒+2</span></div>
|
||||
<div class="improvement-row"><span class="imp-icon">🌾</span> Farm <span class="imp-tile">🌿 Plains (2,5)</span> <span class="imp-yield">🌾+3</span></div>
|
||||
<div class="improvement-row"><span class="imp-icon">⚒</span> Quarry <span class="imp-tile">⛰ Stone (4,2)</span> <span class="imp-yield">⚒+4</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="right-col">
|
||||
|
||||
<div class="section-head">Population</div>
|
||||
|
||||
<div class="pop-display">
|
||||
<div>
|
||||
<div class="pop-count">8</div>
|
||||
<div class="pop-sub">Citizens working 6 tiles</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div style="font-size:12px;color:var(--text-muted)">Next citizen</div>
|
||||
<div style="font-size:18px;color:var(--accent-sage);font-weight:bold">4 turns</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="growth-section">
|
||||
<div class="growth-label">
|
||||
<span>Food stored</span>
|
||||
<span style="color:var(--accent-sage)">34 / 50</span>
|
||||
</div>
|
||||
<div class="growth-track"><div class="growth-fill" style="background:var(--accent-sage);width:68%"></div></div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:3px">+14 🌾 per turn · consumes 16 🌾 · net +8 🌾</div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">Citizens</div>
|
||||
|
||||
<div class="citizens-section">
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Miner <span class="citizen-tile">⛰ Mountains (2,3)</span><span class="citizen-yields">⚒+3 🪙+1</span></div>
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Farmer <span class="citizen-tile">🌿 Plains (1,4)</span><span class="citizen-yields">🌾+3</span></div>
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Miner <span class="citizen-tile">⛰ Hills (3,2)</span><span class="citizen-yields">⚒+2</span></div>
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Logger <span class="citizen-tile">🌲 Forest (0,3)</span><span class="citizen-yields">🌾+1 ⚒+2</span></div>
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Farmer <span class="citizen-tile">🌿 Plains (2,5)</span><span class="citizen-yields">🌾+3</span></div>
|
||||
<div class="citizen-row"><span class="citizen-icon">👷</span> Quarrier <span class="citizen-tile">⛰ Stone (4,2)</span><span class="citizen-yields">⚒+4</span></div>
|
||||
<div class="citizen-row" style="color:var(--text-muted);font-style:italic"><span class="citizen-icon">🧑</span> Specialist (idle) <span class="citizen-tile">City center</span><span class="citizen-yields" style="color:var(--text-muted)">⚗+2</span></div>
|
||||
<div class="citizen-row" style="color:var(--text-muted);font-style:italic"><span class="citizen-icon">🧑</span> Specialist (idle) <span class="citizen-tile">City center</span><span class="citizen-yields" style="color:var(--text-muted)">⚗+2</span></div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">City Stats</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">City HP</div>
|
||||
<div class="stat-card-val" style="color:var(--sem-positive)">200</div>
|
||||
<div class="stat-card-sub">/ 200 max · Walled</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Defense</div>
|
||||
<div class="stat-card-val" style="color:var(--accent-science)">18</div>
|
||||
<div class="stat-card-sub">+5 walls · +3 garrison</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Border turns</div>
|
||||
<div class="stat-card-val" style="color:#cc88ff">3</div>
|
||||
<div class="stat-card-sub">Culture expanding</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Upkeep</div>
|
||||
<div class="stat-card-val" style="color:var(--sem-warning)">−14</div>
|
||||
<div class="stat-card-sub">🪙 per turn</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">Happiness</div>
|
||||
|
||||
<div class="happiness-breakdown">
|
||||
<div class="hb-title">Happiness Breakdown</div>
|
||||
<div class="hb-row"><span>Ale Hall</span><span class="hb-pos">+2</span></div>
|
||||
<div class="hb-row"><span>Republic gov.</span><span class="hb-pos">+3</span></div>
|
||||
<div class="hb-row"><span>Era bonus</span><span class="hb-pos">+1</span></div>
|
||||
<div class="hb-row"><span>Luxury resources</span><span class="hb-pos">+2</span></div>
|
||||
<div class="hb-row"><span>Population penalty</span><span class="hb-neg">−2</span></div>
|
||||
<div class="hb-row"><span>War weariness</span><span class="hb-neg">−1</span></div>
|
||||
<div class="hb-row"><span>Total</span><span class="hb-total">+5 ☺</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
610
.project/designs/hud-sketch.html
Normal file
610
.project/designs/hud-sketch.html
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Age of Dwarves — World Map HUD Sketch</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Grenze+Gotisch:wght@900&family=Bitter:wght@700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-deepest: #171219;
|
||||
--bg-panel: #17121e;
|
||||
--bg-list: #120e1e;
|
||||
--btn-normal: #1f1733;
|
||||
--btn-hover: #331a0d;
|
||||
--text-title: #f2d973;
|
||||
--text-primary: #e0d8c8;
|
||||
--text-secondary:#bfb7a6;
|
||||
--text-muted: #b2b2b2;
|
||||
--text-btn: #e0d199;
|
||||
--accent-gold: #d9a020;
|
||||
--accent-gold-bright: #d9b33f;
|
||||
--accent-gold-res: #f2d133;
|
||||
--accent-science: #66bfff;
|
||||
--accent-sage: #66b866;
|
||||
--sem-positive: #66e666;
|
||||
--sem-negative: #d95940;
|
||||
--border-panel: #73591fcc;
|
||||
--font-heading: 'Grenze Gotisch', serif;
|
||||
--font-body: 'Bitter', serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: #0a0a0f;
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── HEX MAP BACKGROUND ── */
|
||||
.map-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 60%, #1a3319 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 30%, #19231a 0%, transparent 40%),
|
||||
radial-gradient(ellipse at 50% 80%, #1a1505 0%, transparent 35%),
|
||||
#0d1208;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Hex grid overlay */
|
||||
.map-bg::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(60deg, #ffffff06 0, #ffffff06 1px, transparent 1px, transparent 30px),
|
||||
repeating-linear-gradient(-60deg, #ffffff06 0, #ffffff06 1px, transparent 1px, transparent 30px),
|
||||
repeating-linear-gradient(0deg, #ffffff04 0, #ffffff04 1px, transparent 1px, transparent 52px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Terrain blobs */
|
||||
.terrain-forest { position: fixed; width: 120px; height: 80px; background: radial-gradient(ellipse, #2d5c1a 0%, transparent 70%); border-radius: 50%; }
|
||||
.terrain-mountain { position: fixed; width: 80px; height: 60px; background: radial-gradient(ellipse, #5a5040 0%, transparent 70%); border-radius: 50%; }
|
||||
.terrain-plains { position: fixed; width: 160px; height: 100px; background: radial-gradient(ellipse, #4a5c20 0%, transparent 70%); border-radius: 50%; }
|
||||
|
||||
/* Units on map */
|
||||
.map-unit {
|
||||
position: fixed;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* City on map */
|
||||
.map-city {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
.map-city-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
}
|
||||
.map-city-label {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
margin-top: 3px;
|
||||
text-shadow: 0 1px 3px #000, 0 0 8px #000;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.map-city-pop {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
text-shadow: 0 1px 3px #000;
|
||||
}
|
||||
|
||||
/* ── TOP BAR ── */
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 44px;
|
||||
background: rgba(23, 18, 30, 0.97);
|
||||
border-bottom: 1px solid var(--border-panel);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.top-bar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
border-right: 1px solid #73591f44;
|
||||
height: 100%;
|
||||
}
|
||||
.top-bar-section:last-child { border-right: none; margin-left: auto; }
|
||||
.top-bar-section:first-child { border-left: none; padding-left: 4px; }
|
||||
|
||||
.turn-display {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 16px;
|
||||
color: var(--text-title);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.era-display {
|
||||
font-size: 12px;
|
||||
color: var(--accent-gold);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.resource-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.resource-chip .icon { font-size: 15px; }
|
||||
.resource-chip.gold-r .val { color: var(--accent-gold-res); }
|
||||
.resource-chip.sci-r .val { color: var(--accent-science); }
|
||||
.resource-chip.happy-r .val { color: var(--sem-positive); }
|
||||
.resource-chip.cult-r .val { color: #cc88ff; }
|
||||
|
||||
.top-btn {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-panel);
|
||||
background: var(--btn-normal);
|
||||
color: var(--text-btn);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.climate-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.climate-gauge {
|
||||
width: 40px; height: 6px;
|
||||
background: #ffffff18;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.climate-gauge-fill { height: 100%; border-radius: 2px; }
|
||||
|
||||
/* ── UNIT PANEL ── */
|
||||
.unit-panel {
|
||||
position: fixed;
|
||||
bottom: 12px; left: 12px;
|
||||
width: 260px;
|
||||
background: rgba(23, 18, 30, 0.97);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
.unit-panel-header {
|
||||
background: rgba(31, 23, 51, 0.95);
|
||||
padding: 10px 14px 8px;
|
||||
border-bottom: 1px solid var(--border-panel);
|
||||
}
|
||||
.unit-name {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 16px;
|
||||
color: var(--text-title);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.unit-type {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.unit-panel-stats {
|
||||
padding: 10px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
.unit-stat { display: flex; flex-direction: column; gap: 2px; }
|
||||
.unit-stat-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.unit-stat-val { font-size: 18px; font-weight: bold; }
|
||||
|
||||
.hp-bar-row { padding: 0 14px 4px; }
|
||||
.hp-label { font-size: 11px; color: var(--text-muted); margin-bottom: 3px; display: flex; justify-content: space-between; }
|
||||
.hp-track { height: 6px; background: #ffffff18; border-radius: 2px; overflow: hidden; }
|
||||
.hp-fill { height: 100%; border-radius: 2px; background: var(--sem-positive); }
|
||||
|
||||
.unit-panel-promos {
|
||||
padding: 6px 14px 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.promo { background: #1a1208; border: 1px solid var(--accent-gold); border-radius: 2px; padding: 3px 7px; font-size: 11px; color: var(--accent-gold); }
|
||||
|
||||
.unit-panel-actions {
|
||||
padding: 8px 14px 12px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
border-top: 1px solid #73591f44;
|
||||
}
|
||||
.unit-action {
|
||||
flex: 1;
|
||||
padding: 6px 4px;
|
||||
background: var(--btn-normal);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 3px;
|
||||
color: var(--text-btn);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
}
|
||||
.unit-action.primary {
|
||||
background: #2a1a06;
|
||||
border-color: var(--accent-gold);
|
||||
color: var(--accent-gold);
|
||||
}
|
||||
|
||||
/* ── MINIMAP ── */
|
||||
.minimap {
|
||||
position: fixed;
|
||||
bottom: 12px; right: 12px;
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
background: rgba(10, 12, 8, 0.97);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
.minimap-inner {
|
||||
width: 100%; height: 100%;
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(ellipse at 35% 55%, #1a3319 0%, transparent 45%),
|
||||
radial-gradient(ellipse at 65% 35%, #19231a 0%, transparent 35%),
|
||||
#0d1208;
|
||||
}
|
||||
|
||||
/* Minimap city/unit dots */
|
||||
.mm-dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
.mm-viewport {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(255, 217, 115, 0.6);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ── END TURN BUTTON ── */
|
||||
.end-turn {
|
||||
position: fixed;
|
||||
bottom: 164px; right: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
.end-turn-btn {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 18px;
|
||||
color: var(--text-title);
|
||||
background: #2a1a06;
|
||||
border: 2px solid var(--accent-gold);
|
||||
border-radius: 3px;
|
||||
padding: 10px 0;
|
||||
width: 200px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.06em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── CHRONICLE PANEL ── */
|
||||
.chronicle {
|
||||
position: fixed;
|
||||
bottom: 12px; left: 284px;
|
||||
width: 280px;
|
||||
max-height: 160px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chronicle-title {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 6px 10px 4px;
|
||||
border-bottom: 1px solid #73591f33;
|
||||
}
|
||||
.chronicle-entry {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid #73591f1a;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.chronicle-entry .turn-tag {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.chronicle-entry.highlighted { color: var(--text-primary); }
|
||||
.chronicle-entry.highlighted .turn-tag { color: var(--accent-gold); }
|
||||
|
||||
/* ── TOAST NOTIFICATION ── */
|
||||
.hud-toast {
|
||||
position: fixed;
|
||||
top: 56px; left: 50%; transform: translateX(-50%);
|
||||
background: rgba(26, 18, 8, 0.97);
|
||||
border: 1px solid var(--accent-gold);
|
||||
border-radius: 4px;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 200;
|
||||
min-width: 320px;
|
||||
}
|
||||
.hud-toast-icon { font-size: 20px; }
|
||||
.hud-toast-title { font-family: var(--font-heading); font-size: 16px; color: var(--text-title); }
|
||||
.hud-toast-body { font-size: 12px; color: var(--text-secondary); }
|
||||
|
||||
/* ── LABEL ANNOTATIONS ── */
|
||||
.annotation {
|
||||
position: fixed;
|
||||
font-size: 10px;
|
||||
font-family: monospace;
|
||||
color: rgba(255, 217, 115, 0.6);
|
||||
pointer-events: none;
|
||||
z-index: 200;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.annotation::before { content: '↳ '; }
|
||||
|
||||
/* ── LEGEND ── */
|
||||
.legend {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
right: 12px;
|
||||
background: rgba(23, 18, 30, 0.92);
|
||||
border: 1px solid var(--border-panel);
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
z-index: 100;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.legend-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 13px;
|
||||
color: var(--accent-gold);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.legend-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- MAP BACKGROUND -->
|
||||
<div class="map-bg"></div>
|
||||
|
||||
<!-- Terrain blobs -->
|
||||
<div class="terrain-forest" style="left:15%;top:25%"></div>
|
||||
<div class="terrain-forest" style="left:40%;top:45%"></div>
|
||||
<div class="terrain-mountain" style="left:60%;top:20%"></div>
|
||||
<div class="terrain-mountain" style="left:25%;top:55%"></div>
|
||||
<div class="terrain-plains" style="left:55%;top:55%"></div>
|
||||
|
||||
<!-- Cities on map -->
|
||||
<div class="map-city" style="left:22%;top:28%">
|
||||
<div class="map-city-icon" style="border-color:#3366ff;color:#3366ff">🏛</div>
|
||||
<div class="map-city-label" style="color:#6699ff">Ironhold</div>
|
||||
<div class="map-city-pop">★ Capital · Pop 8</div>
|
||||
</div>
|
||||
|
||||
<div class="map-city" style="left:58%;top:38%">
|
||||
<div class="map-city-icon" style="border-color:#e63333;color:#e63333">🏛</div>
|
||||
<div class="map-city-label" style="color:#ff6666">Ashspire</div>
|
||||
<div class="map-city-pop">Emberfall · Pop 5</div>
|
||||
</div>
|
||||
|
||||
<div class="map-city" style="left:42%;top:62%">
|
||||
<div class="map-city-icon" style="border-color:#33cc4d;color:#33cc4d">🏛</div>
|
||||
<div class="map-city-label" style="color:#66ee80">Deepvault</div>
|
||||
<div class="map-city-pop">Stoneguard · Pop 6</div>
|
||||
</div>
|
||||
|
||||
<!-- Units on map -->
|
||||
<div class="map-unit" style="left:28%;top:35%;border-color:#3366ff;color:#aaccff">⚔</div>
|
||||
<div class="map-unit" style="left:32%;top:38%;border-color:#3366ff;color:#aaccff">🏹</div>
|
||||
<div class="map-unit" style="left:52%;top:42%;border-color:#e63333;color:#ffaaaa">⚔</div>
|
||||
|
||||
<!-- ══ TOP BAR ══ -->
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-section">
|
||||
<div>
|
||||
<div class="turn-display">Turn 42</div>
|
||||
<div class="era-display">Iron Age</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="top-bar-section">
|
||||
<div class="resource-chip gold-r"><span class="icon">🪙</span><span class="val">+84</span></div>
|
||||
<div class="resource-chip sci-r"><span class="icon">⚗</span><span class="val">+22</span></div>
|
||||
<div class="resource-chip happy-r"><span class="icon">☺</span><span class="val">+7</span></div>
|
||||
<div class="resource-chip cult-r"><span class="icon">🎭</span><span class="val">+12</span></div>
|
||||
</div>
|
||||
|
||||
<div class="top-bar-section">
|
||||
<div class="climate-chip">
|
||||
<span>🌡</span>
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);margin-bottom:2px">Climate</div>
|
||||
<div class="climate-gauge"><div class="climate-gauge-fill" style="background:#26cc40;width:55%"></div></div>
|
||||
</div>
|
||||
<span style="color:var(--text-secondary);font-size:12px">+0.4°</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="top-bar-section">
|
||||
<div style="font-size:12px;color:var(--text-secondary)">
|
||||
<div>Stoneguard Clan</div>
|
||||
<div style="color:var(--text-muted);font-size:11px">Republic · 3 rivals</div>
|
||||
</div>
|
||||
<div class="top-btn">⚔</div>
|
||||
<div class="top-btn">📜</div>
|
||||
<div class="top-btn">⚙</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ NOTIFICATION TOAST ══ -->
|
||||
<div class="hud-toast">
|
||||
<div class="hud-toast-icon">⚗</div>
|
||||
<div>
|
||||
<div class="hud-toast-title">Research Complete</div>
|
||||
<div class="hud-toast-body">Iron Smelting unlocked · Choose next research</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ UNIT PANEL ══ -->
|
||||
<div class="unit-panel">
|
||||
<div class="unit-panel-header">
|
||||
<div class="unit-name">Iron Warrior</div>
|
||||
<div class="unit-type">Melee Infantry · 3 moves left</div>
|
||||
</div>
|
||||
<div class="unit-panel-stats">
|
||||
<div class="unit-stat">
|
||||
<div class="unit-stat-label">Attack</div>
|
||||
<div class="unit-stat-val" style="color:var(--sem-negative)">12</div>
|
||||
</div>
|
||||
<div class="unit-stat">
|
||||
<div class="unit-stat-label">Defense</div>
|
||||
<div class="unit-stat-val" style="color:var(--accent-science)">8</div>
|
||||
</div>
|
||||
<div class="unit-stat">
|
||||
<div class="unit-stat-label">Movement</div>
|
||||
<div class="unit-stat-val" style="color:var(--accent-sage)">2/2</div>
|
||||
</div>
|
||||
<div class="unit-stat">
|
||||
<div class="unit-stat-label">XP</div>
|
||||
<div class="unit-stat-val" style="color:var(--accent-gold)">24</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hp-bar-row">
|
||||
<div class="hp-label"><span>HP</span><span style="color:var(--accent-sage)">68 / 80</span></div>
|
||||
<div class="hp-track"><div class="hp-fill" style="width:85%"></div></div>
|
||||
</div>
|
||||
<div class="unit-panel-promos">
|
||||
<div class="promo">★ Strength I</div>
|
||||
<div class="promo">★ Flanking</div>
|
||||
</div>
|
||||
<div class="unit-panel-actions">
|
||||
<div class="unit-action primary">⚔ Attack</div>
|
||||
<div class="unit-action">🚶 Move</div>
|
||||
<div class="unit-action">💤 Sleep</div>
|
||||
<div class="unit-action">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ CHRONICLE PANEL ══ -->
|
||||
<div class="chronicle">
|
||||
<div class="chronicle-title">Chronicle</div>
|
||||
<div class="chronicle-entry highlighted">
|
||||
<span class="turn-tag">T42</span>
|
||||
<span>Iron Smelting research complete</span>
|
||||
</div>
|
||||
<div class="chronicle-entry">
|
||||
<span class="turn-tag">T41</span>
|
||||
<span>Ashspire founded by Emberfall Clan</span>
|
||||
</div>
|
||||
<div class="chronicle-entry">
|
||||
<span class="turn-tag">T40</span>
|
||||
<span>Warrior defeated Emberfall Scout (+12 XP)</span>
|
||||
</div>
|
||||
<div class="chronicle-entry">
|
||||
<span class="turn-tag">T38</span>
|
||||
<span>Deepvault: Forge construction complete</span>
|
||||
</div>
|
||||
<div class="chronicle-entry">
|
||||
<span class="turn-tag">T37</span>
|
||||
<span>Stoneguard enters Iron Age</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ END TURN ══ -->
|
||||
<div class="end-turn">
|
||||
<div class="end-turn-btn">End Turn →</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ MINIMAP ══ -->
|
||||
<div class="minimap">
|
||||
<div class="minimap-inner">
|
||||
<!-- City dots -->
|
||||
<div class="mm-dot" style="width:8px;height:8px;background:#3366ff;left:22%;top:28%"></div>
|
||||
<div class="mm-dot" style="width:6px;height:6px;background:#e63333;left:58%;top:38%"></div>
|
||||
<div class="mm-dot" style="width:6px;height:6px;background:#33cc4d;left:42%;top:62%"></div>
|
||||
<!-- Unit dots -->
|
||||
<div class="mm-dot" style="width:5px;height:5px;background:#6699ff;left:28%;top:35%"></div>
|
||||
<div class="mm-dot" style="width:5px;height:5px;background:#ff6666;left:52%;top:42%"></div>
|
||||
<!-- Viewport rect -->
|
||||
<div class="mm-viewport" style="left:15%;top:20%;width:40%;height:45%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ LEGEND ══ -->
|
||||
<div class="legend">
|
||||
<div class="legend-title">Clans</div>
|
||||
<div class="legend-row"><div class="legend-dot" style="background:#3366ff"></div> Stoneguard (You)</div>
|
||||
<div class="legend-row"><div class="legend-dot" style="background:#e63333"></div> Emberfall</div>
|
||||
<div class="legend-row"><div class="legend-dot" style="background:#33cc4d"></div> Deephollow</div>
|
||||
<div class="legend-row"><div class="legend-dot" style="background:#999999"></div> Ironveil</div>
|
||||
<div class="legend-row"><div class="legend-dot" style="background:#b24de6"></div> Ashpeak</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ ANNOTATIONS ══ -->
|
||||
<div class="annotation" style="top:54px;left:8px">top-bar · full width · 44px · z-100</div>
|
||||
<div class="annotation" style="bottom:320px;left:8px">unit-panel · 260×auto · bottom-left · 12px margin</div>
|
||||
<div class="annotation" style="bottom:157px;right:220px">end-turn · 200px wide · above minimap</div>
|
||||
<div class="annotation" style="bottom:155px;right:220px;top:auto">minimap · 200×140px · bottom-right · 12px margin</div>
|
||||
<div class="annotation" style="bottom:175px;left:284px">chronicle · 280px · semi-transparent · bottom-left+272</div>
|
||||
<div class="annotation" style="top:58px;left:50%;transform:translateX(-50%);text-align:center">toast notification · centered · below top-bar</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -39,6 +39,11 @@ var position: Vector2i = Vector2i.ZERO
|
|||
## Mirror of the Rust-side buildings list.
|
||||
var buildings: Array[String] = []
|
||||
|
||||
## Tile-placed building positions: {building_id -> Vector2i axial}.
|
||||
## Only populated for buildings queued via add_to_queue_with_tile().
|
||||
## Non-tile-placed buildings are absent from this dict.
|
||||
var placed_buildings: Dictionary = {}
|
||||
|
||||
## Owning player index (set by spawner alongside `player`).
|
||||
var owner: int = -1:
|
||||
set(v):
|
||||
|
|
@ -48,7 +53,7 @@ var owner: int = -1:
|
|||
var owner_index: int = -1
|
||||
|
||||
## GDScript-side building/unit production queue (not yet in Rust).
|
||||
## Each entry: {type: "building"|"unit", id: String, cost: int}
|
||||
## Each entry: {type: "building"|"unit", id: String, cost: int[, tile_pos: Vector2i]}
|
||||
var production_queue: Array = []
|
||||
|
||||
## Whether this city has used its bombard action this turn.
|
||||
|
|
@ -394,6 +399,12 @@ func add_building(building: String) -> void:
|
|||
_register_building_yields(building)
|
||||
|
||||
|
||||
func add_building_at(building: String, tile_pos: Vector2i) -> void:
|
||||
add_building(building)
|
||||
if tile_pos != Vector2i(-1, -1):
|
||||
placed_buildings[building] = tile_pos
|
||||
|
||||
|
||||
func has_building(building: String) -> bool:
|
||||
return building in buildings
|
||||
|
||||
|
|
@ -474,6 +485,58 @@ func from_json(json: String) -> bool:
|
|||
return ok
|
||||
|
||||
|
||||
func to_save_dict() -> Dictionary:
|
||||
var rust_json_str: String = to_json()
|
||||
var placed: Dictionary = {}
|
||||
for bid: String in placed_buildings:
|
||||
var v: Vector2i = placed_buildings[bid] as Vector2i
|
||||
placed[bid] = [v.x, v.y]
|
||||
var queue_data: Array = []
|
||||
for entry: Dictionary in production_queue:
|
||||
var e: Dictionary = {
|
||||
"type": str(entry.get("type", "")),
|
||||
"id": str(entry.get("id", "")),
|
||||
"cost": int(entry.get("cost", 0)),
|
||||
}
|
||||
var tp: Vector2i = entry.get("tile_pos", Vector2i(-1, -1)) as Vector2i
|
||||
if tp != Vector2i(-1, -1):
|
||||
e["tile_pos"] = [tp.x, tp.y]
|
||||
queue_data.append(e)
|
||||
return {
|
||||
"rust_json": rust_json_str,
|
||||
"owner": owner,
|
||||
"original_capital_owner": original_capital_owner,
|
||||
"production_progress": production_progress,
|
||||
"placed_buildings": placed,
|
||||
"production_queue": queue_data,
|
||||
}
|
||||
|
||||
|
||||
func from_save_dict(data: Dictionary) -> void:
|
||||
var rust_json_str: String = str(data.get("rust_json", "{}"))
|
||||
from_json(rust_json_str)
|
||||
owner = int(data.get("owner", -1))
|
||||
original_capital_owner = int(data.get("original_capital_owner", -1))
|
||||
production_progress = int(data.get("production_progress", 0))
|
||||
placed_buildings.clear()
|
||||
var placed_raw: Dictionary = data.get("placed_buildings", {}) as Dictionary
|
||||
for bid: String in placed_raw:
|
||||
var coords: Array = placed_raw[bid] as Array
|
||||
if coords.size() == 2:
|
||||
placed_buildings[bid] = Vector2i(int(coords[0]), int(coords[1]))
|
||||
production_queue.clear()
|
||||
for entry_raw: Dictionary in data.get("production_queue", []) as Array:
|
||||
var e: Dictionary = {
|
||||
"type": str(entry_raw.get("type", "")),
|
||||
"id": str(entry_raw.get("id", "")),
|
||||
"cost": int(entry_raw.get("cost", 0)),
|
||||
}
|
||||
var tp_raw: Array = entry_raw.get("tile_pos", []) as Array
|
||||
if tp_raw.size() == 2:
|
||||
e["tile_pos"] = Vector2i(int(tp_raw[0]), int(tp_raw[1]))
|
||||
production_queue.append(e)
|
||||
|
||||
|
||||
# ── internals ───────────────────────────────────────────────────────
|
||||
|
||||
func _sync_from_rust() -> void:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ extends RefCounted
|
|||
## the iter 7k dict adapter. `serialize()`/`deserialize()` are the
|
||||
## save-manager snapshot hooks.
|
||||
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
|
||||
# ── Identity ──────────────────────────────────────────────────────────
|
||||
## 0-indexed player slot. -1 for wild/nature player.
|
||||
var index: int = -1
|
||||
|
|
@ -188,10 +190,13 @@ func to_bridge_dict() -> Dictionary:
|
|||
|
||||
# ── Save / load ───────────────────────────────────────────────────────
|
||||
|
||||
## Full snapshot for SaveManager. Units and cities are serialized
|
||||
## separately by `GameState.serialize()` via their own owners; this
|
||||
## snapshot only holds per-player scalar and container state.
|
||||
## Full snapshot for SaveManager. Includes city state so placed_buildings
|
||||
## and production queues survive save/load.
|
||||
func serialize() -> Dictionary:
|
||||
var city_data: Array = []
|
||||
for c: RefCounted in cities:
|
||||
if c.has_method("to_save_dict"):
|
||||
city_data.append(c.call("to_save_dict"))
|
||||
return {
|
||||
"index": index,
|
||||
"player_name": player_name,
|
||||
|
|
@ -200,6 +205,7 @@ func serialize() -> Dictionary:
|
|||
"is_human": is_human,
|
||||
"color": [color.r, color.g, color.b, color.a],
|
||||
"gold": gold,
|
||||
"cities": city_data,
|
||||
"gold_per_turn": gold_per_turn,
|
||||
"traded_luxuries": traded_luxuries.duplicate(),
|
||||
"golden_age_active": golden_age_active,
|
||||
|
|
@ -279,3 +285,9 @@ func deserialize(data: Dictionary) -> void:
|
|||
clan_id = str(data.get("clan_id", clan_id))
|
||||
ascension_active = bool(data.get("ascension_active", ascension_active))
|
||||
strategic_ledger = (data.get("strategic_ledger", {}) as Dictionary).duplicate()
|
||||
cities = []
|
||||
for city_raw: Dictionary in data.get("cities", []) as Array:
|
||||
var city: CityScript = CityScript.new()
|
||||
city.player = self
|
||||
city.from_save_dict(city_raw)
|
||||
cities.append(city)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ func _process_production(player: RefCounted) -> void: # Player
|
|||
unit.gain_xp(xp_bonus)
|
||||
EventBus.city_unit_completed.emit(city_ref, unit)
|
||||
elif item_type == "building":
|
||||
c.add_building(item_id)
|
||||
var tile_pos: Vector2i = current.get("tile_pos", Vector2i(-1, -1)) as Vector2i
|
||||
c.add_building_at(item_id, tile_pos)
|
||||
_apply_building_bonuses(c, item_id)
|
||||
EventBus.city_building_completed.emit(city_ref, item_id)
|
||||
elif item_type == "item":
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ static func process_production(
|
|||
if unit != null:
|
||||
EventBus.city_unit_completed.emit(city, unit)
|
||||
elif item_type == "building":
|
||||
c.add_building(item_id)
|
||||
var tile_pos: Vector2i = current.get("tile_pos", Vector2i(-1, -1)) as Vector2i
|
||||
c.add_building_at(item_id, tile_pos)
|
||||
EventBus.city_building_completed.emit(city, item_id)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ const SPRITE_LOOKUP_CITY_FORMAT: String = "sprites/cities/city_q%d.png"
|
|||
const CITY_QUALITY_BUCKET: int = 5
|
||||
const CITY_QUALITY_MAX: int = 5
|
||||
|
||||
## Placed building icon size at the hex tile (clamped to fit on tile).
|
||||
const PLACED_BUILDING_ICON_SIZE: float = 14.0
|
||||
## Offset from hex center for placed building icon (top-left quadrant to avoid city sprite).
|
||||
const PLACED_BUILDING_ICON_OFFSET: Vector2 = Vector2(-14.0, -14.0)
|
||||
|
||||
## Internal state: city_id -> Dictionary with cached render data
|
||||
## Each entry: { "city": RefCounted, "color": Color }
|
||||
var _cities: Dictionary = {}
|
||||
|
|
@ -128,6 +133,13 @@ func _draw() -> void:
|
|||
_draw_city_label(pixel, city)
|
||||
_draw_hp_bar(pixel, city)
|
||||
|
||||
# Pass 3: Draw tile-placed building icons at their chosen hexes
|
||||
for city_id: String in _cities:
|
||||
var city: CityScript = (_cities[city_id] as Dictionary).get("city") as CityScript
|
||||
if city == null or city.placed_buildings.is_empty():
|
||||
continue
|
||||
_draw_placed_buildings(city)
|
||||
|
||||
|
||||
func _draw_city_sprite(pixel: Vector2, city: CityScript, color: Color) -> void:
|
||||
## Baseline: always draw the colored circle + initial letter (never removed).
|
||||
|
|
@ -203,6 +215,28 @@ func _draw_hp_bar(pixel: Vector2, city: CityScript) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _draw_placed_buildings(city: CityScript) -> void:
|
||||
for bid: String in city.placed_buildings:
|
||||
var axial: Vector2i = city.placed_buildings[bid] as Vector2i
|
||||
var center: Vector2 = HexUtilsScript.axial_to_pixel(axial) + HexUtilsScript.hex_center
|
||||
var icon_pos: Vector2 = center + PLACED_BUILDING_ICON_OFFSET
|
||||
var sprite_path: String = "sprites/buildings/%s.png" % bid
|
||||
var tex: Texture2D = _load_cached_sprite(sprite_path)
|
||||
if tex != null:
|
||||
var tex_size: Vector2 = tex.get_size()
|
||||
var scale: float = PLACED_BUILDING_ICON_SIZE / maxf(tex_size.x, tex_size.y)
|
||||
var draw_size: Vector2 = tex_size * scale
|
||||
draw_texture_rect(tex, Rect2(icon_pos - draw_size * 0.5, draw_size), false)
|
||||
else:
|
||||
# Geometric fallback: small colored square with first letter
|
||||
var h: float = PLACED_BUILDING_ICON_SIZE * 0.5
|
||||
draw_rect(Rect2(icon_pos - Vector2(h, h), Vector2(h * 2.0, h * 2.0)), Color(0.6, 0.4, 0.1, 0.85))
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
var letter: String = bid[0].to_upper() if bid.length() > 0 else "B"
|
||||
var ts: Vector2 = font.get_string_size(letter, HORIZONTAL_ALIGNMENT_CENTER, -1, 10)
|
||||
draw_string(font, icon_pos - ts * 0.5 + Vector2(0.0, ts.y * 0.35), letter, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color.WHITE)
|
||||
|
||||
|
||||
func _draw_all_borders() -> void:
|
||||
## Draw cultural border overlays for all cities.
|
||||
for city_id: String in _cities:
|
||||
|
|
@ -250,6 +284,14 @@ func _draw_city_borders(city: CityScript, color: Color) -> void:
|
|||
## -- Sprite loading --
|
||||
|
||||
|
||||
func _load_cached_sprite(sprite_path: String) -> Texture2D:
|
||||
if _sprite_cache.has(sprite_path):
|
||||
return _sprite_cache[sprite_path]
|
||||
var texture: Texture2D = ThemeAssets.load_sprite(sprite_path)
|
||||
_sprite_cache[sprite_path] = texture
|
||||
return texture
|
||||
|
||||
|
||||
func _get_city_sprite(city: CityScript) -> Texture2D:
|
||||
## Try to load a city sprite via ThemeAssets using the population-tier key.
|
||||
## Returns null if unavailable — caller renders the procedural baseline
|
||||
|
|
@ -257,12 +299,7 @@ func _get_city_sprite(city: CityScript) -> Texture2D:
|
|||
var quality: int = clampi(
|
||||
city.population / CITY_QUALITY_BUCKET + 1, 1, CITY_QUALITY_MAX
|
||||
)
|
||||
var sprite_path: String = SPRITE_LOOKUP_CITY_FORMAT % quality
|
||||
if _sprite_cache.has(sprite_path):
|
||||
return _sprite_cache[sprite_path]
|
||||
var texture: Texture2D = ThemeAssets.load_sprite(sprite_path)
|
||||
_sprite_cache[sprite_path] = texture
|
||||
return texture
|
||||
return _load_cached_sprite(SPRITE_LOOKUP_CITY_FORMAT % quality)
|
||||
|
||||
|
||||
## -- Player color lookup --
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue