mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 09:04:37 +00:00
Boat retreat (#705)
## Description: Add boat retreat (continue #365 by [QuentinSiruguet](https://github.com/openfrontio/OpenFrontIO/issues?q=is%3Apr+author%3AQuentinSiruguet)) Basically implements all the pending reviews from #365  ## 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: VivaciousBox --------- Co-authored-by: Quentin SIRUGUET <quentin.siruguet@gmail.com>
This commit is contained in:
@@ -132,6 +132,10 @@ export class CancelAttackIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class CancelBoatIntentEvent implements GameEvent {
|
||||
constructor(public readonly unitID: number) {}
|
||||
}
|
||||
|
||||
export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
constructor(public readonly ratio: number) {}
|
||||
}
|
||||
@@ -221,6 +225,10 @@ export class Transport {
|
||||
this.eventBus.on(CancelAttackIntentEvent, (e) =>
|
||||
this.onCancelAttackIntentEvent(e),
|
||||
);
|
||||
this.eventBus.on(CancelBoatIntentEvent, (e) =>
|
||||
this.onCancelBoatIntentEvent(e),
|
||||
);
|
||||
|
||||
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
|
||||
this.onMoveWarshipEvent(e);
|
||||
});
|
||||
@@ -568,6 +576,14 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onCancelBoatIntentEvent(event: CancelBoatIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "cancel_boat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitID: event.unitID,
|
||||
});
|
||||
}
|
||||
|
||||
private onMoveWarshipEvent(event: MoveWarshipIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "move_warship",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import {
|
||||
CancelAttackIntentEvent,
|
||||
CancelBoatIntentEvent,
|
||||
SendAllianceReplyIntentEvent,
|
||||
} from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -380,6 +381,12 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.eventBus.emit(new CancelAttackIntentEvent(myPlayer.id(), id));
|
||||
}
|
||||
|
||||
emitBoatCancelIntent(id: number) {
|
||||
const myPlayer = this.game.playerByClientID(this.clientID);
|
||||
if (!myPlayer) return;
|
||||
this.eventBus.emit(new CancelBoatIntentEvent(id));
|
||||
}
|
||||
|
||||
emitGoToPlayerEvent(attackerID: number) {
|
||||
const attacker = this.game.playerBySmallID(attackerID) as PlayerView;
|
||||
if (!attacker) return;
|
||||
@@ -572,25 +579,29 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private renderBoats() {
|
||||
if (this.outgoingBoats.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.outgoingBoats.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td
|
||||
class="lg:p-3 p-1 text-left text-blue-400 grid grid-cols-3 gap-2"
|
||||
>
|
||||
<td class="lg:p-3 p-1 text-left text-blue-400">
|
||||
${this.outgoingBoats.map(
|
||||
(boats) => html`
|
||||
(boat) => html`
|
||||
<button
|
||||
translate="no"
|
||||
@click=${() => this.emitGoToUnitEvent(boats)}
|
||||
@click=${() => this.emitGoToUnitEvent(boat)}
|
||||
>
|
||||
Boat: ${renderTroops(boats.troops())}
|
||||
Boat: ${renderTroops(boat.troops())}
|
||||
</button>
|
||||
${!boat.retreating()
|
||||
? html`<button
|
||||
${boat.retreating() ? "disabled" : ""}
|
||||
@click=${() => {
|
||||
this.emitBoatCancelIntent(boat.id());
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>`
|
||||
: "(retreating...)"}
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -20,6 +20,7 @@ export type Intent =
|
||||
| AttackIntent
|
||||
| CancelAttackIntent
|
||||
| BoatAttackIntent
|
||||
| CancelBoatIntent
|
||||
| AllianceRequestIntent
|
||||
| AllianceRequestReplyIntent
|
||||
| BreakAllianceIntent
|
||||
@@ -37,6 +38,7 @@ export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>;
|
||||
export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>;
|
||||
export type CancelBoatIntent = z.infer<typeof CancelBoatIntentSchema>;
|
||||
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>;
|
||||
export type AllianceRequestReplyIntent = z.infer<
|
||||
typeof AllianceRequestReplyIntentSchema
|
||||
@@ -196,6 +198,7 @@ const BaseIntentSchema = z.object({
|
||||
"cancel_attack",
|
||||
"spawn",
|
||||
"boat",
|
||||
"cancel_boat",
|
||||
"name",
|
||||
"targetPlayer",
|
||||
"emoji",
|
||||
@@ -294,6 +297,11 @@ export const CancelAttackIntentSchema = BaseIntentSchema.extend({
|
||||
attackID: z.string(),
|
||||
});
|
||||
|
||||
export const CancelBoatIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("cancel_boat"),
|
||||
unitID: z.number(),
|
||||
});
|
||||
|
||||
export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("move_warship"),
|
||||
unitId: z.number(),
|
||||
@@ -318,6 +326,7 @@ const IntentSchema = z.union([
|
||||
CancelAttackIntentSchema,
|
||||
SpawnIntentSchema,
|
||||
BoatAttackIntentSchema,
|
||||
CancelBoatIntentSchema,
|
||||
AllianceRequestIntentSchema,
|
||||
AllianceRequestReplyIntentSchema,
|
||||
BreakAllianceIntentSchema,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { Execution, Game, Player, PlayerID, UnitType } from "../game/Game";
|
||||
|
||||
export class BoatRetreatExecution implements Execution {
|
||||
private active = true;
|
||||
private player: Player | undefined;
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private unitID: number,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.warn(`BoatRetreatExecution: Player ${this.playerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.player = mg.player(this.playerID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (!this.player) {
|
||||
console.warn(`BoatRetreatExecution: Player ${this.playerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const unit = this.player
|
||||
.units()
|
||||
.find(
|
||||
(unit) =>
|
||||
unit.id() === this.unitID && unit.type() === UnitType.TransportShip,
|
||||
);
|
||||
|
||||
if (!unit) {
|
||||
consolex.warn(`Didn't find outgoing boat with id ${this.unitID}`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
unit.orderBoatRetreat();
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
if (this.player === undefined) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
return this.player;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
||||
import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution";
|
||||
import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { BoatRetreatExecution } from "./BoatRetreatExecution";
|
||||
import { BotSpawner } from "./BotSpawner";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { DonateGoldExecution } from "./DonateGoldExecution";
|
||||
@@ -59,6 +60,8 @@ export class Executor {
|
||||
}
|
||||
case "cancel_attack":
|
||||
return new RetreatExecution(playerID, intent.attackID);
|
||||
case "cancel_boat":
|
||||
return new BoatRetreatExecution(playerID, intent.unitID);
|
||||
case "move_warship":
|
||||
return new MoveWarshipExecution(intent.unitId, intent.tile);
|
||||
case "spawn":
|
||||
|
||||
@@ -15,7 +15,7 @@ export class RetreatExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.warn(`RetreatExecution: player ${this.player.id()} not found`);
|
||||
console.warn(`RetreatExecution: player ${this.playerID} not found`);
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
|
||||
@@ -165,6 +165,10 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
this.lastMove = ticks;
|
||||
|
||||
if (this.boat.retreating()) {
|
||||
this.dst = this.src!; // src is guaranteed to be set at this point
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
|
||||
@@ -348,6 +348,8 @@ export interface Unit {
|
||||
|
||||
// Health
|
||||
hasHealth(): boolean;
|
||||
retreating(): boolean;
|
||||
orderBoatRetreat(): void;
|
||||
health(): number;
|
||||
modifyHealth(delta: number): void;
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface UnitUpdate {
|
||||
pos: TileRef;
|
||||
lastPos: TileRef;
|
||||
isActive: boolean;
|
||||
retreating: boolean;
|
||||
targetUnitId?: number; // Only for trade ships
|
||||
targetTile?: TileRef; // Only for nukes
|
||||
health?: number;
|
||||
|
||||
@@ -78,6 +78,12 @@ export class UnitView {
|
||||
troops(): number {
|
||||
return this.data.troops;
|
||||
}
|
||||
retreating(): boolean {
|
||||
if (this.type() !== UnitType.TransportShip) {
|
||||
throw Error("Must be a transport ship");
|
||||
}
|
||||
return this.data.retreating;
|
||||
}
|
||||
tile(): TileRef {
|
||||
return this.data.pos;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export class UnitImpl implements Unit {
|
||||
private _targetUnit: Unit | undefined;
|
||||
private _health: bigint;
|
||||
private _lastTile: TileRef;
|
||||
private _retreating: boolean = false;
|
||||
private _targetedBySAM = false;
|
||||
private _lastSetSafeFromPirates: number; // Only for trade ships
|
||||
private _constructionType: UnitType | undefined;
|
||||
@@ -73,6 +74,7 @@ export class UnitImpl implements Unit {
|
||||
ownerID: this._owner.smallID(),
|
||||
lastOwnerID: this._lastOwner?.smallID(),
|
||||
isActive: this._active,
|
||||
retreating: this._retreating,
|
||||
pos: this._tile,
|
||||
lastPos: this._lastTile,
|
||||
health: this.hasHealth() ? Number(this._health) : undefined,
|
||||
@@ -171,6 +173,17 @@ export class UnitImpl implements Unit {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
retreating(): boolean {
|
||||
return this._retreating;
|
||||
}
|
||||
|
||||
orderBoatRetreat() {
|
||||
if (this.type() !== UnitType.TransportShip) {
|
||||
throw new Error(`Cannot retreat ${this.type()}`);
|
||||
}
|
||||
this._retreating = true;
|
||||
}
|
||||
|
||||
constructionType(): UnitType | null {
|
||||
if (this.type() !== UnitType.Construction) {
|
||||
throw new Error(`Cannot get construction type on ${this.type()}`);
|
||||
|
||||
Reference in New Issue
Block a user