// Civilization — city detail modal: growth, yields, worked-tile emphasis, // build queue with buy, buildings, supported units and trade routes. import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; import { Button } from '../../ui/Button.js'; import * as Logic from './CivilizationLogic.js'; const FONT = '"Julius Sans One"'; export function openCityScreen(scene, rules, state, city, onClose) { const root = scene.add.container(0, 0).setDepth(65); const dim = scene.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6) .setInteractive(); const W = 1500; const H = 860; const px = GAME_WIDTH / 2; const py = GAME_HEIGHT / 2; const panel = scene.add.rectangle(px, py, W, H, COLORS.panel) .setStrokeStyle(3, COLORS.accent); root.add([dim, panel]); let dynamic = scene.add.container(0, 0); root.add(dynamic); const close = () => { root.destroy(true); onClose(); }; const closeBtn = new Button(scene, px + W / 2 - 80, py - H / 2 + 40, '✕', close, { width: 60, height: 44, fontSize: 22, variant: 'ghost' }); root.add(closeBtn); function redraw() { dynamic.destroy(true); dynamic = scene.add.container(0, 0); root.add(dynamic); const civ = state.civs[city.civ]; Logic.autoAssignTiles(rules, state, city); const y = Logic.cityYields(rules, state, city); const left = px - W / 2 + 40; const top = py - H / 2 + 30; dynamic.add(scene.add.text(px, top + 10, `${city.name} — Population ${city.size}`, { fontFamily: 'Righteous', fontSize: '34px', color: COLORS.accentHex, }).setOrigin(0.5, 0)); // Growth bar. const boxSize = (city.size + 1) * Logic.FOODBOX_PER_SIZE; const gw = 400; dynamic.add(scene.add.text(left, top + 70, 'Growth', { fontFamily: FONT, fontSize: '19px', color: COLORS.mutedHex })); dynamic.add(scene.add.rectangle(left + 90, top + 80, gw, 20, 0x0a0d12).setOrigin(0, 0.5) .setStrokeStyle(1, COLORS.muted)); dynamic.add(scene.add.rectangle(left + 90, top + 80, gw * Math.max(0, Math.min(1, city.foodBox / boxSize)), 16, 0x4a9e44).setOrigin(0, 0.5)); dynamic.add(scene.add.text(left + 100 + gw, top + 70, `${city.foodBox}/${boxSize}`, { fontFamily: FONT, fontSize: '18px', color: COLORS.textHex, })); // Yields breakdown. const lines = [ `Food ${y.food} (need ${y.foodNeed}) surplus ${y.foodSurplus >= 0 ? '+' : ''}${y.foodSurplus}`, `Shields ${y.shield}${y.supportShields ? ` (support −${y.supportShields})` : ''}`, `Trade ${y.trade} corruption −${y.corruption}${y.routeTrade ? ` routes +${y.routeTrade}` : ''}`, `Gold +${y.gold} · Science +${y.science} · Upkeep −${y.upkeep}`, ]; dynamic.add(scene.add.text(left, top + 112, lines.join('\n'), { fontFamily: FONT, fontSize: '20px', color: COLORS.textHex, lineSpacing: 8, })); // Emphasis toggle. dynamic.add(scene.add.text(left, top + 260, 'Worker emphasis:', { fontFamily: FONT, fontSize: '19px', color: COLORS.mutedHex, })); ['balanced', 'food', 'production', 'trade'].forEach((emp, i) => { const bx = left + 200 + i * 150; const rect = scene.add.rectangle(bx, top + 270, 138, 40, COLORS.panel) .setStrokeStyle(2, city.emphasis === emp ? COLORS.gold : COLORS.muted); const txt = scene.add.text(bx, top + 270, emp.toUpperCase(), { fontFamily: FONT, fontSize: '16px', color: city.emphasis === emp ? COLORS.goldHex : COLORS.textHex, }).setOrigin(0.5); rect.setInteractive({ useHandCursor: true }); rect.on('pointerdown', () => { city.emphasis = emp; redraw(); }); dynamic.add(rect); dynamic.add(txt); }); // Buildings list. dynamic.add(scene.add.text(left, top + 320, 'City Improvements', { fontFamily: FONT, fontSize: '21px', color: COLORS.accentHex, })); const built = Object.keys(city.buildings).map((id) => rules.buildings[id]?.name ?? id); dynamic.add(scene.add.text(left, top + 355, built.length ? built.join(', ') : '(none yet)', { fontFamily: FONT, fontSize: '18px', color: COLORS.textHex, wordWrap: { width: 640 }, lineSpacing: 6, })); // Supported units + routes. const supported = state.units.filter((u) => u.homeCity === city.id) .map((u) => rules.units[u.type].name); dynamic.add(scene.add.text(left, top + 500, `Supported units (${supported.length})`, { fontFamily: FONT, fontSize: '21px', color: COLORS.accentHex, })); dynamic.add(scene.add.text(left, top + 535, supported.length ? supported.join(', ') : '(none)', { fontFamily: FONT, fontSize: '17px', color: COLORS.textHex, wordWrap: { width: 640 }, lineSpacing: 5, })); const routes = city.routes.map((r) => { const other = Logic.cityById(state, r.cityId); return other ? `${other.name} (+${r.amount})` : `(lost city +${r.amount})`; }); dynamic.add(scene.add.text(left, top + 660, `Trade routes: ${routes.length ? routes.join(', ') : '(none)'}`, { fontFamily: FONT, fontSize: '18px', color: COLORS.textHex, wordWrap: { width: 640 }, })); // --- Build picker (right column) const rx = px + 60; const cost = Logic.buildCost(rules, city); const cur = city.build.type === 'unit' ? rules.units[city.build.id] : rules.buildings[city.build.id]; const turns = y.shield > 0 ? Math.ceil((cost - city.shieldBox) / y.shield) : '∞'; dynamic.add(scene.add.text(rx, top + 64, `Building: ${cur.name} ${city.shieldBox}/${cost} shields (${turns} turns)`, { fontFamily: FONT, fontSize: '22px', color: COLORS.textHex, })); const buyPrice = Logic.buyCost(rules, city); const buyBtn = new Button(scene, rx + 560, top + 120, `BUY (${buyPrice}g)`, () => { if (Logic.buyBuild(rules, state, city)) redraw(); }, { width: 190, height: 46, fontSize: 18, variant: civ.gold >= buyPrice && !city.boughtThisTurn ? 'solid' : 'ghost' }); dynamic.add(buyBtn); dynamic.add(scene.add.text(rx, top + 108, `Treasury: ${civ.gold} gold`, { fontFamily: FONT, fontSize: '19px', color: COLORS.goldHex, })); // Choices: units then buildings, two columns of rows. const unitChoices = Logic.availableUnits(rules, state, civ, city) .map((u) => ({ type: 'unit', id: u.id, label: `${u.name} (${u.cost})`, tip: `A${u.attack} D${u.defense} M${u.move}` })); const bldChoices = Logic.availableBuildings(rules, state, civ, city) .map((b) => ({ type: 'building', id: b.id, label: `${b.name} (${b.cost})`, tip: `upkeep ${b.upkeep}` })); const choices = [...unitChoices, ...bldChoices]; const colW = 330; const rowH = 36; const perCol = 14; // leaves room below for the garrison strip choices.slice(0, perCol * 2).forEach((ch, i) => { const colX = rx + Math.floor(i / perCol) * (colW + 20); const rowY = top + 170 + (i % perCol) * rowH; const active = city.build.type === ch.type && city.build.id === ch.id; const rect = scene.add.rectangle(colX + colW / 2, rowY, colW, rowH - 4, active ? 0x3a3222 : 0x181510) .setStrokeStyle(1, active ? COLORS.gold : COLORS.muted, active ? 1 : 0.5); const txt = scene.add.text(colX + 10, rowY, ch.label, { fontFamily: FONT, fontSize: '16px', color: ch.type === 'unit' ? '#cfe3ff' : '#ffe9bd', }).setOrigin(0, 0.5); rect.setInteractive({ useHandCursor: true }); rect.on('pointerdown', () => { Logic.setBuild(rules, state, city, ch.type, ch.id); redraw(); }); dynamic.add(rect); dynamic.add(txt); }); // Garrison — units fortified here are hidden on the map (see // CivilizationMapView); clicking one un-fortifies it and closes the city. const garrison = Logic.unitsAt(state, city.x, city.y).filter((u) => u.fortified); if (garrison.length) { const gTop = top + 170 + perCol * rowH + 20; dynamic.add(scene.add.text(rx, gTop, 'Garrison (click to unfortify)', { fontFamily: FONT, fontSize: '16px', color: COLORS.mutedHex, })); const boxW = 150; const boxH = 36; const gap = 10; const perRow = 4; garrison.forEach((u, i) => { const col = i % perRow; const row = (i / perRow) | 0; const bx = rx + col * (boxW + gap) + boxW / 2; const by = gTop + 30 + row * (boxH + gap) + boxH / 2; const rect = scene.add.rectangle(bx, by, boxW, boxH, 0x181510) .setStrokeStyle(2, COLORS.gold, 1); const label = scene.add.text(bx, by, rules.units[u.type].name, { fontFamily: FONT, fontSize: '14px', color: COLORS.textHex, }).setOrigin(0.5); rect.setInteractive({ useHandCursor: true }); rect.on('pointerdown', () => { u.fortified = false; close(); }); dynamic.add(rect); dynamic.add(label); }); } } redraw(); }