feat(@projects/@magic-civilization): add city screen design templates

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-26 14:51:05 -07:00
parent 2823d7facf
commit 9263d5b4c5
7 changed files with 1326 additions and 12 deletions

View 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>

View 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>

View file

@ -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:

View file

@ -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)

View file

@ -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":

View file

@ -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)

View file

@ -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 --