192 lines
8.7 KiB
JavaScript
192 lines
8.7 KiB
JavaScript
// 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();
|
||
}
|