Files
OpenFrontIO/src/core/execution/TransportShipExecution.ts
T
Will Dunlop eeb9f0c279 add target visualization for boat attacks (#2025)
## Description:
- Adds warship count, transport count (deployed out of maximum) to unit
display
- Adds a target that appears when a boat attack is dispatched, which
disappears when the boat attack arrives
- Updates the unit display alt text to pass through translation

## Please complete the following:

- [X] I have added screenshots for all UI updates (see below)
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file (in this case it is only alt-text)
- [X] I have added relevant tests to the test directory (n/a, fully
visual)
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

See new target effect and addition to units display 

As each transport ship arrives, the target draw stops, together with the
effect for the trail.


https://github.com/user-attachments/assets/c36c57d3-e2b7-456e-85ab-1e786bd28a07

## Please put your Discord username so you can be contacted if a bug or
regression is found:

@dxtron_28992 (my invite is still pending to dev discord)
2025-09-24 11:08:52 -07:00

243 lines
6.2 KiB
TypeScript

import {
Execution,
Game,
MessageType,
Player,
PlayerID,
TerraNullius,
Unit,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { targetTransportTile } from "../game/TransportShipUtils";
import { PathFindResultType } from "../pathfinding/AStar";
import { PathFinder } from "../pathfinding/PathFinding";
import { AttackExecution } from "./AttackExecution";
export class TransportShipExecution implements Execution {
private lastMove: number;
// TODO: make this configurable
private ticksPerMove = 1;
private active = true;
private mg: Game;
private target: Player | TerraNullius;
// TODO make private
public path: TileRef[];
private dst: TileRef | null;
private boat: Unit;
private pathFinder: PathFinder;
constructor(
private attacker: Player,
private targetID: PlayerID | null,
private ref: TileRef,
private startTroops: number,
private src: TileRef | null,
) {}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
if (this.targetID !== null && !mg.hasPlayer(this.targetID)) {
console.warn(`TransportShipExecution: target ${this.targetID} not found`);
this.active = false;
return;
}
if (!mg.isValidRef(this.ref)) {
console.warn(`TransportShipExecution: ref ${this.ref} not valid`);
this.active = false;
return;
}
if (this.src !== null && !mg.isValidRef(this.src)) {
console.warn(`TransportShipExecution: src ${this.src} not valid`);
this.active = false;
return;
}
this.lastMove = ticks;
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, true, 100);
if (
this.attacker.unitCount(UnitType.TransportShip) >=
mg.config().boatMaxNumber()
) {
mg.displayMessage(
`No boats available, max ${mg.config().boatMaxNumber()}`,
MessageType.ATTACK_FAILED,
this.attacker.id(),
);
this.active = false;
return;
}
if (
this.targetID === null ||
this.targetID === this.mg.terraNullius().id()
) {
this.target = mg.terraNullius();
} else {
this.target = mg.player(this.targetID);
}
this.startTroops ??= this.mg
.config()
.boatAttackAmount(this.attacker, this.target);
this.startTroops = Math.min(this.startTroops, this.attacker.troops());
this.dst = targetTransportTile(this.mg, this.ref);
if (this.dst === null) {
console.warn(
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
);
this.active = false;
return;
}
const closestTileSrc = this.attacker.canBuild(
UnitType.TransportShip,
this.dst,
);
if (closestTileSrc === false) {
console.warn(`can't build transport ship`);
this.active = false;
return;
}
if (this.src === null) {
// Only update the src if it's not already set
// because we assume that the src is set to the best spawn tile
this.src = closestTileSrc;
} else {
if (
this.mg.owner(this.src) !== this.attacker ||
!this.mg.isShore(this.src)
) {
console.warn(
`src is not a shore tile or not owned by: ${this.attacker.name()}`,
);
this.src = closestTileSrc;
}
}
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.src, {
troops: this.startTroops,
});
if (this.dst !== null) {
this.boat.setTargetTile(this.dst);
} else {
this.boat.setTargetTile(undefined);
}
// Notify the target player about the incoming naval invasion
if (this.targetID && this.targetID !== mg.terraNullius().id()) {
mg.displayIncomingUnit(
this.boat.id(),
// TODO TranslateText
`Naval invasion incoming from ${this.attacker.displayName()}`,
MessageType.NAVAL_INVASION_INBOUND,
this.targetID,
);
}
// Record stats
this.mg
.stats()
.boatSendTroops(this.attacker, this.target, this.boat.troops());
}
tick(ticks: number) {
if (this.dst === null) {
this.active = false;
return;
}
if (!this.active) {
return;
}
if (!this.boat.isActive()) {
this.active = false;
return;
}
if (ticks - this.lastMove < this.ticksPerMove) {
return;
}
this.lastMove = ticks;
if (this.boat.retreating()) {
this.dst = this.src!; // src is guaranteed to be set at this point
if (this.boat.targetTile() !== this.dst) {
this.boat.setTargetTile(this.dst);
}
}
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
switch (result.type) {
case PathFindResultType.Completed:
if (this.mg.owner(this.dst) === this.attacker) {
this.attacker.addTroops(this.boat.troops());
this.boat.delete(false);
this.active = false;
// Record stats
this.mg
.stats()
.boatArriveTroops(this.attacker, this.target, this.boat.troops());
return;
}
this.attacker.conquer(this.dst);
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
this.attacker.addTroops(this.boat.troops());
} else {
this.mg.addExecution(
new AttackExecution(
this.boat.troops(),
this.attacker,
this.targetID,
this.dst,
false,
),
);
}
this.boat.delete(false);
this.active = false;
// Record stats
this.mg
.stats()
.boatArriveTroops(this.attacker, this.target, this.boat.troops());
return;
case PathFindResultType.NextTile:
this.boat.move(result.node);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
// TODO: add to poisoned port list
console.warn(`path not found to dst`);
this.attacker.addTroops(this.boat.troops());
this.boat.delete(false);
this.active = false;
return;
}
}
owner(): Player {
return this.attacker;
}
isActive(): boolean {
return this.active;
}
}