refactor: have players store attacks

This commit is contained in:
Evan
2025-02-11 10:16:08 -08:00
parent 81a83706c7
commit 6f02bd250e
4 changed files with 142 additions and 51 deletions
+48 -48
View File
@@ -1,5 +1,6 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import {
Attack,
Cell,
Execution,
Game,
@@ -37,8 +38,10 @@ export class AttackExecution implements Execution {
private border = new Set<TileRef>();
private attack: Attack = null;
constructor(
private troops: number | null,
private startTroops: number | null = null,
private _ownerID: PlayerID,
private _targetID: PlayerID | null,
private sourceTile: TileRef | null,
@@ -80,57 +83,49 @@ export class AttackExecution implements Execution {
return;
}
if (this.troops == null) {
this.troops = this.mg.config().attackAmount(this._owner, this.target);
if (this.startTroops == null) {
this.startTroops = this.mg
.config()
.attackAmount(this._owner, this.target);
}
this.troops = Math.min(this._owner.troops(), this.troops);
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
if (this.removeTroops) {
this._owner.removeTroops(this.troops);
this._owner.removeTroops(this.startTroops);
}
this.attack = this._owner.createAttack(
this.target,
this.startTroops,
this.sourceTile
);
for (const exec of mg.executions()) {
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
const otherAttack = exec as AttackExecution;
for (const incoming of this._owner.incomingAttacks()) {
if (incoming.attacker() == this.target) {
// Target has opposing attack, cancel them out
if (
this.target.isPlayer() &&
otherAttack._targetID == this._ownerID &&
this._targetID == otherAttack._ownerID
) {
if (otherAttack.troops > this.troops) {
otherAttack.troops -= this.troops;
// otherAttack.calculateToConquer()
this.active = false;
return;
} else {
this.troops -= otherAttack.troops;
otherAttack.active = false;
}
}
// Existing attack on same target, add troops
if (
otherAttack._owner == this._owner &&
otherAttack._targetID == this._targetID &&
this.sourceTile == otherAttack.sourceTile
) {
otherAttack.troops += this.troops;
otherAttack.refreshToConquer();
if (incoming.troops() > this.attack.troops()) {
incoming.setTroops(incoming.troops() - this.attack.troops());
this.attack.delete();
this.active = false;
return;
} else {
this.attack.setTroops(this.attack.troops() - incoming.troops());
incoming.delete();
}
}
}
if (
this._owner.type() != PlayerType.Bot &&
this.target.isPlayer() &&
this.target.type() == PlayerType.Human
) {
mg.displayMessage(
`You are being attacked by ${this._owner.displayName()}`,
MessageType.ERROR,
this._targetID
);
for (const outgoing of this._owner.outgoingAttacks()) {
if (
outgoing != this.attack &&
outgoing.target() == this.attack.target() &&
outgoing.sourceTile() == this.attack.sourceTile()
) {
// Existing attack on same target, add troops
outgoing.setTroops(outgoing.troops() + this.attack.troops());
this.active = false;
this.attack.delete();
return;
}
}
if (this.sourceTile != null) {
this.addNeighbors(this.sourceTile);
} else {
@@ -155,9 +150,11 @@ export class AttackExecution implements Execution {
}
tick(ticks: number) {
if (!this.active) {
if (!this.attack.isActive()) {
this.active = false;
return;
}
const alliance = this._owner.allianceWith(this.target as Player);
if (this.breakAlliance && alliance != null) {
this.breakAlliance = false;
@@ -165,7 +162,8 @@ export class AttackExecution implements Execution {
}
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
// In this case a new alliance was created AFTER the attack started.
this._owner.addTroops(this.troops);
this._owner.addTroops(this.attack.troops());
this.attack.delete();
this.active = false;
return;
}
@@ -173,7 +171,7 @@ export class AttackExecution implements Execution {
let numTilesPerTick = this.mg
.config()
.attackTilesPerTick(
this.troops,
this.attack.troops(),
this._owner,
this.target,
this.border.size + this.random.nextInt(0, 5)
@@ -182,7 +180,8 @@ export class AttackExecution implements Execution {
// consolex.log(`num execs: ${this.mg.executions().length}`)
while (numTilesPerTick > 0) {
if (this.troops < 1) {
if (this.attack.troops() < 1) {
this.attack.delete();
this.active = false;
return;
}
@@ -190,7 +189,8 @@ export class AttackExecution implements Execution {
if (this.toConquer.size() == 0) {
this.refreshToConquer();
this.active = false;
this._owner.addTroops(this.troops);
this._owner.addTroops(this.attack.troops());
this.attack.delete();
return;
}
@@ -209,13 +209,13 @@ export class AttackExecution implements Execution {
.config()
.attackLogic(
this.mg,
this.troops,
this.attack.troops(),
this._owner,
this.target,
tileToConquer
);
numTilesPerTick -= tilesPerTickUsed;
this.troops -= attackerTroopLoss;
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
if (this.target.isPlayer()) {
this.target.removeTroops(defenderTroopLoss);
}
+49
View File
@@ -0,0 +1,49 @@
import { Attack, Player, TerraNullius } from "./Game";
import { TileRef } from "./GameMap";
import { PlayerImpl } from "./PlayerImpl";
export class AttackImpl implements Attack {
private _isActive = true;
constructor(
private _target: Player | TerraNullius,
private _attacker: Player,
private _troops: number,
private _sourceTile: TileRef | null
) {}
sourceTile(): TileRef | null {
return this._sourceTile;
}
target(): Player | TerraNullius {
return this._target;
}
attacker(): Player {
return this._attacker;
}
troops(): number {
return this._troops;
}
setTroops(troops: number) {
this._troops = troops;
}
isActive() {
return this._isActive;
}
delete() {
if (this._target.isPlayer()) {
(this._target as PlayerImpl)._incomingAttacks = (
this._target as PlayerImpl
)._incomingAttacks.filter((a) => a != this);
}
(this._attacker as PlayerImpl)._outgoingAttacks = (
this._attacker as PlayerImpl
)._outgoingAttacks.filter((a) => a != this);
this._isActive = false;
}
}
+21 -3
View File
@@ -135,6 +135,17 @@ export interface Execution {
owner(): Player;
}
export interface Attack {
target(): Player | TerraNullius;
attacker(): Player;
troops(): number;
setTroops(troops: number): void;
isActive(): boolean;
delete(): void;
// The tile the attack originated from, mostly used for boat attacks.
sourceTile(): TileRef | null;
}
export interface AllianceRequest {
accept(): void;
reject(): void;
@@ -284,12 +295,21 @@ export interface Player {
canDonate(recipient: Player): boolean;
donate(recipient: Player, troops: number): void;
// Attacking.
canAttack(tile: TileRef): boolean;
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef
): Attack;
outgoingAttacks(): Attack[];
incomingAttacks(): Attack[];
// Misc
executions(): Execution[];
toUpdate(): PlayerUpdate;
playerProfile(): PlayerProfile;
canBoat(tile: TileRef): boolean;
canAttack(tile: TileRef);
}
export interface Game extends GameMap {
@@ -324,8 +344,6 @@ export interface Game extends GameMap {
unitInfo(type: UnitType): UnitInfo;
nearbyDefensePosts(tile: TileRef): Unit[];
// Events & Messages
executions(): Execution[];
addExecution(...exec: Execution[]): void;
displayMessage(
message: string,
+24
View File
@@ -17,6 +17,7 @@ import {
Relation,
EmojiMessage,
PlayerProfile,
Attack,
} from "./Game";
import { PlayerUpdate } from "./GameUpdates";
import { GameUpdateType } from "./GameUpdates";
@@ -37,6 +38,7 @@ import { renderTroops } from "../../client/Utils";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
import { Emoji } from "discord.js";
import { AttackImpl } from "./AttackImpl";
interface Target {
tick: Tick;
@@ -75,6 +77,9 @@ export class PlayerImpl implements Player {
private relations = new Map<Player, number>();
public _incomingAttacks: Attack[] = [];
public _outgoingAttacks: Attack[] = [];
constructor(
private mg: GameImpl,
private _smallID: number,
@@ -759,6 +764,25 @@ export class PlayerImpl implements Player {
}
}
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef
): Attack {
const attack = new AttackImpl(target, this, troops, sourceTile);
this._outgoingAttacks.push(attack);
if (target.isPlayer()) {
(target as PlayerImpl)._incomingAttacks.push(attack);
}
return attack;
}
outgoingAttacks(): Attack[] {
return this._outgoingAttacks;
}
incomingAttacks(): Attack[] {
return this._incomingAttacks;
}
public canAttack(tile: TileRef): boolean {
if (
this.mg.hasOwner(tile) &&