feat: add bulk disembark for transport units
- Add disembark action to transport unit action menu when carrying units - Highlight valid landing tiles adjacent to boats (clear of enemies) - Unload all passengers onto a single chosen tile at once - Implement targeting mode with cancel support (Escape or clicking away) - Extract unitsOnBoat and disembarkTiles logic functions - Add green diamond tile highlights in view layer
This commit is contained in:
parent
a91db38a0f
commit
bace0356c2
|
|
@ -70,6 +70,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
this.logScrollY = 0;
|
||||
this.statusQueue = [];
|
||||
this.pendingCityAttacks = [];
|
||||
this.disembarkPending = null;
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -104,6 +105,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
onEscape() {
|
||||
if (this.disembarkPending) { this.cancelDisembark(); return; }
|
||||
if (this.modalOpen) return; // modals close themselves
|
||||
if (this.phase === 'playing') this.openMenu();
|
||||
else this.scene.start('GameMenu');
|
||||
|
|
@ -671,6 +673,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
onTileClick(c, r) {
|
||||
if (this.disembarkPending) { this.resolveDisembarkClick(c, r); return; }
|
||||
const state = this.state;
|
||||
const human = state.humanIndex;
|
||||
const city = Logic.cityAt(state, c, r);
|
||||
|
|
@ -807,6 +810,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
selectUnit(unit) {
|
||||
this.cancelDisembark();
|
||||
this.selectedUnitId = unit?.id ?? null;
|
||||
this.view.selectedUnitId = this.selectedUnitId;
|
||||
this.view.showPath(null);
|
||||
|
|
@ -1030,6 +1034,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
|
||||
onUnitClick(unit) {
|
||||
if (this.modalOpen || this.busy || this.state.current !== this.state.humanIndex) return;
|
||||
this.cancelDisembark();
|
||||
this.openUnitActionMenu(unit);
|
||||
}
|
||||
|
||||
|
|
@ -1064,10 +1069,52 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
if (Logic.canEstablishRoute(rules, state, unit)) {
|
||||
actions.push({ label: 'Establish Trade Route', onSelect: () => this.tryCaravan(unit) });
|
||||
}
|
||||
if (Logic.unitsOnBoat(state, unit).length) {
|
||||
const tiles = Logic.disembarkTiles(rules, state, unit);
|
||||
if (tiles.length) {
|
||||
actions.push({ label: 'Disembark', onSelect: () => this.beginDisembark(unit, tiles) });
|
||||
}
|
||||
}
|
||||
actions.push({ label: 'Skip Turn', onSelect: () => { unit.mp = 0; this.selectNextUnit(); } });
|
||||
return actions;
|
||||
}
|
||||
|
||||
// "Disembark" arms target-picking mode: the next tile click either unloads
|
||||
// the whole hold there (if it's one of the highlighted candidate tiles) or
|
||||
// just cancels, same as clicking away from any other targeting cursor.
|
||||
beginDisembark(boat, tiles) {
|
||||
this.disembarkPending = { boatId: boat.id, tiles };
|
||||
this.view.showDisembarkTiles(tiles);
|
||||
}
|
||||
|
||||
cancelDisembark() {
|
||||
if (!this.disembarkPending) return;
|
||||
this.disembarkPending = null;
|
||||
this.view?.showDisembarkTiles(null);
|
||||
}
|
||||
|
||||
resolveDisembarkClick(c, r) {
|
||||
const { boatId, tiles } = this.disembarkPending;
|
||||
this.cancelDisembark();
|
||||
const match = tiles.find((t) => t.x === c && t.y === r);
|
||||
if (!match) return;
|
||||
const boat = Logic.unitById(this.state, boatId);
|
||||
if (!boat) return;
|
||||
this.tryDisembarkAll(boat, match.dx, match.dy);
|
||||
}
|
||||
|
||||
// disembarkTiles() only ever offers tiles clear of foreign cities/units, so
|
||||
// every passenger's individual disembark() below is guaranteed to succeed —
|
||||
// no war-confirmation branch needed here (contrast tryStep's single-unit
|
||||
// path, which can walk a unit into a tile that does need one).
|
||||
tryDisembarkAll(boat, dx, dy) {
|
||||
if (this.state.current !== this.state.humanIndex) return;
|
||||
for (const u of Logic.unitsOnBoat(this.state, boat)) {
|
||||
Logic.disembark(this.rules, this.state, u, dx, dy);
|
||||
}
|
||||
this.afterAction();
|
||||
}
|
||||
|
||||
openUnitActionMenu(unit) {
|
||||
const def = this.rules.units[unit.type];
|
||||
this.openActionListMenu(def.name, this.getUnitActions(unit));
|
||||
|
|
@ -1191,6 +1238,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
|||
onEndTurn() {
|
||||
if (this.busy || this.modalOpen || this.phase !== 'playing') return;
|
||||
if (this.state.current !== this.state.humanIndex) return;
|
||||
this.cancelDisembark();
|
||||
Logic.endCivTurn(this.rules, this.state, this.state.humanIndex);
|
||||
this.saveGame();
|
||||
this.runToHumanTurn();
|
||||
|
|
|
|||
|
|
@ -810,8 +810,12 @@ function spendMove(unit, cost) {
|
|||
unit.moved = true;
|
||||
}
|
||||
|
||||
export function unitsOnBoat(state, boat) {
|
||||
return state.units.filter((u) => u.carriedBy === boat.id);
|
||||
}
|
||||
|
||||
function cargoCount(state, boat) {
|
||||
return state.units.filter((u) => u.carriedBy === boat.id).length;
|
||||
return unitsOnBoat(state, boat).length;
|
||||
}
|
||||
|
||||
function dropCarried(rules, state, unit, x, y) {
|
||||
|
|
@ -836,6 +840,29 @@ export function disembark(rules, state, unit, dx, dy) {
|
|||
return out;
|
||||
}
|
||||
|
||||
const ADJACENT_DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]];
|
||||
|
||||
// Land tiles next to `boat` that are clear of any foreign city/units — used
|
||||
// to offer the human player a "Disembark" action (see CivilizationGame.js)
|
||||
// that unloads the whole hold onto a single chosen tile at once. Kept clear
|
||||
// of enemies so it never surprises the player with a fight; attacking off a
|
||||
// transport is still possible unit-by-unit via the AI's own logic, just not
|
||||
// through this bulk convenience command.
|
||||
export function disembarkTiles(rules, state, boat) {
|
||||
const tiles = [];
|
||||
for (const [dx, dy] of ADJACENT_DIRS) {
|
||||
const x = boat.x + dx;
|
||||
const y = boat.y + dy;
|
||||
if (!inBounds(state.world, x, y)) continue;
|
||||
if (terrainAt(rules, state.world, x, y).water) continue;
|
||||
const city = cityAt(state, x, y);
|
||||
if (city && city.civ !== boat.civ) continue;
|
||||
if (unitsAt(state, x, y).some((u) => u.civ !== boat.civ)) continue;
|
||||
tiles.push({ x, y, dx, dy });
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
function resolveHut(rules, state, unit) {
|
||||
const civ = state.civs[unit.civ];
|
||||
const roll = rand(state);
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@ export class CivilizationMapView {
|
|||
}
|
||||
}
|
||||
this.pathGfx = scene.add.graphics();
|
||||
this.disembarkGfx = scene.add.graphics();
|
||||
this.dynamic = scene.add.container(0, 0);
|
||||
this.root.add([
|
||||
...this.terrainChunks.map((c) => c.rt),
|
||||
this.pathGfx, this.dynamic,
|
||||
this.pathGfx, this.disembarkGfx, this.dynamic,
|
||||
...this.fogChunks.map((c) => c.rt),
|
||||
]);
|
||||
|
||||
|
|
@ -968,6 +969,30 @@ export class CivilizationMapView {
|
|||
}
|
||||
}
|
||||
|
||||
// Highlights candidate landing tiles while a "Disembark" action is armed
|
||||
// (see CivilizationGame.js) — pass null/empty to clear. Diamond shape
|
||||
// mirrors paintProceduralTile's tile stamp so the outline sits flush with
|
||||
// the tile underneath it.
|
||||
showDisembarkTiles(tiles) {
|
||||
this.disembarkGfx.clear();
|
||||
if (!tiles || !tiles.length) return;
|
||||
const cy = TILE_H / 2;
|
||||
this.disembarkGfx.lineStyle(3, 0x4a9e44, 0.95);
|
||||
this.disembarkGfx.fillStyle(0x4a9e44, 0.22);
|
||||
for (const { x: c, y: r } of tiles) {
|
||||
const x = this.isoX(c, r);
|
||||
const y = this.isoY(c, r);
|
||||
this.disembarkGfx.beginPath();
|
||||
this.disembarkGfx.moveTo(x, y + cy - TILE_H / 2);
|
||||
this.disembarkGfx.lineTo(x + TILE_W / 2, y + cy);
|
||||
this.disembarkGfx.lineTo(x, y + cy + TILE_H / 2);
|
||||
this.disembarkGfx.lineTo(x - TILE_W / 2, y + cy);
|
||||
this.disembarkGfx.closePath();
|
||||
this.disembarkGfx.fillPath();
|
||||
this.disembarkGfx.strokePath();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Minimap
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue