mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 15:20:47 +00:00
Added local attack
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
Transport,
|
||||
} from "./Transport";
|
||||
import { createCanvas } from "./Utils";
|
||||
import { resolveAttackSourceTile } from "./attackSource";
|
||||
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
|
||||
import { GoToPlayerEvent } from "./graphics/layers/Leaderboard";
|
||||
import SoundManager from "./sound/SoundManager";
|
||||
@@ -523,12 +524,7 @@ export class ClientGameRunner {
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
if (this.myPlayer === null) return;
|
||||
if (actions.canAttack) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.gameView.owner(tile).id(),
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
|
||||
),
|
||||
);
|
||||
void this.sendAttackIntent(tile);
|
||||
} else if (this.canAutoBoat(actions, tile)) {
|
||||
this.sendBoatAttackIntent(tile);
|
||||
}
|
||||
@@ -639,12 +635,7 @@ export class ClientGameRunner {
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
if (this.myPlayer === null) return;
|
||||
if (actions.canAttack) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.gameView.owner(tile).id(),
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
|
||||
),
|
||||
);
|
||||
void this.sendAttackIntent(tile);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -693,6 +684,25 @@ export class ClientGameRunner {
|
||||
});
|
||||
}
|
||||
|
||||
private async sendAttackIntent(tile: TileRef) {
|
||||
if (!this.myPlayer) return;
|
||||
|
||||
const targetId = this.gameView.owner(tile).id();
|
||||
const sourceTile = await resolveAttackSourceTile(
|
||||
this.gameView,
|
||||
this.myPlayer,
|
||||
targetId,
|
||||
tile,
|
||||
);
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
targetId,
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
|
||||
sourceTile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private canAutoBoat(actions: PlayerActions, tile: TileRef): boolean {
|
||||
if (!this.gameView.isLand(tile)) return false;
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export class SendAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID | null,
|
||||
public readonly troops: number,
|
||||
public readonly sourceTile: TileRef | null = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -491,6 +492,7 @@ export class Transport {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
sourceTile: event.sourceTile,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PlayerID } from "../core/game/Game";
|
||||
import { TileRef } from "../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../core/game/GameView";
|
||||
|
||||
export async function resolveAttackSourceTile(
|
||||
game: GameView,
|
||||
player: PlayerView,
|
||||
targetId: PlayerID | null,
|
||||
clickedTile: TileRef,
|
||||
): Promise<TileRef | null> {
|
||||
const { borderTiles } = await player.borderTiles();
|
||||
let bestTile: TileRef | null = null;
|
||||
let bestDistance = Infinity;
|
||||
|
||||
for (const borderTile of borderTiles) {
|
||||
if (!bordersTarget(game, borderTile, targetId)) {
|
||||
continue;
|
||||
}
|
||||
const distance = game.manhattanDist(borderTile, clickedTile);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestTile = borderTile;
|
||||
}
|
||||
}
|
||||
|
||||
return bestTile;
|
||||
}
|
||||
|
||||
function bordersTarget(
|
||||
game: GameView,
|
||||
borderTile: TileRef,
|
||||
targetId: PlayerID | null,
|
||||
): boolean {
|
||||
for (const neighbor of game.neighbors(borderTile)) {
|
||||
if (game.owner(neighbor).id() === targetId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -30,11 +30,16 @@ export class PlayerActionHandler {
|
||||
return await player.actions(tile);
|
||||
}
|
||||
|
||||
handleAttack(player: PlayerView, targetId: string | null) {
|
||||
handleAttack(
|
||||
player: PlayerView,
|
||||
targetId: string | null,
|
||||
sourceTile: TileRef | null = null,
|
||||
) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
targetId,
|
||||
this.uiState.attackRatio * player.troops(),
|
||||
sourceTile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { resolveAttackSourceTile } from "../../attackSource";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
@@ -584,7 +585,7 @@ export const centerButtonElement: CenterButtonElement = {
|
||||
|
||||
return !params.playerActions.canAttack;
|
||||
},
|
||||
action: (params: MenuElementParams) => {
|
||||
action: async (params: MenuElementParams) => {
|
||||
if (params.game.inSpawnPhase()) {
|
||||
params.playerActionHandler.handleSpawn(params.tile);
|
||||
} else {
|
||||
@@ -599,9 +600,17 @@ export const centerButtonElement: CenterButtonElement = {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const targetId = params.selected?.id() ?? null;
|
||||
const sourceTile = await resolveAttackSourceTile(
|
||||
params.game,
|
||||
params.myPlayer,
|
||||
targetId,
|
||||
params.tile,
|
||||
);
|
||||
params.playerActionHandler.handleAttack(
|
||||
params.myPlayer,
|
||||
params.selected?.id() ?? null,
|
||||
targetId,
|
||||
sourceTile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("attack"),
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nonnegative().nullable(),
|
||||
sourceTile: z.number().nullable().optional(),
|
||||
});
|
||||
|
||||
export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
|
||||
@@ -97,6 +97,8 @@ export class AttackExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceTile = this.resolveSourceTile();
|
||||
|
||||
this.startTroops ??= this.mg
|
||||
.config()
|
||||
.attackAmount(this._owner, this.target);
|
||||
@@ -311,6 +313,21 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveSourceTile(): TileRef | null {
|
||||
if (this.sourceTile === null) {
|
||||
return null;
|
||||
}
|
||||
if (this.mg.owner(this.sourceTile) !== this._owner) {
|
||||
return null;
|
||||
}
|
||||
for (const neighbor of this.mg.neighbors(this.sourceTile)) {
|
||||
if (this.mg.owner(neighbor) === this.target) {
|
||||
return this.sourceTile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private addNeighbors(tile: TileRef) {
|
||||
if (this.attack === null) {
|
||||
throw new Error("Attack not initialized");
|
||||
|
||||
@@ -60,7 +60,7 @@ export class Executor {
|
||||
intent.troops,
|
||||
player,
|
||||
intent.targetID,
|
||||
null,
|
||||
intent.sourceTile ?? null,
|
||||
);
|
||||
}
|
||||
case "cancel_attack":
|
||||
|
||||
Reference in New Issue
Block a user