diff --git a/src/core/execution/nation/NationStructureBehavior.ts b/src/core/execution/nation/NationStructureBehavior.ts index 03f3db17de..9bdff5256c 100644 --- a/src/core/execution/nation/NationStructureBehavior.ts +++ b/src/core/execution/nation/NationStructureBehavior.ts @@ -134,11 +134,30 @@ export class NationStructureBehavior { private _hasHighStartingGold: boolean | null = null; private _postSaveUpStartTick: number | null = null; + private attackStressMeter = 0; + private lastAttackCheckTick = 0; constructor( private random: PseudoRandom, private game: Game, private player: Player, ) {} + private isThreatenedByAttack(ratio: number): boolean { + const currentTick = this.game.ticks(); + // Cap elapsed at 100 ticks (10s) to prevent stress spikes after long peace + const elapsed = this.lastAttackCheckTick === 0 ? 0 : Math.min(100, currentTick - this.lastAttackCheckTick); + this.lastAttackCheckTick = currentTick; + + // Convert to Basis Points (whole numbers) for multiplayer safety (desync prevention) + const ratioBps = Math.floor(ratio * 10000); + const thresholdBps = Math.floor(UNDER_ATTACK_THREAT_RATIO * 10000); + + // Cooldown by 1x. If attacked (>5%), heat up by 3x + this.attackStressMeter = Math.max(0, this.attackStressMeter - elapsed); + if (ratioBps > 500) this.attackStressMeter += elapsed * 3; + + // Trigger if over 35% threshold OR sustained stress over 200 (approx 10s) + return ratioBps >= thresholdBps || (this.attackStressMeter > 200 && ratioBps > 0); + } handleStructures(): boolean { // Defense posts are handled outside the normal pacing/counter system: @@ -195,15 +214,15 @@ export class NationStructureBehavior { const incomingTroops = landAttacks.reduce((sum, a) => sum + a.troops(), 0); const ratio = incomingTroops / ourTroops; - if (ratio < UNDER_ATTACK_THREAT_RATIO) return false; + + if (!this.isThreatenedByAttack(ratio)) return false; let allowed: number; if (difficulty === Difficulty.Medium) { allowed = 1; } else { - allowed = Math.ceil(ratio / DEFENSE_POST_RATIO_PER_POST); + allowed = Math.max(1, Math.ceil(ratio / DEFENSE_POST_RATIO_PER_POST)); } - const frontTiles = this.getAttackFrontTiles(landAttacks); if (this.countDefensePostsNearFront(frontTiles, allowed) >= allowed) return false; @@ -236,7 +255,8 @@ export class NationStructureBehavior { const ourTroops = this.player.troops(); if (ourTroops <= 0) return false; const incomingTroops = landAttacks.reduce((sum, a) => sum + a.troops(), 0); - return incomingTroops / ourTroops >= UNDER_ATTACK_THREAT_RATIO; + const ratio = incomingTroops / ourTroops; + return this.isThreatenedByAttack(ratio); } /**