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.logScrollY = 0;
|
||||||
this.statusQueue = [];
|
this.statusQueue = [];
|
||||||
this.pendingCityAttacks = [];
|
this.pendingCityAttacks = [];
|
||||||
|
this.disembarkPending = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -104,6 +105,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEscape() {
|
onEscape() {
|
||||||
|
if (this.disembarkPending) { this.cancelDisembark(); return; }
|
||||||
if (this.modalOpen) return; // modals close themselves
|
if (this.modalOpen) return; // modals close themselves
|
||||||
if (this.phase === 'playing') this.openMenu();
|
if (this.phase === 'playing') this.openMenu();
|
||||||
else this.scene.start('GameMenu');
|
else this.scene.start('GameMenu');
|
||||||
|
|
@ -671,6 +673,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
onTileClick(c, r) {
|
onTileClick(c, r) {
|
||||||
|
if (this.disembarkPending) { this.resolveDisembarkClick(c, r); return; }
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
const human = state.humanIndex;
|
const human = state.humanIndex;
|
||||||
const city = Logic.cityAt(state, c, r);
|
const city = Logic.cityAt(state, c, r);
|
||||||
|
|
@ -807,6 +810,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectUnit(unit) {
|
selectUnit(unit) {
|
||||||
|
this.cancelDisembark();
|
||||||
this.selectedUnitId = unit?.id ?? null;
|
this.selectedUnitId = unit?.id ?? null;
|
||||||
this.view.selectedUnitId = this.selectedUnitId;
|
this.view.selectedUnitId = this.selectedUnitId;
|
||||||
this.view.showPath(null);
|
this.view.showPath(null);
|
||||||
|
|
@ -1030,6 +1034,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
|
|
||||||
onUnitClick(unit) {
|
onUnitClick(unit) {
|
||||||
if (this.modalOpen || this.busy || this.state.current !== this.state.humanIndex) return;
|
if (this.modalOpen || this.busy || this.state.current !== this.state.humanIndex) return;
|
||||||
|
this.cancelDisembark();
|
||||||
this.openUnitActionMenu(unit);
|
this.openUnitActionMenu(unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1064,10 +1069,52 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
if (Logic.canEstablishRoute(rules, state, unit)) {
|
if (Logic.canEstablishRoute(rules, state, unit)) {
|
||||||
actions.push({ label: 'Establish Trade Route', onSelect: () => this.tryCaravan(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(); } });
|
actions.push({ label: 'Skip Turn', onSelect: () => { unit.mp = 0; this.selectNextUnit(); } });
|
||||||
return actions;
|
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) {
|
openUnitActionMenu(unit) {
|
||||||
const def = this.rules.units[unit.type];
|
const def = this.rules.units[unit.type];
|
||||||
this.openActionListMenu(def.name, this.getUnitActions(unit));
|
this.openActionListMenu(def.name, this.getUnitActions(unit));
|
||||||
|
|
@ -1191,6 +1238,7 @@ export default class CivilizationGame extends Phaser.Scene {
|
||||||
onEndTurn() {
|
onEndTurn() {
|
||||||
if (this.busy || this.modalOpen || this.phase !== 'playing') return;
|
if (this.busy || this.modalOpen || this.phase !== 'playing') return;
|
||||||
if (this.state.current !== this.state.humanIndex) return;
|
if (this.state.current !== this.state.humanIndex) return;
|
||||||
|
this.cancelDisembark();
|
||||||
Logic.endCivTurn(this.rules, this.state, this.state.humanIndex);
|
Logic.endCivTurn(this.rules, this.state, this.state.humanIndex);
|
||||||
this.saveGame();
|
this.saveGame();
|
||||||
this.runToHumanTurn();
|
this.runToHumanTurn();
|
||||||
|
|
|
||||||
|
|
@ -810,8 +810,12 @@ function spendMove(unit, cost) {
|
||||||
unit.moved = true;
|
unit.moved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unitsOnBoat(state, boat) {
|
||||||
|
return state.units.filter((u) => u.carriedBy === boat.id);
|
||||||
|
}
|
||||||
|
|
||||||
function cargoCount(state, boat) {
|
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) {
|
function dropCarried(rules, state, unit, x, y) {
|
||||||
|
|
@ -836,6 +840,29 @@ export function disembark(rules, state, unit, dx, dy) {
|
||||||
return out;
|
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) {
|
function resolveHut(rules, state, unit) {
|
||||||
const civ = state.civs[unit.civ];
|
const civ = state.civs[unit.civ];
|
||||||
const roll = rand(state);
|
const roll = rand(state);
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,11 @@ export class CivilizationMapView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.pathGfx = scene.add.graphics();
|
this.pathGfx = scene.add.graphics();
|
||||||
|
this.disembarkGfx = scene.add.graphics();
|
||||||
this.dynamic = scene.add.container(0, 0);
|
this.dynamic = scene.add.container(0, 0);
|
||||||
this.root.add([
|
this.root.add([
|
||||||
...this.terrainChunks.map((c) => c.rt),
|
...this.terrainChunks.map((c) => c.rt),
|
||||||
this.pathGfx, this.dynamic,
|
this.pathGfx, this.disembarkGfx, this.dynamic,
|
||||||
...this.fogChunks.map((c) => c.rt),
|
...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
|
// Minimap
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue