feat: add garrison system and improve city interaction flow
- Hide player's fortified units inside cities on the map to reduce clutter - Add garrison strip in city screen to view/unfortify garrisoned units - Add "move or inspect" dialog when selecting a city with a movable unit - Refactor moveTo into reusable method for cleaner click handling - Make confirmDialog accept custom button labels - Add civilization-terrain.png sprite sheet for terrain rendering
This commit is contained in:
parent
67a7ac857e
commit
9f7bac9787
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
Binary file not shown.
|
|
@ -136,7 +136,7 @@ export function openCityScreen(scene, rules, state, city, onClose) {
|
|||
const choices = [...unitChoices, ...bldChoices];
|
||||
const colW = 330;
|
||||
const rowH = 36;
|
||||
const perCol = 17;
|
||||
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;
|
||||
|
|
@ -156,6 +156,35 @@ export function openCityScreen(scene, rules, state, city, onClose) {
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -389,9 +389,21 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
onTileClick(c, r) {
|
||||
const state = this.state;
|
||||
const human = state.humanIndex;
|
||||
const myUnits = Logic.unitsAt(state, c, r).filter((u) => u.civ === human);
|
||||
const city = Logic.cityAt(state, c, r);
|
||||
// Fortified units in a city are hidden on the map (see CivilizationMapView) —
|
||||
// clicking the tile should open the city, not select an invisible unit.
|
||||
const myUnits = Logic.unitsAt(state, c, r)
|
||||
.filter((u) => u.civ === human && !(city && u.fortified));
|
||||
const sel = this.selectedUnit();
|
||||
|
||||
// A movable selected unit clicking a different owned city is ambiguous —
|
||||
// ask whether to move it in or just inspect the city.
|
||||
if (city && city.civ === human && sel && sel.mp > 0 && state.current === human
|
||||
&& (sel.x !== c || sel.y !== r)) {
|
||||
this.confirmMoveOrInspectCity(sel, city, c, r);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myUnits.length && (!sel || sel.x !== c || sel.y !== r)) {
|
||||
this.selectUnit(myUnits[0]);
|
||||
return;
|
||||
|
|
@ -402,22 +414,32 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
this.selectUnit(myUnits[(i + 1) % myUnits.length]);
|
||||
return;
|
||||
}
|
||||
const city = Logic.cityAt(state, c, r);
|
||||
if (city && city.civ === human && !myUnits.length) {
|
||||
this.onCityClick(city);
|
||||
return;
|
||||
}
|
||||
// Move the selected unit toward the clicked tile.
|
||||
if (sel && state.current === human) {
|
||||
if (Logic.cheb(sel.x, sel.y, c, r) === 1) {
|
||||
this.tryStep(sel, c - sel.x, r - sel.y);
|
||||
return;
|
||||
}
|
||||
const path = Logic.findPath(this.rules, state, sel, c, r);
|
||||
if (!path) { this.toast('No route there'); return; }
|
||||
this.view.showPath(path);
|
||||
this.walkPath(sel, path);
|
||||
if (sel) this.moveTo(sel, c, r);
|
||||
}
|
||||
|
||||
moveTo(unit, c, r) {
|
||||
if (this.state.current !== this.state.humanIndex) return;
|
||||
if (Logic.cheb(unit.x, unit.y, c, r) === 1) {
|
||||
this.tryStep(unit, c - unit.x, r - unit.y);
|
||||
return;
|
||||
}
|
||||
const path = Logic.findPath(this.rules, this.state, unit, c, r);
|
||||
if (!path) { this.toast('No route there'); return; }
|
||||
this.view.showPath(path);
|
||||
this.walkPath(unit, path);
|
||||
}
|
||||
|
||||
confirmMoveOrInspectCity(unit, city, c, r) {
|
||||
const name = this.rules.units[unit.type].name;
|
||||
this.confirmDialog(`Move ${name} into ${city.name}, or inspect the city?`,
|
||||
() => this.moveTo(unit, c, r),
|
||||
() => this.onCityClick(city),
|
||||
'MOVE', 'INSPECT');
|
||||
}
|
||||
|
||||
onCityClick(city) {
|
||||
|
|
@ -736,7 +758,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
this.confirmDialog(`Attack ${name}? This means WAR!`, onYes, () => {});
|
||||
}
|
||||
|
||||
confirmDialog(message, onYes, onNo) {
|
||||
confirmDialog(message, onYes, onNo, yesLabel = 'YES', noLabel = 'NO') {
|
||||
this.modalOpen = true;
|
||||
const root = this.add.container(0, 0).setDepth(D.modal);
|
||||
const dim = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55)
|
||||
|
|
@ -748,9 +770,9 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
wordWrap: { width: 580 }, align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
const close = (fn) => () => { root.destroy(true); this.modalOpen = false; fn?.(); };
|
||||
const yes = new Button(this, GAME_WIDTH / 2 - 110, GAME_HEIGHT / 2 + 50, 'YES', close(onYes),
|
||||
const yes = new Button(this, GAME_WIDTH / 2 - 110, GAME_HEIGHT / 2 + 50, yesLabel, close(onYes),
|
||||
{ width: 180, height: 52 });
|
||||
const no = new Button(this, GAME_WIDTH / 2 + 110, GAME_HEIGHT / 2 + 50, 'NO', close(onNo),
|
||||
const no = new Button(this, GAME_WIDTH / 2 + 110, GAME_HEIGHT / 2 + 50, noLabel, close(onNo),
|
||||
{ width: 180, height: 52, variant: 'ghost' });
|
||||
root.add([dim, panel, txt, yes, no]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,17 +444,22 @@ export class CivilizationMapView {
|
|||
const visible = computeVisible(state, povIdx);
|
||||
const explored = this.humanIdx >= 0 ? state.explored[povIdx] : null;
|
||||
|
||||
const cityTiles = new Set();
|
||||
for (const city of state.cities) {
|
||||
const idx = tileIndex(state.world, city.x, city.y);
|
||||
cityTiles.add(idx);
|
||||
if (explored && !explored[idx]) continue;
|
||||
this.drawCity(city);
|
||||
}
|
||||
|
||||
// One marker per occupied tile (top defender), with a stack badge.
|
||||
// Own units fortified inside a city are tucked away instead of drawn —
|
||||
// they no longer overlap the city graphic; see them via onCityClick.
|
||||
const byTile = new Map();
|
||||
for (const u of state.units) {
|
||||
if (u.carriedBy) continue;
|
||||
const idx = tileIndex(state.world, u.x, u.y);
|
||||
if (u.fortified && u.civ === povIdx && cityTiles.has(idx)) continue;
|
||||
if (u.civ !== povIdx) {
|
||||
if (!visible.has(idx)) continue;
|
||||
if (!isUnitVisibleTo(rules, state, u, povIdx)) continue;
|
||||
|
|
|
|||
Loading…
Reference in New Issue