diff --git a/resources/images/MIRVIcon.svg b/resources/images/MIRVIcon.svg new file mode 100644 index 000000000..bc38d2de2 --- /dev/null +++ b/resources/images/MIRVIcon.svg @@ -0,0 +1,262 @@ + + diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index 4b1f0cf1c..8a6e69c2a 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -15,6 +15,7 @@ import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg"; import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg"; import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg"; import portIcon from "../../../../resources/images/PortIcon.svg"; +import mirvIcon from "../../../../resources/images/MIRVIcon.svg"; import cityIcon from "../../../../resources/images/CityIconWhite.svg"; import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg"; import { renderNumber } from "../../Utils"; @@ -28,7 +29,8 @@ interface BuildItemDisplay { const buildTable: BuildItemDisplay[][] = [ [ { unitType: UnitType.AtomBomb, icon: atomBombIcon }, - { unitType: UnitType.MIRV, icon: hydrogenBombIcon }, + { unitType: UnitType.MIRV, icon: mirvIcon }, + { unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon }, { unitType: UnitType.Warship, icon: warshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon }, diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index aabde5bb6..dd1468265 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -195,7 +195,7 @@ export class ControlPanel extends LitElement implements Layer { type="range" min="1" max="100" - .value=${this.targetTroopRatio * 100} + .value=${(this.targetTroopRatio * 100).toString()} @input=${(e: Event) => { this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 100; @@ -225,7 +225,7 @@ export class ControlPanel extends LitElement implements Layer { type="range" min="1" max="100" - .value=${this.attackRatio * 100} + .value=${(this.attackRatio * 100).toString()} @input=${(e: Event) => { this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 100; diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 1178ddda4..407658016 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -80,6 +80,17 @@ export class EventsDisplay extends LitElement implements Layer { this.events = remainingEvents; this.requestUpdate(); } + + const myPlayer = this.game.myPlayer(); + if (!myPlayer) { + return; + } + myPlayer.incomingAttacks().forEach((a) => { + // console.log(`got incoming attack: ${JSON.stringify(a)}`); + }); + myPlayer.outgoingAttacks().forEach((a) => { + // console.log(`got outgoing attack: ${JSON.stringify(a)}`); + }); } private addEvent(event: Event) { @@ -125,10 +136,10 @@ export class EventsDisplay extends LitElement implements Layer { } const requestor = this.game.playerBySmallID( - update.requestorID, + update.requestorID ) as PlayerView; const recipient = this.game.playerBySmallID( - update.recipientID, + update.recipientID ) as PlayerView; this.addEvent({ @@ -139,7 +150,7 @@ export class EventsDisplay extends LitElement implements Layer { className: "btn", action: () => this.eventBus.emit( - new SendAllianceReplyIntentEvent(requestor, recipient, true), + new SendAllianceReplyIntentEvent(requestor, recipient, true) ), }, { @@ -147,7 +158,7 @@ export class EventsDisplay extends LitElement implements Layer { className: "btn-info", action: () => this.eventBus.emit( - new SendAllianceReplyIntentEvent(requestor, recipient, false), + new SendAllianceReplyIntentEvent(requestor, recipient, false) ), }, ], @@ -156,7 +167,7 @@ export class EventsDisplay extends LitElement implements Layer { createdAt: this.game.ticks(), onDelete: () => this.eventBus.emit( - new SendAllianceReplyIntentEvent(requestor, recipient, false), + new SendAllianceReplyIntentEvent(requestor, recipient, false) ), }); } @@ -168,7 +179,7 @@ export class EventsDisplay extends LitElement implements Layer { } const recipient = this.game.playerBySmallID( - update.request.recipientID, + update.request.recipientID ) as PlayerView; this.addEvent({ @@ -213,8 +224,8 @@ export class EventsDisplay extends LitElement implements Layer { update.player1ID === myPlayer.smallID() ? update.player2ID : update.player2ID === myPlayer.smallID() - ? update.player1ID - : null; + ? update.player1ID + : null; const other = this.game.playerBySmallID(otherID) as PlayerView; if (!other || !myPlayer.isAlive() || !other.isAlive()) return; @@ -250,7 +261,7 @@ export class EventsDisplay extends LitElement implements Layer { ? AllPlayers : this.game.playerBySmallID(update.emoji.recipientID); const sender = this.game.playerBySmallID( - update.emoji.senderID, + update.emoji.senderID ) as PlayerView; if (recipient == myPlayer) { @@ -306,7 +317,7 @@ export class EventsDisplay extends LitElement implements Layer { (event, index) => html` @@ -331,14 +342,14 @@ export class EventsDisplay extends LitElement implements Layer { > ${btn.text} - `, + ` )} ` : ""} - `, + ` )} diff --git a/src/client/index.html b/src/client/index.html index ec219127d..3be81e359 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -147,8 +147,10 @@
How to Play - Discord + Discord + Wiki
© 2025 diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 7f5881410..521c28b25 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -129,7 +129,7 @@ export class DefaultConfig implements Config { }; case UnitType.MIRV: return { - cost: () => 5_000_000, + cost: () => 10_000_000, territoryBound: false, }; case UnitType.MIRVWarhead: @@ -351,14 +351,28 @@ export class DefaultConfig implements Config { } maxPopulation(player: Player | PlayerView): number { - let maxPop = Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000; + let maxPop = + 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + + player.units(UnitType.City).length * this.cityPopulationIncrease(); + if (player.type() == PlayerType.Bot) { + return maxPop / 2; + } + + if (player.type() == PlayerType.Human) { return maxPop; } - return ( - maxPop * 2 + - player.units(UnitType.City).length * this.cityPopulationIncrease() - ); + + switch (this._gameConfig.difficulty) { + case Difficulty.Easy: + return maxPop * 0.5; + case Difficulty.Medium: + return maxPop * 0.7; + case Difficulty.Hard: + return maxPop * 1; + case Difficulty.Impossible: + return maxPop * 1.5; + } } populationIncreaseRate(player: Player): number { @@ -372,24 +386,6 @@ export class DefaultConfig implements Config { if (player.type() == PlayerType.Bot) { toAdd *= 0.7; } - let difficultyMultiplier = 1; - switch (this._gameConfig.difficulty) { - case Difficulty.Easy: - difficultyMultiplier = 0.3; - break; - case Difficulty.Medium: - difficultyMultiplier = 0.5; - break; - case Difficulty.Hard: - difficultyMultiplier = 1; - break; - case Difficulty.Impossible: - difficultyMultiplier = 1.2; - break; - } - if (player.type() == PlayerType.FakeHuman) { - toAdd *= difficultyMultiplier; - } return Math.min(player.population() + toAdd, max) - player.population(); } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 717a8c3ed..56d2bf27c 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -18,14 +18,14 @@ export class DevConfig extends DefaultConfig { } numSpawnPhaseTurns(): number { - return this.gameConfig().gameType == GameType.Singleplayer ? 20 : 100; + return this.gameConfig().gameType == GameType.Singleplayer ? 40 : 100; // return 100 } unitInfo(type: UnitType): UnitInfo { const info = super.unitInfo(type); const oldCost = info.cost; - info.cost = (p: Player) => oldCost(p) / 1000000000; + // info.cost = (p: Player) => oldCost(p) / 1000000000; return info; } 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/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts index e7f8af2f9..4f60b0954 100644 --- a/src/core/execution/MIRVExecution.ts +++ b/src/core/execution/MIRVExecution.ts @@ -26,9 +26,8 @@ export class MirvExecution implements Execution { private nuke: Unit; - private mirvRange = 500; - private warheadCount = 1000; - // private warheadRange = 5; + private mirvRange = 1500; + private warheadCount = 500; private random: PseudoRandom; @@ -92,7 +91,7 @@ export class MirvExecution implements Execution { private separate() { const dsts: TileRef[] = [this.dst]; - let attempts = 1000; + let attempts = 10000; while (attempts > 0 && dsts.length < this.warheadCount) { attempts--; const potential = this.randomLand(this.dst); @@ -106,6 +105,7 @@ export class MirvExecution implements Execution { (a, b) => this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst) ); + console.log(`got ${dsts.length} dsts!!`); for (const [i, dst] of dsts.entries()) { this.mg.addExecution( diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 2b6ac9361..dee6155ac 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -104,13 +104,13 @@ export class NukeExecution implements Execution { let magnitude; switch (this.type) { case UnitType.MIRVWarhead: - magnitude = { inner: 10, outer: 14 }; + magnitude = { inner: 20, outer: 25 }; break; case UnitType.AtomBomb: magnitude = { inner: 15, outer: 40 }; break; case UnitType.HydrogenBomb: - magnitude = { inner: 140, outer: 160 }; + magnitude = { inner: 120, outer: 140 }; break; } 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/GameUpdates.ts b/src/core/game/GameUpdates.ts index 7bd89182a..166297260 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -70,6 +70,12 @@ export interface UnitUpdate { constructionType?: UnitType; } +export interface AttackUpdate { + attackerID: number; + targetID: number; + troops: number; +} + export interface PlayerUpdate { type: GameUpdateType.Player; nameViewData?: NameViewData; @@ -90,6 +96,8 @@ export interface PlayerUpdate { isTraitor: boolean; targets: number[]; outgoingEmojis: EmojiMessage[]; + outgoingAttacks: AttackUpdate[]; + incomingAttacks: AttackUpdate[]; } export interface AllianceRequestUpdate { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index ff1416c7f..4459b3437 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -7,7 +7,7 @@ import { PlayerProfile, Unit, } from "./Game"; -import { PlayerUpdate } from "./GameUpdates"; +import { AttackUpdate, PlayerUpdate } from "./GameUpdates"; import { UnitUpdate } from "./GameUpdates"; import { NameViewData } from "./Game"; import { GameUpdateType } from "./GameUpdates"; @@ -106,6 +106,14 @@ export class PlayerView { ); } + outgoingAttacks(): AttackUpdate[] { + return this.data.outgoingAttacks; + } + + incomingAttacks(): AttackUpdate[] { + return this.data.incomingAttacks; + } + units(...types: UnitType[]): UnitView[] { return this.game .units(...types) diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 921008f5c..8c45a895e 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -17,8 +17,9 @@ import { Relation, EmojiMessage, PlayerProfile, + Attack, } from "./Game"; -import { PlayerUpdate } from "./GameUpdates"; +import { AttackUpdate, PlayerUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; import { ClientID } from "../Schemas"; import { @@ -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, @@ -111,6 +116,22 @@ export class PlayerImpl implements Player { isTraitor: this.isTraitor(), targets: this.targets().map((p) => p.smallID()), outgoingEmojis: this.outgoingEmojis(), + outgoingAttacks: this._outgoingAttacks.map( + (a) => + ({ + attackerID: a.attacker().smallID(), + targetID: a.target().smallID(), + troops: a.troops(), + } as AttackUpdate) + ), + incomingAttacks: this._incomingAttacks.map( + (a) => + ({ + attackerID: a.attacker().smallID(), + targetID: a.target().smallID(), + troops: a.troops(), + } as AttackUpdate) + ), }; } @@ -759,6 +780,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) &&