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 @@
© 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) &&