mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
refactor & update warships (#796)
## Description: 1. Refactor WarshipExecution so that it takes either attrs or a warship unit. This makes testing much simpler as the unit test can construct a warship and then pass it into a warship execution 2. Have MoveWarshipExecution set the patrol tile, not the move tile so warships stay in new location instead of moving back to original location. 3. Warships no longer target trade ships outside of its patrol range. this prevents warships from wandering 4. Refactored & simplified WarshipExecution 5. Added more tests for warships 6. Move health modification from PlayerExecution to WarshipExecution since Warships are the only unit that have health 7. Move fields from WarshipExecution to the Warship unit itself, this allows other executions & components to see more data about the warship. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: <DISCORD USERNAME>
This commit is contained in:
@@ -104,7 +104,9 @@ export class ConstructionExecution implements Execution {
|
||||
this.mg.addExecution(new MirvExecution(player.id(), this.tile));
|
||||
break;
|
||||
case UnitType.Warship:
|
||||
this.mg.addExecution(new WarshipExecution(player.id(), this.tile));
|
||||
this.mg.addExecution(
|
||||
new WarshipExecution({ owner: player, patrolTile: this.tile }),
|
||||
);
|
||||
break;
|
||||
case UnitType.Port:
|
||||
this.mg.addExecution(new PortExecution(player.id(), this.tile));
|
||||
|
||||
@@ -24,7 +24,8 @@ export class MoveWarshipExecution implements Execution {
|
||||
console.log("MoveWarshipExecution: warship is already dead");
|
||||
return;
|
||||
}
|
||||
warship.setTargetTile(this.position);
|
||||
warship.setPatrolTile(this.position);
|
||||
warship.setTargetTile(undefined);
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,17 +46,12 @@ export class PlayerExecution implements Execution {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
this.player.decayRelations();
|
||||
const hasPort = this.player.units(UnitType.Port).length > 0;
|
||||
this.player.units().forEach((u) => {
|
||||
if (hasPort && u.type() === UnitType.Warship) {
|
||||
u.modifyHealth(1);
|
||||
}
|
||||
if (this.mg === null) return;
|
||||
const tileOwner = this.mg.owner(u.tile());
|
||||
const tileOwner = this.mg!.owner(u.tile());
|
||||
if (u.info().territoryBound) {
|
||||
if (tileOwner.isPlayer()) {
|
||||
if (tileOwner !== this.player) {
|
||||
this.mg.player(tileOwner.id()).captureUnit(u);
|
||||
this.mg!.player(tileOwner.id()).captureUnit(u);
|
||||
}
|
||||
} else {
|
||||
u.delete();
|
||||
|
||||
@@ -50,7 +50,7 @@ export class TradeShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, spawn, {
|
||||
dstPort: this._dstPort,
|
||||
targetUnit: this._dstPort,
|
||||
lastSetSafeFromPirates: ticks,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import { consolex } from "../Consolex";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
isUnit,
|
||||
OwnerComp,
|
||||
Unit,
|
||||
UnitParams,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
@@ -15,178 +16,91 @@ import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
export class WarshipExecution implements Execution {
|
||||
private random: PseudoRandom;
|
||||
|
||||
private _owner: Player;
|
||||
private active = true;
|
||||
private warship: Unit | null = null;
|
||||
private warship: Unit;
|
||||
private mg: Game;
|
||||
|
||||
private target: Unit | undefined = undefined;
|
||||
private pathfinder: PathFinder | null = null;
|
||||
|
||||
private patrolTile: TileRef | undefined;
|
||||
|
||||
private pathfinder: PathFinder;
|
||||
private lastShellAttack = 0;
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
private input: (UnitParams<UnitType.Warship> & OwnerComp) | Unit,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.log(`WarshipExecution: player ${this.playerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000);
|
||||
this._owner = mg.player(this.playerID);
|
||||
this.patrolTile = this.patrolCenterTile;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
|
||||
// Only for warships with "moveTarget" set
|
||||
goToMoveTarget(target: TileRef) {
|
||||
if (this.warship === null || this.pathfinder === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
const result = this.pathfinder.nextTile(this.warship.tile(), target);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.warship.setTargetTile(undefined);
|
||||
this.warship.touch();
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.warship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
this.warship.touch();
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (
|
||||
this.mg === null ||
|
||||
this.warship === null ||
|
||||
this.target === undefined
|
||||
) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
new ShellExecution(
|
||||
this.warship.tile(),
|
||||
this.warship.owner(),
|
||||
this.warship,
|
||||
this.target,
|
||||
),
|
||||
if (isUnit(this.input)) {
|
||||
this.warship = this.input;
|
||||
} else {
|
||||
const spawn = this.input.owner.canBuild(
|
||||
UnitType.Warship,
|
||||
this.input.patrolTile,
|
||||
);
|
||||
if (!this.target.hasHealth()) {
|
||||
// Don't send multiple shells to target that can be oneshotted
|
||||
this.alreadySentShell.add(this.target);
|
||||
this.target = undefined;
|
||||
if (spawn === false) {
|
||||
console.warn(
|
||||
`Failed to spawn warship for ${this.input.owner.name()} at ${this.input.patrolTile}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
if (this.warship === null || this.pathfinder === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
if (this.patrolTile === undefined) {
|
||||
this.patrolTile = this.randomTile();
|
||||
if (this.patrolTile === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.warship.setTargetUnit(this.target);
|
||||
if (
|
||||
this.target === undefined ||
|
||||
this.target.type() !== UnitType.TradeShip
|
||||
) {
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.patrolTile,
|
||||
this.warship = this.input.owner.buildUnit(
|
||||
UnitType.Warship,
|
||||
spawn,
|
||||
this.input,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.patrolTile = undefined;
|
||||
this.warship.touch();
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
this.warship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
this.warship.touch();
|
||||
return;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to patrol tile`);
|
||||
this.patrolTile = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.pathfinder === null) throw new Error("Warship not initialized");
|
||||
if (this.warship === null) {
|
||||
if (this.patrolTile === undefined) {
|
||||
console.log(
|
||||
`WarshipExecution: no patrol tile for ${this._owner.name()}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile);
|
||||
if (spawn === false) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.warship = this._owner.buildUnit(UnitType.Warship, spawn, {});
|
||||
if (this.warship.health() <= 0) {
|
||||
this.warship.delete();
|
||||
return;
|
||||
}
|
||||
if (!this.warship.isActive()) {
|
||||
this.active = false;
|
||||
const hasPort = this.warship.owner().units(UnitType.Port).length > 0;
|
||||
if (hasPort) {
|
||||
this.warship.modifyHealth(1);
|
||||
}
|
||||
|
||||
this.warship.setTargetUnit(this.findTargetUnit());
|
||||
if (this.warship.targetUnit()?.type() === UnitType.TradeShip) {
|
||||
this.huntDownTradeShip();
|
||||
return;
|
||||
}
|
||||
if (this.target !== undefined && !this.target.isActive()) {
|
||||
this.target = undefined;
|
||||
|
||||
this.patrol();
|
||||
|
||||
if (this.warship.targetUnit() !== undefined) {
|
||||
this.shootTarget();
|
||||
return;
|
||||
}
|
||||
const hasPort = this._owner.units(UnitType.Port).length > 0;
|
||||
const warship = this.warship;
|
||||
if (warship === undefined) throw new Error("Warship not initialized");
|
||||
}
|
||||
|
||||
private findTargetUnit(): Unit | undefined {
|
||||
const hasPort = this.warship.owner().units(UnitType.Port).length > 0;
|
||||
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
|
||||
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.warship.tile(),
|
||||
this.warship.patrolTile()!,
|
||||
this.mg.config().warshipTargettingRange(),
|
||||
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
||||
)
|
||||
.filter(
|
||||
({ unit }) =>
|
||||
unit.owner() !== warship.owner() &&
|
||||
unit !== warship &&
|
||||
!unit.owner().isFriendly(warship.owner()) &&
|
||||
unit.owner() !== this.warship.owner() &&
|
||||
unit !== this.warship &&
|
||||
!unit.owner().isFriendly(this.warship.owner()) &&
|
||||
!this.alreadySentShell.has(unit) &&
|
||||
(unit.type() !== UnitType.TradeShip ||
|
||||
(hasPort &&
|
||||
this.warship !== null &&
|
||||
this.mg.euclideanDistSquared(this.warship.tile(), unit.tile()) <=
|
||||
patrolRangeSquared &&
|
||||
unit.targetUnit()?.owner() !== this.warship.owner() &&
|
||||
!unit.targetUnit()?.owner().isFriendly(this.warship.owner()) &&
|
||||
unit.isSafeFromPirates() !== true)),
|
||||
);
|
||||
|
||||
this.target = ships.sort((a, b) => {
|
||||
return ships.sort((a, b) => {
|
||||
const { unit: unitA, distSquared: distA } = a;
|
||||
const { unit: unitB, distSquared: distB } = b;
|
||||
|
||||
@@ -217,60 +131,48 @@ export class WarshipExecution implements Execution {
|
||||
// If both are the same type, sort by distance (lower `distSquared` means closer)
|
||||
return distA - distB;
|
||||
})[0]?.unit;
|
||||
}
|
||||
|
||||
const moveTarget = this.warship.targetTile();
|
||||
if (moveTarget) {
|
||||
this.goToMoveTarget(moveTarget);
|
||||
// If we have a "move target" then we cannot target trade ships as it
|
||||
// requires moving.
|
||||
if (this.target && this.target.type() === UnitType.TradeShip) {
|
||||
this.target = undefined;
|
||||
private shootTarget() {
|
||||
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
new ShellExecution(
|
||||
this.warship.tile(),
|
||||
this.warship.owner(),
|
||||
this.warship,
|
||||
this.warship.targetUnit()!,
|
||||
),
|
||||
);
|
||||
if (!this.warship.targetUnit()!.hasHealth()) {
|
||||
// Don't send multiple shells to target that can be oneshotted
|
||||
this.alreadySentShell.add(this.warship.targetUnit()!);
|
||||
this.warship.setTargetUnit(undefined);
|
||||
return;
|
||||
}
|
||||
} else if (!this.target || this.target.type() !== UnitType.TradeShip) {
|
||||
this.patrol();
|
||||
}
|
||||
|
||||
if (
|
||||
this.target === undefined ||
|
||||
!this.target.isActive() ||
|
||||
this.target.owner() === this._owner ||
|
||||
this.target.isSafeFromPirates() === true
|
||||
) {
|
||||
// In case another warship captured or destroyed target, or the target escaped into safe waters
|
||||
this.target = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.warship.setTargetUnit(this.target);
|
||||
|
||||
// If we have a move target we do not want to go after trading ships
|
||||
if (!this.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.target.type() !== UnitType.TradeShip) {
|
||||
this.shoot();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private huntDownTradeShip() {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// target is trade ship so capture it.
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.target.tile(),
|
||||
this.warship.targetUnit()!.tile(),
|
||||
5,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this._owner.captureUnit(this.target);
|
||||
this.target = undefined;
|
||||
this.warship.owner().captureUnit(this.warship.targetUnit()!);
|
||||
this.warship.setTargetUnit(undefined);
|
||||
this.warship.move(this.warship.tile());
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.warship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
this.warship.move(this.warship.tile());
|
||||
this.warship.touch();
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.log(`path not found to target`);
|
||||
@@ -279,8 +181,38 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
this.warship.setTargetTile(this.randomTile());
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.warship.targetTile()!,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.warship.setTargetTile(undefined);
|
||||
this.warship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
this.warship.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.Pending:
|
||||
this.warship.touch();
|
||||
return;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(`path not found to target tile`);
|
||||
this.warship.setTargetTile(undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
return this.warship?.isActive();
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -288,19 +220,16 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
randomTile(allowShoreline: boolean = false): TileRef | undefined {
|
||||
if (this.mg === null) {
|
||||
throw new Error("Warship not initialized");
|
||||
}
|
||||
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
|
||||
const maxAttemptBeforeExpand: number = 500;
|
||||
let attempts: number = 0;
|
||||
let expandCount: number = 0;
|
||||
while (expandCount < 3) {
|
||||
const x =
|
||||
this.mg.x(this.patrolCenterTile) +
|
||||
this.mg.x(this.warship.patrolTile()!) +
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
const y =
|
||||
this.mg.y(this.patrolCenterTile) +
|
||||
this.mg.y(this.warship.patrolTile()!) +
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
@@ -322,7 +251,7 @@ export class WarshipExecution implements Execution {
|
||||
return tile;
|
||||
}
|
||||
console.warn(
|
||||
`Failed to find random tile for warship for ${this._owner.name()}`,
|
||||
`Failed to find random tile for warship for ${this.warship.owner().name()}`,
|
||||
);
|
||||
if (!allowShoreline) {
|
||||
// If we failed to find a tile on the ocean, try again but allow shoreline
|
||||
|
||||
+18
-3
@@ -152,13 +152,19 @@ export enum UnitType {
|
||||
Construction = "Construction",
|
||||
}
|
||||
|
||||
export interface OwnerComp {
|
||||
owner: Player;
|
||||
}
|
||||
|
||||
export interface UnitParamsMap {
|
||||
[UnitType.TransportShip]: {
|
||||
troops?: number;
|
||||
destination?: TileRef;
|
||||
};
|
||||
|
||||
[UnitType.Warship]: {};
|
||||
[UnitType.Warship]: {
|
||||
patrolTile: TileRef;
|
||||
};
|
||||
|
||||
[UnitType.Shell]: {};
|
||||
|
||||
@@ -175,7 +181,7 @@ export interface UnitParamsMap {
|
||||
};
|
||||
|
||||
[UnitType.TradeShip]: {
|
||||
dstPort: Unit;
|
||||
targetUnit: Unit;
|
||||
lastSetSafeFromPirates?: number;
|
||||
};
|
||||
|
||||
@@ -334,8 +340,12 @@ export class PlayerInfo {
|
||||
}
|
||||
}
|
||||
|
||||
export function isUnit(unit: Unit | UnitParams<UnitType>): unit is Unit {
|
||||
return "isUnit" in unit && typeof unit.isUnit === "function" && unit.isUnit();
|
||||
}
|
||||
|
||||
export interface Unit {
|
||||
hash(): number;
|
||||
isUnit(): this is Unit;
|
||||
|
||||
// Common properties.
|
||||
id(): number;
|
||||
@@ -349,6 +359,7 @@ export interface Unit {
|
||||
isActive(): boolean;
|
||||
setOwner(owner: Player): void;
|
||||
touch(): void;
|
||||
hash(): number;
|
||||
toUpdate(): UnitUpdate;
|
||||
|
||||
// Targeting
|
||||
@@ -387,6 +398,10 @@ export interface Unit {
|
||||
constructionType(): UnitType | null;
|
||||
setConstructionType(type: UnitType): void;
|
||||
|
||||
// Warships
|
||||
setPatrolTile(tile: TileRef): void;
|
||||
patrolTile(): TileRef | undefined;
|
||||
|
||||
// Ports
|
||||
cachePut(from: TileRef, to: TileRef): void;
|
||||
cacheGet(from: TileRef): TileRef | undefined;
|
||||
|
||||
@@ -286,8 +286,8 @@ export class GameImpl implements Game {
|
||||
this.updates = createGameUpdatesMap();
|
||||
this.execs.forEach((e) => {
|
||||
if (
|
||||
e.isActive() &&
|
||||
(!this.inSpawnPhase() || e.activeDuringSpawnPhase())
|
||||
(!this.inSpawnPhase() || e.activeDuringSpawnPhase()) &&
|
||||
e.isActive()
|
||||
) {
|
||||
e.tick(this._ticks);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class UnitImpl implements Unit {
|
||||
private _troops: number;
|
||||
private _cooldownStartTick: Tick | null = null;
|
||||
private _pathCache: Map<TileRef, TileRef> = new Map();
|
||||
|
||||
private _patrolTile: TileRef | undefined;
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
private mg: GameImpl,
|
||||
@@ -46,6 +46,10 @@ export class UnitImpl implements Unit {
|
||||
"lastSetSafeFromPirates" in params
|
||||
? (params.lastSetSafeFromPirates ?? 0)
|
||||
: 0;
|
||||
this._patrolTile =
|
||||
"patrolTile" in params ? (params.patrolTile ?? undefined) : undefined;
|
||||
this._targetUnit =
|
||||
"targetUnit" in params ? (params.targetUnit ?? undefined) : undefined;
|
||||
|
||||
switch (this._type) {
|
||||
case UnitType.Warship:
|
||||
@@ -57,6 +61,19 @@ export class UnitImpl implements Unit {
|
||||
this.mg.stats().unitBuild(_owner, this._type);
|
||||
}
|
||||
}
|
||||
|
||||
setPatrolTile(tile: TileRef): void {
|
||||
this._patrolTile = tile;
|
||||
}
|
||||
|
||||
patrolTile(): TileRef | undefined {
|
||||
return this._patrolTile;
|
||||
}
|
||||
|
||||
isUnit(): this is Unit {
|
||||
return true;
|
||||
}
|
||||
|
||||
touch(): void {
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
+130
-60
@@ -1,4 +1,5 @@
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { MoveWarshipExecution } from "../src/core/execution/MoveWarshipExecution";
|
||||
import { WarshipExecution } from "../src/core/execution/WarshipExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { setup } from "./util/Setup";
|
||||
import { constructionExecution } from "./util/utils";
|
||||
import { executeTicks } from "./util/utils";
|
||||
|
||||
const coastX = 7;
|
||||
let game: Game;
|
||||
@@ -16,44 +17,36 @@ let player2: Player;
|
||||
|
||||
describe("Warship", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("half_land_half_ocean", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
const player_1_info = new PlayerInfo(
|
||||
"us",
|
||||
"boat dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_1_id",
|
||||
);
|
||||
game.addPlayer(player_1_info);
|
||||
const player_2_info = new PlayerInfo(
|
||||
"us",
|
||||
"boat dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_2_id",
|
||||
);
|
||||
game.addPlayer(player_2_info);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(
|
||||
game.player(player_1_info.id).info(),
|
||||
game.ref(coastX, 10),
|
||||
),
|
||||
new SpawnExecution(
|
||||
game.player(player_2_info.id).info(),
|
||||
game.ref(coastX, 15),
|
||||
),
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
[
|
||||
new PlayerInfo(
|
||||
"us",
|
||||
"boat dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_1_id",
|
||||
),
|
||||
new PlayerInfo(
|
||||
"us",
|
||||
"boat dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"player_2_id",
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player(player_1_info.id);
|
||||
player2 = game.player(player_2_info.id);
|
||||
player1 = game.player("player_1_id");
|
||||
player2 = game.player("player_2_id");
|
||||
});
|
||||
|
||||
test("Warship heals only if player has port", async () => {
|
||||
@@ -67,8 +60,11 @@ describe("Warship", () => {
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{},
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
@@ -85,26 +81,21 @@ describe("Warship", () => {
|
||||
});
|
||||
|
||||
test("Warship captures trade if player has port", async () => {
|
||||
constructionExecution(game, player1.id(), coastX, 10, UnitType.Port);
|
||||
constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship);
|
||||
// Warship need one more tick (for warship exec to actually build warship)
|
||||
game.executeNextTick();
|
||||
expect(player1.units(UnitType.Warship)).toHaveLength(1);
|
||||
expect(player1.units(UnitType.Port)).toHaveLength(1);
|
||||
|
||||
const dstPort = player2.buildUnit(
|
||||
UnitType.Port,
|
||||
game.ref(coastX + 2, 10),
|
||||
{},
|
||||
const portTile = game.ref(coastX, 10);
|
||||
player1.buildUnit(UnitType.Port, portTile, {});
|
||||
game.addExecution(
|
||||
new WarshipExecution(
|
||||
player1.buildUnit(UnitType.Warship, portTile, {
|
||||
patrolTile: portTile,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Cannot buildExec with trade ship as it's not buildable (but
|
||||
// we can obviously directly add it to the player)
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 7),
|
||||
{
|
||||
dstPort,
|
||||
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -113,32 +104,111 @@ describe("Warship", () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
expect(tradeShip.owner().id()).toBe(player1.id());
|
||||
expect(tradeShip.owner()).toBe(player1);
|
||||
});
|
||||
|
||||
test("Warship do not capture trade if player has no port", async () => {
|
||||
constructionExecution(game, player1.id(), coastX, 10, UnitType.Port);
|
||||
constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship);
|
||||
expect(player1.units(UnitType.Warship)).toHaveLength(1);
|
||||
game.addExecution(
|
||||
new WarshipExecution(
|
||||
player1.buildUnit(UnitType.Warship, game.ref(coastX + 1, 11), {
|
||||
patrolTile: game.ref(coastX + 1, 11),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const [dstPort] = player1.units(UnitType.Port);
|
||||
|
||||
player1.units(UnitType.Port)[0].delete();
|
||||
// Cannot buildExec with trade ship as it's not buildable (but
|
||||
// we can obviously directly add it to the player)
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 11),
|
||||
{
|
||||
dstPort,
|
||||
targetUnit: player1.buildUnit(UnitType.Port, game.ref(coastX, 11), {}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
// Let plenty of time for A* to execute
|
||||
// Let plenty of time for warship to potentially capture trade ship
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
});
|
||||
|
||||
test("Warship does not target trade ships that are safe from pirates", async () => {
|
||||
// build port so warship can target trade ships
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 10),
|
||||
{
|
||||
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
|
||||
},
|
||||
);
|
||||
|
||||
tradeShip.setSafeFromPirates();
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
});
|
||||
|
||||
test("Warship moves to new patrol tile", async () => {
|
||||
game.config().warshipTargettingRange = () => 1;
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
game.addExecution(
|
||||
new MoveWarshipExecution(warship.id(), game.ref(coastX + 5, 15)),
|
||||
);
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
expect(warship.patrolTile()).toBe(game.ref(coastX + 5, 15));
|
||||
});
|
||||
|
||||
test("Warship does not not target trade ships outside of patrol range", async () => {
|
||||
game.config().warshipTargettingRange = () => 3;
|
||||
|
||||
// build port so warship can target trade ships
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
|
||||
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 10),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 10),
|
||||
},
|
||||
);
|
||||
game.addExecution(new WarshipExecution(warship));
|
||||
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 15),
|
||||
{
|
||||
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
|
||||
},
|
||||
);
|
||||
|
||||
executeTicks(game, 10);
|
||||
|
||||
// Trade ship should not be captured
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,6 @@ export async function setup(
|
||||
false,
|
||||
);
|
||||
|
||||
// Create and return the game
|
||||
return createGame(humans, [], gameMap, miniGameMap, config);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user