mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 00:28:14 +00:00
feat: move warship (#196)
This commit is contained in:
@@ -135,6 +135,13 @@ export class SendHashEvent implements GameEvent {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MoveWarshipIntentEvent implements GameEvent {
|
||||||
|
constructor(
|
||||||
|
public readonly unitId: number,
|
||||||
|
public readonly tile: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
export class Transport {
|
export class Transport {
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
|
|
||||||
@@ -194,6 +201,9 @@ export class Transport {
|
|||||||
this.eventBus.on(CancelAttackIntentEvent, (e) =>
|
this.eventBus.on(CancelAttackIntentEvent, (e) =>
|
||||||
this.onCancelAttackIntentEvent(e),
|
this.onCancelAttackIntentEvent(e),
|
||||||
);
|
);
|
||||||
|
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
|
||||||
|
this.onMoveWarshipEvent(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private startPing() {
|
private startPing() {
|
||||||
@@ -522,6 +532,16 @@ export class Transport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMoveWarshipEvent(event: MoveWarshipIntentEvent) {
|
||||||
|
this.sendIntent({
|
||||||
|
type: "move_warship",
|
||||||
|
clientID: this.lobbyConfig.clientID,
|
||||||
|
playerID: this.lobbyConfig.playerID,
|
||||||
|
unitId: event.unitId,
|
||||||
|
tile: event.tile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private sendIntent(intent: Intent) {
|
private sendIntent(intent: Intent) {
|
||||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||||
const msg = ClientIntentMessageSchema.parse({
|
const msg = ClientIntentMessageSchema.parse({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "../../../core/game/GameMap";
|
} from "../../../core/game/GameMap";
|
||||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||||
import { TransformHandler } from "../TransformHandler";
|
import { TransformHandler } from "../TransformHandler";
|
||||||
|
import { MoveWarshipIntentEvent } from "../../Transport";
|
||||||
|
|
||||||
enum Relationship {
|
enum Relationship {
|
||||||
Self,
|
Self,
|
||||||
@@ -44,7 +45,7 @@ export class UnitLayer implements Layer {
|
|||||||
private selectedUnit: UnitView | null = null;
|
private selectedUnit: UnitView | null = null;
|
||||||
|
|
||||||
// Configuration for unit selection
|
// Configuration for unit selection
|
||||||
private readonly WARSHIP_SELECTION_RADIUS = 3; // Radius in game cells for warship selection hit zone
|
private readonly WARSHIP_SELECTION_RADIUS = 10; // Radius in game cells for warship selection hit zone
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private game: GameView,
|
private game: GameView,
|
||||||
@@ -121,19 +122,19 @@ export class UnitLayer implements Layer {
|
|||||||
// Find warships near this cell, sorted by distance
|
// Find warships near this cell, sorted by distance
|
||||||
const nearbyWarships = this.findWarshipsNearCell(cell);
|
const nearbyWarships = this.findWarshipsNearCell(cell);
|
||||||
|
|
||||||
if (nearbyWarships.length > 0) {
|
if (this.selectedUnit) {
|
||||||
|
const clickRef = this.game.ref(cell.x, cell.y);
|
||||||
|
if (this.game.isOcean(clickRef)) {
|
||||||
|
this.eventBus.emit(
|
||||||
|
new MoveWarshipIntentEvent(this.selectedUnit.id(), clickRef),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Deselect
|
||||||
|
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
||||||
|
} else if (nearbyWarships.length > 0) {
|
||||||
// Toggle selection of the closest warship
|
// Toggle selection of the closest warship
|
||||||
const clickedUnit = nearbyWarships[0];
|
const clickedUnit = nearbyWarships[0];
|
||||||
if (this.selectedUnit === clickedUnit) {
|
this.eventBus.emit(new UnitSelectionEvent(clickedUnit, true));
|
||||||
// Deselect if already selected
|
|
||||||
this.eventBus.emit(new UnitSelectionEvent(clickedUnit, false));
|
|
||||||
} else {
|
|
||||||
// Select the new unit
|
|
||||||
this.eventBus.emit(new UnitSelectionEvent(clickedUnit, true));
|
|
||||||
}
|
|
||||||
} else if (this.selectedUnit) {
|
|
||||||
// If clicked elsewhere and there's a selection, deselect it
|
|
||||||
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -24,7 +24,8 @@ export type Intent =
|
|||||||
| DonateIntent
|
| DonateIntent
|
||||||
| TargetTroopRatioIntent
|
| TargetTroopRatioIntent
|
||||||
| BuildUnitIntent
|
| BuildUnitIntent
|
||||||
| EmbargoIntent;
|
| EmbargoIntent
|
||||||
|
| MoveWarshipIntent;
|
||||||
|
|
||||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||||
@@ -43,6 +44,7 @@ export type TargetTroopRatioIntent = z.infer<
|
|||||||
typeof TargetTroopRatioIntentSchema
|
typeof TargetTroopRatioIntentSchema
|
||||||
>;
|
>;
|
||||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
||||||
|
export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
|
||||||
|
|
||||||
export type Turn = z.infer<typeof TurnSchema>;
|
export type Turn = z.infer<typeof TurnSchema>;
|
||||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||||
@@ -164,6 +166,7 @@ const BaseIntentSchema = z.object({
|
|||||||
"troop_ratio",
|
"troop_ratio",
|
||||||
"build_unit",
|
"build_unit",
|
||||||
"embargo",
|
"embargo",
|
||||||
|
"move_warship",
|
||||||
]),
|
]),
|
||||||
clientID: ID,
|
clientID: ID,
|
||||||
playerID: ID,
|
playerID: ID,
|
||||||
@@ -261,6 +264,12 @@ export const CancelAttackIntentSchema = BaseIntentSchema.extend({
|
|||||||
attackID: z.string(),
|
attackID: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
|
||||||
|
type: z.literal("move_warship"),
|
||||||
|
unitId: z.number(),
|
||||||
|
tile: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
const IntentSchema = z.union([
|
const IntentSchema = z.union([
|
||||||
AttackIntentSchema,
|
AttackIntentSchema,
|
||||||
CancelAttackIntentSchema,
|
CancelAttackIntentSchema,
|
||||||
@@ -275,6 +284,7 @@ const IntentSchema = z.union([
|
|||||||
TargetTroopRatioIntentSchema,
|
TargetTroopRatioIntentSchema,
|
||||||
BuildUnitIntentSchema,
|
BuildUnitIntentSchema,
|
||||||
EmbargoIntentSchema,
|
EmbargoIntentSchema,
|
||||||
|
MoveWarshipIntentSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const TurnSchema = z.object({
|
export const TurnSchema = z.object({
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { fixProfaneUsername, isProfaneUsername } from "../validations/username";
|
|||||||
import { NoOpExecution } from "./NoOpExecution";
|
import { NoOpExecution } from "./NoOpExecution";
|
||||||
import { EmbargoExecution } from "./EmbargoExecution";
|
import { EmbargoExecution } from "./EmbargoExecution";
|
||||||
import { RetreatExecution } from "./RetreatExecution";
|
import { RetreatExecution } from "./RetreatExecution";
|
||||||
|
import { MoveWarshipExecution } from "./MoveWarshipExecution";
|
||||||
|
|
||||||
export class Executor {
|
export class Executor {
|
||||||
// private random = new PseudoRandom(999)
|
// private random = new PseudoRandom(999)
|
||||||
@@ -83,6 +84,8 @@ export class Executor {
|
|||||||
}
|
}
|
||||||
case "cancel_attack":
|
case "cancel_attack":
|
||||||
return new RetreatExecution(intent.playerID, intent.attackID);
|
return new RetreatExecution(intent.playerID, intent.attackID);
|
||||||
|
case "move_warship":
|
||||||
|
return new MoveWarshipExecution(intent.unitId, intent.tile);
|
||||||
case "spawn":
|
case "spawn":
|
||||||
return new SpawnExecution(
|
return new SpawnExecution(
|
||||||
new PlayerInfo(
|
new PlayerInfo(
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||||
|
|
||||||
|
const cancelDelay = 2;
|
||||||
|
|
||||||
|
export class MoveWarshipExecution implements Execution {
|
||||||
|
private active = true;
|
||||||
|
private mg: Game;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly unitId: number,
|
||||||
|
public readonly position: number,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
init(mg: Game, ticks: number): void {
|
||||||
|
this.mg = mg;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(ticks: number): void {
|
||||||
|
const warship = this.mg.units().find((u) => u.id() == this.unitId);
|
||||||
|
if (!warship) {
|
||||||
|
console.log("MoveWarshipExecution: warship is already dead");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warship.setMoveTarget(this.position);
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
owner(): Player {
|
||||||
|
const warship = this.mg.units().find((u) => u.id() == this.unitId);
|
||||||
|
return warship ? warship.owner() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeDuringSpawnPhase(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,70 @@ export class WarshipExecution implements Execution {
|
|||||||
this.random = new PseudoRandom(mg.ticks());
|
this.random = new PseudoRandom(mg.ticks());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only for warships with "moveTarget" set
|
||||||
|
goToMoveTarget(target: TileRef): boolean {
|
||||||
|
// 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.setMoveTarget(null);
|
||||||
|
return;
|
||||||
|
case PathFindResultType.NextTile:
|
||||||
|
this.warship.move(result.tile);
|
||||||
|
break;
|
||||||
|
case PathFindResultType.Pending:
|
||||||
|
break;
|
||||||
|
case PathFindResultType.PathNotFound:
|
||||||
|
consolex.log(`path not found to target`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private shoot() {
|
||||||
|
if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) {
|
||||||
|
this.lastShellAttack = this.mg.ticks();
|
||||||
|
this.mg.addExecution(
|
||||||
|
new ShellExecution(
|
||||||
|
this.warship.tile(),
|
||||||
|
this.warship.owner(),
|
||||||
|
this.warship,
|
||||||
|
this.target,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!this.target.hasHealth()) {
|
||||||
|
// Don't send multiple shells to target that can be oneshotted
|
||||||
|
this.alreadySentShell.add(this.target);
|
||||||
|
this.target = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private patrol() {
|
||||||
|
this.warship.setWarshipTarget(this.target);
|
||||||
|
if (this.target == null || this.target.type() != UnitType.TradeShip) {
|
||||||
|
// Patrol unless we are hunting down a tradeship
|
||||||
|
const result = this.pathfinder.nextTile(
|
||||||
|
this.warship.tile(),
|
||||||
|
this.patrolTile,
|
||||||
|
);
|
||||||
|
switch (result.type) {
|
||||||
|
case PathFindResultType.Completed:
|
||||||
|
this.patrolTile = this.randomTile();
|
||||||
|
break;
|
||||||
|
case PathFindResultType.NextTile:
|
||||||
|
this.warship.move(result.tile);
|
||||||
|
break;
|
||||||
|
case PathFindResultType.Pending:
|
||||||
|
return;
|
||||||
|
case PathFindResultType.PathNotFound:
|
||||||
|
consolex.log(`path not found to patrol tile`);
|
||||||
|
this.patrolTile = this.randomTile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tick(ticks: number): void {
|
tick(ticks: number): void {
|
||||||
if (this.warship == null) {
|
if (this.warship == null) {
|
||||||
const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile);
|
const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile);
|
||||||
@@ -110,28 +174,17 @@ export class WarshipExecution implements Execution {
|
|||||||
return distSortUnit(this.mg, this.warship)(a, b);
|
return distSortUnit(this.mg, this.warship)(a, b);
|
||||||
})[0] ?? null;
|
})[0] ?? null;
|
||||||
|
|
||||||
this.warship.setWarshipTarget(this.target);
|
if (this.warship.moveTarget()) {
|
||||||
if (this.target == null || this.target.type() != UnitType.TradeShip) {
|
this.goToMoveTarget(this.warship.moveTarget());
|
||||||
// Patrol unless we are hunting down a tradeship
|
// If we have a "move target" then we cannot target trade ships as it
|
||||||
const result = this.pathfinder.nextTile(
|
// requires moving.
|
||||||
this.warship.tile(),
|
if (this.target && this.target.type() == UnitType.TradeShip) {
|
||||||
this.patrolTile,
|
this.target = null;
|
||||||
);
|
|
||||||
switch (result.type) {
|
|
||||||
case PathFindResultType.Completed:
|
|
||||||
this.patrolTile = this.randomTile();
|
|
||||||
break;
|
|
||||||
case PathFindResultType.NextTile:
|
|
||||||
this.warship.move(result.tile);
|
|
||||||
break;
|
|
||||||
case PathFindResultType.Pending:
|
|
||||||
return;
|
|
||||||
case PathFindResultType.PathNotFound:
|
|
||||||
consolex.log(`path not found to patrol tile`);
|
|
||||||
this.patrolTile = this.randomTile();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} else if (!this.target || this.target.type() != UnitType.TradeShip) {
|
||||||
|
this.patrol();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.target == null ||
|
this.target == null ||
|
||||||
!this.target.isActive() ||
|
!this.target.isActive() ||
|
||||||
@@ -141,25 +194,16 @@ export class WarshipExecution implements Execution {
|
|||||||
this.target = null;
|
this.target = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.warship.setWarshipTarget(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) {
|
if (this.target.type() != UnitType.TradeShip) {
|
||||||
if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) {
|
this.shoot();
|
||||||
this.lastShellAttack = this.mg.ticks();
|
|
||||||
this.mg.addExecution(
|
|
||||||
new ShellExecution(
|
|
||||||
this.warship.tile(),
|
|
||||||
this.warship.owner(),
|
|
||||||
this.warship,
|
|
||||||
this.target,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!this.target.hasHealth()) {
|
|
||||||
// Don't send multiple shells to target that can be oneshotted
|
|
||||||
this.alreadySentShell.add(this.target);
|
|
||||||
this.target = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Only hunt down tradeships
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,9 @@ export interface Unit {
|
|||||||
dstPort(): Unit; // Only for trade ships
|
dstPort(): Unit; // Only for trade ships
|
||||||
detonationDst(): TileRef; // Only for nukes
|
detonationDst(): TileRef; // Only for nukes
|
||||||
|
|
||||||
|
setMoveTarget(cell: TileRef): void;
|
||||||
|
moveTarget(): TileRef | null;
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
setTroops(troops: number): void;
|
setTroops(troops: number): void;
|
||||||
delete(displayerMessage?: boolean): void;
|
delete(displayerMessage?: boolean): void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MessageType, nukeTypes, UnitSpecificInfos } from "./Game";
|
import { MessageType, UnitSpecificInfos } from "./Game";
|
||||||
import { UnitUpdate } from "./GameUpdates";
|
import { UnitUpdate } from "./GameUpdates";
|
||||||
import { GameUpdateType } from "./GameUpdates";
|
import { GameUpdateType } from "./GameUpdates";
|
||||||
import { simpleHash, toInt, within, withinInt } from "../Util";
|
import { simpleHash, toInt, within, withinInt } from "../Util";
|
||||||
@@ -14,6 +14,7 @@ export class UnitImpl implements Unit {
|
|||||||
private _lastTile: TileRef = null;
|
private _lastTile: TileRef = null;
|
||||||
// Currently only warship use it
|
// Currently only warship use it
|
||||||
private _target: Unit = null;
|
private _target: Unit = null;
|
||||||
|
private _moveTarget: TileRef = null;
|
||||||
|
|
||||||
private _constructionType: UnitType = undefined;
|
private _constructionType: UnitType = undefined;
|
||||||
|
|
||||||
@@ -184,4 +185,12 @@ export class UnitImpl implements Unit {
|
|||||||
setDstPort(dstPort: Unit): void {
|
setDstPort(dstPort: Unit): void {
|
||||||
this._dstPort = dstPort;
|
this._dstPort = dstPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMoveTarget(moveTarget: TileRef) {
|
||||||
|
this._moveTarget = moveTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTarget(): TileRef | null {
|
||||||
|
return this._moveTarget;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user