fertig-classic-games/src/games/civilization/CivilizationCityScreen.js

192 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
}