Added local attack

This commit is contained in:
Aotumuri
2026-01-10 21:46:28 +09:00
parent 240690c574
commit 54b4c5cdd8
8 changed files with 100 additions and 16 deletions
+22 -12
View File
@@ -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;
+2
View File
@@ -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,
});
}
+40
View File
@@ -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,
);
}
}
+1
View File
@@ -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({
+17
View File
@@ -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");
+1 -1
View File
@@ -60,7 +60,7 @@ export class Executor {
intent.troops,
player,
intent.targetID,
null,
intent.sourceTile ?? null,
);
}
case "cancel_attack":