diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index d082784bb..9fc77e3d6 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -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(); + 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); } diff --git a/src/core/game/AttackImpl.ts b/src/core/game/AttackImpl.ts new file mode 100644 index 000000000..87d7a0017 --- /dev/null +++ b/src/core/game/AttackImpl.ts @@ -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; + } +} diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 371c3e905..b586e37b0 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -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, diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 921008f5c..a9481b361 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -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(); + 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) &&