From b34dd8acf2c97b0ed3b14bece13abec7599a2841 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 20 Mar 2025 10:38:36 -0700 Subject: [PATCH 01/17] only send desync message once per client (#300) --- src/server/GameServer.ts | 83 +++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index b845f4604..f60a63efd 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -29,7 +29,7 @@ export enum GamePhase { } export class GameServer { - private outOfSyncClients = new Set(); + private sentDesyncMessageClients = new Set(); private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours @@ -428,52 +428,47 @@ export class GameServer { if (this.activeClients.length < 1) { return; } - if (this.turns.length % 10 == 0 && this.turns.length != 0) { - const lastHashTurn = this.turns.length - 10; + if (this.turns.length % 10 != 0 || this.turns.length < 10) { + // Check hashes every 10 turns + return; + } - let { mostCommonHash, outOfSyncClients } = - this.findOutOfSyncClients(lastHashTurn); + const lastHashTurn = this.turns.length - 10; - if (outOfSyncClients.length == 0) { - this.turns[lastHashTurn].hash = mostCommonHash; - } - - if ( - outOfSyncClients.length > 0 && - outOfSyncClients.length >= Math.floor(this.activeClients.length / 2) - ) { - // If half clients out of sync assume all are out of sync. - outOfSyncClients = this.activeClients; - } - - for (const oos of outOfSyncClients) { - if (!this.outOfSyncClients.has(oos.clientID)) { - this.log.warn( - `Game ${this.id}: has out of sync client ${oos.clientID} on turn ${lastHashTurn}`, - ); - this.outOfSyncClients.add(oos.clientID); - } - } - - const serverDesync = ServerDesyncSchema.safeParse({ - type: "desync", - turn: lastHashTurn, - correctHash: mostCommonHash, - clientsWithCorrectHash: - this.activeClients.length - outOfSyncClients.length, - totalActiveClients: this.activeClients.length, - }); - if (serverDesync.success) { - const desyncMsg = JSON.stringify(serverDesync.data); - for (const c of outOfSyncClients) { - this.log.info( - `game: ${this.id}: sending desync to client ${c.clientID}`, - ); - c.ws.send(desyncMsg); - } - } else { - this.log.warn(`failed to create desync message ${serverDesync.error}`); + let { mostCommonHash, outOfSyncClients } = + this.findOutOfSyncClients(lastHashTurn); + + if (outOfSyncClients.length == 0) { + this.turns[lastHashTurn].hash = mostCommonHash; + return; + } + + if (outOfSyncClients.length >= Math.floor(this.activeClients.length / 2)) { + // If half clients out of sync assume all are out of sync. + outOfSyncClients = this.activeClients; + } + + const serverDesync = ServerDesyncSchema.safeParse({ + type: "desync", + turn: lastHashTurn, + correctHash: mostCommonHash, + clientsWithCorrectHash: + this.activeClients.length - outOfSyncClients.length, + totalActiveClients: this.activeClients.length, + }); + if (!serverDesync.success) { + this.log.warn(`failed to create desync message ${serverDesync.error}`); + return; + } + + const desyncMsg = JSON.stringify(serverDesync.data); + for (const c of outOfSyncClients) { + if (this.sentDesyncMessageClients.has(c.clientID)) { + continue; } + this.sentDesyncMessageClients.add(c.clientID); + this.log.info(`game: ${this.id}: sending desync to client ${c.clientID}`); + c.ws.send(desyncMsg); } } From ce676d0efba2fc7994541ac635ea544f0372bae2 Mon Sep 17 00:00:00 2001 From: Xuarig Date: Thu, 20 Mar 2025 13:39:41 -0400 Subject: [PATCH 02/17] [Security] Added username sanitization on server (#299) Fixing issues #282 where players can bypass max username length by editing their storage. I added a sanitization on the server side to avoid all kind of cheat on the username as we can't trust clients --- src/core/game/PlayerImpl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index de96e2cb6..c1dd2aba4 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -44,6 +44,7 @@ import { andFN, manhattanDistFN, TileRef } from "./GameMap"; import { AttackImpl } from "./AttackImpl"; import { PseudoRandom } from "../PseudoRandom"; import { consolex } from "../Consolex"; +import { sanitizeUsername } from "../validations/username"; interface Target { tick: Tick; @@ -101,7 +102,7 @@ export class PlayerImpl implements Player { startTroops: number, ) { this._flag = playerInfo.flag; - this._name = playerInfo.name; + this._name = sanitizeUsername(playerInfo.name); this._targetTroopRatio = 95n; this._troops = toInt(startTroops); this._workers = 0n; From 00e7425e74bd5077e479018c641163cac827e898 Mon Sep 17 00:00:00 2001 From: Xuarig Date: Thu, 20 Mar 2025 13:40:52 -0400 Subject: [PATCH 03/17] [UI/Bugfix] Fixed the player info overlay displaying false when troops where equal to 0 (#298) The player info was: ![Before](https://github.com/user-attachments/assets/1fc3a4f1-3052-4a0d-b310-f94de10178a0) I fixed it to: ![After](https://github.com/user-attachments/assets/901ffc10-4d93-4418-9d56-e3b94a4fb5af) One thing I'm not sure about, is if we want to hide the attacking troops on 0 or always show it? In this PR it's always shown. --- .../graphics/layers/PlayerInfoOverlay.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 0d071bd1a..19ce5d599 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -205,14 +205,16 @@ export class PlayerInfoOverlay extends LitElement implements Layer { ${player.name()}
Type: ${playerType}
- ${player.troops() >= 1 && - html`
- Defending troops: ${renderTroops(player.troops())} -
`} - ${attackingTroops >= 1 && - html`
- Attacking troops: ${renderTroops(attackingTroops)} -
`} + ${player.troops() >= 1 + ? html`
+ Defending troops: ${renderTroops(player.troops())} +
` + : ""} + ${attackingTroops >= 1 + ? html`
+ Attacking troops: ${renderTroops(attackingTroops)} +
` + : ""}
Gold: ${renderNumber(player.gold())}
From 2094a29c8b70d6b491365277c5de5bf9162b6cc9 Mon Sep 17 00:00:00 2001 From: Xuarig Date: Thu, 20 Mar 2025 13:43:26 -0400 Subject: [PATCH 04/17] [UI] Added count on items of the construction menu (#297) As suggested on the discord server [here](https://discordapp.com/channels/1284581928254701718/1349545887801806869), I've added the count of building and permanent units on the construction menu. It's included in each button, and follows the background colour of the button on hover & disabled (see the screen below) ![In Game Render](https://github.com/user-attachments/assets/b753a494-e9cc-49e3-90b9-f3fce7d8d8b4) --- src/client/graphics/layers/BuildMenu.ts | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index ff803c383..8afa0f1af 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -27,6 +27,7 @@ interface BuildItemDisplay { unitType: UnitType; icon: string; description?: string; + countable?: boolean; } const buildTable: BuildItemDisplay[][] = [ @@ -35,47 +36,56 @@ const buildTable: BuildItemDisplay[][] = [ unitType: UnitType.AtomBomb, icon: atomBombIcon, description: "Small explosion", + countable: false, }, { unitType: UnitType.MIRV, icon: mirvIcon, description: "Huge explosion, only targets selected player", + countable: false, }, { unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon, description: "Large explosion", + countable: false, }, { unitType: UnitType.Warship, icon: warshipIcon, description: "Captures trade ships, destroys ships and boats", + countable: true, }, { unitType: UnitType.Port, icon: portIcon, description: "Sends trade ships to allies to generate gold", + countable: true, }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon, description: "Used to launch nukes", + countable: true, }, // needs new icon { unitType: UnitType.SAMLauncher, icon: shieldIcon, description: "Defends against incoming nukes", + countable: true, }, { unitType: UnitType.DefensePost, icon: shieldIcon, description: "Increase defenses of nearby borders", + countable: true, }, { unitType: UnitType.City, icon: cityIcon, description: "Increase max population", + countable: true, }, ], ]; @@ -124,6 +134,7 @@ export class BuildMenu extends LitElement implements Layer { width: 100%; } .build-button { + position: relative; width: 120px; height: 140px; border: 2px solid #444; @@ -177,6 +188,37 @@ export class BuildMenu extends LitElement implements Layer { .hidden { display: none !important; } + .build-count-chip { + position: absolute; + top: -10px; + right: -10px; + background-color: #2c2c2c; + color: white; + padding: 2px 10px; + border-radius: 10000px; + transition: all 0.3s ease; + font-size: 12px; + display: flex; + justify-content: center; + align-content: center; + border: 1px solid #444; + } + .build-button:not(:disabled):hover > .build-count-chip { + background-color: #3a3a3a; + border-color: #666; + } + .build-button:not(:disabled):active > .build-count-chip { + background-color: #4a4a4a; + } + .build-button:disabled > .build-count-chip { + background-color: #1a1a1a; + border-color: #333; + cursor: not-allowed; + } + .build-count { + font-weight: bold; + font-size: 14px; + } @media (max-width: 768px) { .build-menu { @@ -201,6 +243,13 @@ export class BuildMenu extends LitElement implements Layer { .build-cost { font-size: 11px; } + .build-count { + font-weight: bold; + font-size: 10px; + } + .build-count-chip { + padding: 1px 5px; + } } @media (max-width: 480px) { @@ -225,6 +274,13 @@ export class BuildMenu extends LitElement implements Layer { .build-cost { font-size: 9px; } + .build-count { + font-weight: bold; + font-size: 8px; + } + .build-count-chip { + padding: 0 3px; + } .build-button img { width: 24px; height: 24px; @@ -261,6 +317,15 @@ export class BuildMenu extends LitElement implements Layer { return 0; } + private count(item: BuildItemDisplay): string { + const player = this.game?.myPlayer(); + if (!player) { + return "?"; + } + + return player.units(item.unitType).length.toString(); + } + public onBuildSelected = (item: BuildItemDisplay) => { this.eventBus.emit( new BuildUnitIntentEvent( @@ -308,6 +373,11 @@ export class BuildMenu extends LitElement implements Layer { style="vertical-align: middle;" /> + ${item.countable + ? html`
+ ${this.count(item)} +
` + : ""} `, )} From e65efdebc7597ce3a8114be024c3a5a8844c0d24 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 20 Mar 2025 11:11:32 -0700 Subject: [PATCH 05/17] revert lobby times back to 60s, lobbies too small off times --- src/core/configuration/DefaultConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 6b63fcc06..2a9bece6f 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -55,7 +55,7 @@ export abstract class DefaultServerConfig implements ServerConfig { return 100; } gameCreationRate(): number { - return 30 * 1000; + return 60 * 1000; } lobbyMaxPlayers(map: GameMapType): number { if (map == GameMapType.World) { From d4ddffe1eb180b6731d0166283189eedff2d0ab4 Mon Sep 17 00:00:00 2001 From: Juan Broullon Date: Fri, 21 Mar 2025 18:06:09 +0100 Subject: [PATCH 06/17] fix: nginx not caching static resources (#254) The current nginx config isn't caching any images given they're not being served under the `/static/` path. --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index 703198e76..e1c3e4e47 100644 --- a/nginx.conf +++ b/nginx.conf @@ -43,7 +43,7 @@ server { error_log /var/log/nginx/error.log; # Static file handling with proper MIME types and consistent caching - location ~* ^/static/(.*)$ { + location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { proxy_pass http://127.0.0.1:3000; # Include MIME types From 68621f326a84b2247cc10edd117d7d34803d6707 Mon Sep 17 00:00:00 2001 From: Ilan Schemoul Date: Fri, 21 Mar 2025 18:17:33 +0100 Subject: [PATCH 07/17] sam do not target twice same nuke (#270) --- src/core/configuration/Config.ts | 2 + src/core/configuration/DefaultConfig.ts | 8 ++ src/core/execution/ConstructionExecution.ts | 2 +- src/core/execution/NukeExecution.ts | 2 +- src/core/execution/SAMLauncherExecution.ts | 34 +++-- src/core/execution/SAMMissileExecution.ts | 26 ++-- src/core/game/Game.ts | 3 + src/core/game/UnitImpl.ts | 11 +- tests/SAM.test.ts | 132 ++++++++++++++++++++ tests/util/TestConfig.ts | 6 +- tests/util/utils.ts | 7 +- 11 files changed, 200 insertions(+), 33 deletions(-) create mode 100644 tests/SAM.test.ts diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 0116b6992..e0cfc2d3a 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -46,6 +46,8 @@ export interface ServerConfig { } export interface Config { + samHittingChance(): number; + samCooldown(): Tick; spawnImmunityDuration(): Tick; serverConfig(): ServerConfig; gameConfig(): GameConfig; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 2a9bece6f..f3e1612f3 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -89,6 +89,14 @@ export class DefaultConfig implements Config { private _userSettings: UserSettings, ) {} + samHittingChance(): number { + return 0.8; + } + + samCooldown(): Tick { + return 100; + } + traitorDefenseDebuff(): number { return 0.8; } diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index efe416033..0f5914f1d 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -55,7 +55,7 @@ export class ConstructionExecution implements Execution { } const spawnTile = this.player.canBuild(this.constructionType, this.tile); if (spawnTile == false) { - consolex.warn(`cannot build ${UnitType.Construction}`); + consolex.warn(`cannot build ${this.constructionType}`); this.active = false; return; } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index f27c47b9d..3efc43525 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -87,7 +87,7 @@ export class NukeExecution implements Execution { // make the nuke unactive if it was intercepted if (!this.nuke.isActive()) { - consolex.warn(`Nuke destroyed before reaching target`); + consolex.log(`Nuke destroyed before reaching target`); this.active = false; return; } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 5f6eb89ea..810e9a5e6 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -7,6 +7,7 @@ import { Unit, PlayerID, UnitType, + MessageType, } from "../game/Game"; import { manhattanDistFN, TileRef } from "../game/GameMap"; import { SAMMissileExecution } from "./SAMMissileExecution"; @@ -22,7 +23,6 @@ export class SAMLauncherExecution implements Execution { private searchRangeRadius = 75; - private missileAttackRate = 75; // 7.5 seconds private lastMissileAttack = 0; private pseudoRandom: PseudoRandom; @@ -100,22 +100,38 @@ export class SAMLauncherExecution implements Execution { const cooldown = this.lastMissileAttack != 0 && - this.mg.ticks() - this.lastMissileAttack <= this.missileAttackRate; - if (this.post.isSamCooldown() != cooldown) { - this.post.setSamCooldown(cooldown); + this.mg.ticks() - this.lastMissileAttack <= + this.mg.config().samCooldown(); + + if (this.post.isSamCooldown() && !cooldown) { + this.post.setSamCooldown(false); } - if (this.target != null) { - if (!this.post.isSamCooldown()) { - this.lastMissileAttack = this.mg.ticks(); + if ( + this.target && + !this.post.isSamCooldown() && + !this.target.targetedBySAM() + ) { + this.lastMissileAttack = this.mg.ticks(); + this.post.setSamCooldown(true); + const random = this.pseudoRandom.next(); + const hit = random < this.mg.config().samHittingChance(); + + this.lastMissileAttack = this.mg.ticks(); + if (!hit) { + this.mg.displayMessage( + `Missile failed to intercept ${this.target.type()}`, + MessageType.ERROR, + this.post.owner().id(), + ); + } else { + this.target.setTargetedBySAM(true); this.mg.addExecution( new SAMMissileExecution( this.post.tile(), this.post.owner(), this.post, this.target, - this.mg, - this.pseudoRandom.next(), ), ); } diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 9a110c3be..1ea88acd4 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -15,20 +15,19 @@ export class SAMMissileExecution implements Execution { private active = true; private pathFinder: PathFinder; private SAMMissile: Unit; + private mg: Game; constructor( private spawn: TileRef, private _owner: Player, private ownerUnit: Unit, private target: Unit, - private mg: Game, - private pseudoRandom: number, private speed: number = 12, - private hittingChance: number = 0.75, ) {} init(mg: Game, ticks: number): void { this.pathFinder = PathFinder.Mini(mg, 2000, true, 10); + this.mg = mg; } tick(ticks: number): void { @@ -63,22 +62,13 @@ export class SAMMissileExecution implements Execution { ); switch (result.type) { case PathFindResultType.Completed: + this.mg.displayMessage( + `Missile intercepted ${this.target.type()}`, + MessageType.SUCCESS, + this._owner.id(), + ); this.active = false; - if (this.pseudoRandom < this.hittingChance) { - this.target.delete(); - - this.mg.displayMessage( - `Missile succesfully intercepted ${this.target.type()}`, - MessageType.SUCCESS, - this._owner.id(), - ); - } else { - this.mg.displayMessage( - `Missile failed to target ${this.target.type()}`, - MessageType.ERROR, - this._owner.id(), - ); - } + this.target.delete(); this.SAMMissile.delete(false); return; case PathFindResultType.NextTile: diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 77c9a7d94..d61143fa8 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -244,6 +244,9 @@ export interface Unit { setMoveTarget(cell: TileRef): void; moveTarget(): TileRef | null; + setTargetedBySAM(targeted: boolean): void; + targetedBySAM(): boolean; + // Mutations setTroops(troops: number): void; delete(displayerMessage?: boolean): void; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index aaa783508..d31e6b111 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -15,10 +15,11 @@ export class UnitImpl implements Unit { // Currently only warship use it private _target: Unit = null; private _moveTarget: TileRef = null; + private _targetedBySAM = false; private _constructionType: UnitType = undefined; - private _isSamCooldown: boolean; + private _isSamCooldown: boolean = false; private _dstPort: Unit | null = null; // Only for trade ships private _detonationDst: TileRef | null = null; // Only for nukes private _warshipTarget: Unit | null = null; @@ -204,4 +205,12 @@ export class UnitImpl implements Unit { moveTarget(): TileRef | null { return this._moveTarget; } + + setTargetedBySAM(targeted: boolean): void { + this._targetedBySAM = targeted; + } + + targetedBySAM(): boolean { + return this._targetedBySAM; + } } diff --git a/tests/SAM.test.ts b/tests/SAM.test.ts new file mode 100644 index 000000000..3e033c61c --- /dev/null +++ b/tests/SAM.test.ts @@ -0,0 +1,132 @@ +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { setup } from "./util/Setup"; +import { constructionExecution } from "./util/utils"; +import { NukeExecution } from "../src/core/execution/NukeExecution"; +import { TileRef } from "../src/core/game/GameMap"; + +let game: Game; +let attacker: Player; +let defender: Player; + +function attackerBuildsNuke( + source: TileRef, + target: TileRef, + initialize = true, +) { + game.addExecution( + new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source), + ); + if (initialize) { + game.executeNextTick(); + game.executeNextTick(); + } +} + +function defenderBuildsSam(x: number, y: number) { + constructionExecution(game, defender.id(), x, y, UnitType.SAMLauncher); +} + +describe("SAM", () => { + beforeEach(async () => { + game = await setup("Plains", { infiniteGold: true, instantBuild: true }); + const defender_info = new PlayerInfo( + "us", + "defender_id", + PlayerType.Human, + null, + "defender_id", + ); + const attacker_info = new PlayerInfo( + "fr", + "attacker_id", + PlayerType.Human, + null, + "attacker_id", + ); + game.addPlayer(defender_info, 1000); + game.addPlayer(attacker_info, 1000); + + game.addExecution( + new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)), + new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + defender = game.player("defender_id"); + attacker = game.player("attacker_id"); + + constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo); + }); + + test("one sam should take down one nuke", async () => { + defenderBuildsSam(1, 1); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + }); + + test("sam should only get one nuke at a time", async () => { + defenderBuildsSam(1, 1); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1), false); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + }); + + test("sam should cooldown as long as configured", async () => { + defenderBuildsSam(1, 1); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + + for (let i = 0; i < game.config().samCooldown() - 1; i++) { + game.executeNextTick(); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe( + true, + ); + } + + game.executeNextTick(); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + }); + + test("two sams should not target twice same nuke", async () => { + defenderBuildsSam(1, 1); + defenderBuildsSam(1, 2); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + + expect(defender.units(UnitType.SAMLauncher)).toHaveLength(2); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + const sams = defender.units(UnitType.SAMLauncher); + // Only one sam must have shot + expect( + (sams[0].isSamCooldown() && !sams[1].isSamCooldown()) || + (sams[1].isSamCooldown() && !sams[0].isSamCooldown()), + ).toBe(true); + }); +}); diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts index e48a63162..1e52658c0 100644 --- a/tests/util/TestConfig.ts +++ b/tests/util/TestConfig.ts @@ -1,3 +1,7 @@ import { DefaultConfig } from "../../src/core/configuration/DefaultConfig"; -export class TestConfig extends DefaultConfig {} +export class TestConfig extends DefaultConfig { + samHittingChance(): number { + return 1; + } +} diff --git a/tests/util/utils.ts b/tests/util/utils.ts index 17ca1a447..e3175423e 100644 --- a/tests/util/utils.ts +++ b/tests/util/utils.ts @@ -15,9 +15,12 @@ export function constructionExecution( unit: UnitType, ) { game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit)); - // Init + // Init exec game.executeNextTick(); - // Exec + // Exec construction execution game.executeNextTick(); + // Add the execution related to the building + game.executeNextTick(); + // First tick of the execution of the constructed structure/unit game.executeNextTick(); } From 2cac950574d891dba2a5504a0a80dad818edef60 Mon Sep 17 00:00:00 2001 From: APuddle210 Date: Fri, 21 Mar 2025 13:19:59 -0400 Subject: [PATCH 08/17] Add gateway map to rotation (#301) Changes made via GitHub editor. I'll test as soon as I get home ~2.5-3 hours from now. --- resources/maps/GatewayToTheAtlantic.json | 6 +++--- src/server/Master.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/maps/GatewayToTheAtlantic.json b/resources/maps/GatewayToTheAtlantic.json index d22cc0a4b..b0f6d615f 100644 --- a/resources/maps/GatewayToTheAtlantic.json +++ b/resources/maps/GatewayToTheAtlantic.json @@ -1,7 +1,7 @@ { - "name": "Britannia", - "width": 2000, - "height": 1397, + "name": "GatewayToTheAtlantic", + "width": 2216, + "height": 1968, "nations": [ { "coordinates": [2144, 344], diff --git a/src/server/Master.ts b/src/server/Master.ts index 2c89fe5a2..9c3d4be6f 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -285,6 +285,7 @@ function getNextMap(): GameMapType { Asia: 2, Mars: 2, Britannia: 2, + GatewayToTheAtlantic: 2, }; Object.keys(GameMapType).forEach((key) => { From 2392f3a2fedc7efc5e720973211af49237aea3fb Mon Sep 17 00:00:00 2001 From: APuddle210 Date: Fri, 21 Mar 2025 13:21:24 -0400 Subject: [PATCH 09/17] Update NorthAmerica.json (#303) Just removing some bots from the densest populated parts of the map. Hopefully will improve responsiveness of the game for some players when on this map. Also correcting the map dimensions in the json (I forgot to update those). --- resources/maps/NorthAmerica.json | 112 +------------------------------ 1 file changed, 2 insertions(+), 110 deletions(-) diff --git a/resources/maps/NorthAmerica.json b/resources/maps/NorthAmerica.json index 8614e8f05..5bfe3765a 100644 --- a/resources/maps/NorthAmerica.json +++ b/resources/maps/NorthAmerica.json @@ -1,7 +1,7 @@ { "name": "NorthAmerica", - "width": 2000, - "height": 1000, + "width": 3000, + "height": 1500, "nations": [ { "coordinates": [1814, 1083], @@ -21,24 +21,12 @@ "strength": 2, "flag": "mx" }, - { - "coordinates": [1684, 1301], - "name": "Belize", - "strength": 1, - "flag": "bz" - }, { "coordinates": [1667, 1341], "name": "Guatemala", "strength": 1, "flag": "gt" }, - { - "coordinates": [1681, 1362], - "name": "El Salvador", - "strength": 1, - "flag": "sv" - }, { "coordinates": [1727, 1335], "name": "Honduras", @@ -51,12 +39,6 @@ "strength": 1, "flag": "ni" }, - { - "coordinates": [1801, 1462], - "name": "Costa Rica", - "strength": 1, - "flag": "cr" - }, { "coordinates": [1858, 1453], "name": "Panama", @@ -81,30 +63,6 @@ "strength": 1, "flag": "cu" }, - { - "coordinates": [1895, 1279], - "name": "Jamaica", - "strength": 1, - "flag": "jm" - }, - { - "coordinates": [1994, 1270], - "name": "Haiti", - "strength": 1, - "flag": "ht" - }, - { - "coordinates": [2028, 1267], - "name": "Dominican Republic", - "strength": 1, - "flag": "do" - }, - { - "coordinates": [1748, 1000], - "name": "Alabama", - "strength": 1, - "flag": "Alabama" - }, { "coordinates": [500, 345], "name": "Alaska", @@ -117,12 +75,6 @@ "strength": 1, "flag": "Arizona" }, - { - "coordinates": [1620, 980], - "name": "Arkansas", - "strength": 1, - "flag": "Arkansas" - }, { "coordinates": [1082, 896], "name": "California", @@ -159,18 +111,6 @@ "strength": 1, "flag": "Illinois" }, - { - "coordinates": [1726, 848], - "name": "Indiana", - "strength": 1, - "flag": "Indiana" - }, - { - "coordinates": [1607, 814], - "name": "Iowa", - "strength": 1, - "flag": "Iowa" - }, { "coordinates": [1513, 904], "name": "Kansas", @@ -195,18 +135,6 @@ "strength": 1, "flag": "Maine" }, - { - "coordinates": [1912, 867], - "name": "Maryland", - "strength": 1, - "flag": "Maryland" - }, - { - "coordinates": [1995, 816], - "name": "Massachusetts", - "strength": 1, - "flag": "" - }, { "coordinates": [1751, 791], "name": "Michigan", @@ -249,18 +177,6 @@ "strength": 1, "flag": "Nevada" }, - { - "coordinates": [1996, 759], - "name": "New Hampshire", - "strength": 1, - "flag": "New_Hampshire" - }, - { - "coordinates": [1939, 960], - "name": "New Jersey", - "strength": 1, - "flag": "New_Jersey" - }, { "coordinates": [1342, 967], "name": "New Mexico", @@ -273,12 +189,6 @@ "strength": 1, "flag": "New_York" }, - { - "coordinates": [1883, 942], - "name": "North Carolina", - "strength": 1, - "flag": "North_Carolina" - }, { "coordinates": [1547, 742], "name": "North Dakota", @@ -309,12 +219,6 @@ "strength": 1, "flag": "Pennsylvania" }, - { - "coordinates": [2014, 819], - "name": "Rhode Island", - "strength": 1, - "flag": "Rhode_Island" - }, { "coordinates": [1839, 971], "name": "South Carolina", @@ -357,18 +261,6 @@ "strength": 1, "flag": "Washington" }, - { - "coordinates": [1836, 867], - "name": "West Virginia", - "strength": 1, - "flag": "West Virginia" - }, - { - "coordinates": [1681, 781], - "name": "Wisconsin", - "strength": 1, - "flag": "Wisconsin" - }, { "coordinates": [1351, 786], "name": "Wyoming", From c4614fe0baf8fc5ba45e8b89146278d605c12544 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Sat, 22 Mar 2025 05:51:04 +0900 Subject: [PATCH 10/17] Add multi-language support to the start screen and help-modal (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request adds multi-language support to the start screen. Languages are managed via JSON files, making it easy to add new languages in the future. I added a basic language selection button for switching between languages. However, I believe it would benefit from a better design — my design skills are limited, so I apologize in advance. Looking forward to your feedback and possible design improvements. --- resources/lang/en.json | 92 ++++++++++++++++++++++++++ resources/lang/ja.json | 81 +++++++++++++++++++++++ src/client/HelpModal.ts | 140 ++++++++++++++++++++-------------------- src/client/index.html | 20 +++++- src/client/lang.js | 57 ++++++++++++++++ 5 files changed, 319 insertions(+), 71 deletions(-) create mode 100644 resources/lang/en.json create mode 100644 resources/lang/ja.json create mode 100644 src/client/lang.js diff --git a/resources/lang/en.json b/resources/lang/en.json new file mode 100644 index 000000000..1c2795114 --- /dev/null +++ b/resources/lang/en.json @@ -0,0 +1,92 @@ +{ + "main": { + "join_discord": "Join the Discord!", + "create_lobby": "Create Lobby", + "join_lobby": "Join Lobby", + "single_player": "Single Player", + "instructions": "Instructions", + "how_to_play": "How to Play", + "wiki": "Wiki" + }, + "help_modal": { + "hotkeys": "Hotkeys", + + "table_key": "Key", + + "table_action": "Action", + + "action_alt_view": "Alternate view (terrain/countries)", + "action_attack_altclick": "Attack (when left click is set to open menu)", + "action_build": "Open build menu", + "action_center": "Center camera on player", + "action_zoom": "Zoom out/in", + "action_move_camera": "Move camera", + "action_ratio_change": "Decrease/Increase attack ratio", + "action_reset_gfx": "Reset graphics", + + "ui_section": "Game UI", + "ui_leaderboard": "Leaderboard", + "ui_leaderboard_desc": "Shows the top players of the game and their names, % owned land and gold.", + "ui_control": "Control panel", + "ui_control_desc": "The control panel contains the following elements:", + "ui_pop": "Pop - The amount of units you have, your max population and the rate at which you gain them.", + "ui_gold": "Gold - The amount of gold you have and the rate at which you gain it.", + "ui_troops_workers": "Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.", + "ui_attack_ratio": "Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.", + + "ui_options": "Options", + "ui_options_desc": "The following elements can be found inside:", + "option_pause": "Pause/Unpause the game - Only available in single player mode.", + "option_timer": "Timer - Time passed since the start of the game.", + "option_exit": "Exit button.", + "option_settings": "Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.", + + "radial_title": "Radial menu", + "radial_desc": "Right clicking (or touch on mobile) opens the radial menu. From there you can:", + "radial_build": "Open the build menu.", + "radial_info": "Open the Info menu.", + "radial_boat": "Send a boat to attack at the selected location (only available if you have access to water).", + "radial_close": "Close the menu.", + + "info_title": "Info menu", + "info_enemy_desc": "Contains information such for the selected player name, gold, troops, and if the player is a traitor.Traitor is a player who betrayed and attacked a player who was in an alliance with them. The icons below represent the following interactions:", + "info_target": "Place a target mark on the player, marking it for all allies, used to coordinate attacks.", + "info_alliance": "Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.", + "info_emoji": "Send an emoji to the player.", + + "info_ally_panel": "Ally info panel", + "info_ally_desc": "When you ally with a player, the following new icons become available:", + "ally_betray": "Betray your ally, ending the alliance. You will now have a permanent icon stuck next to your name. Bots are less likely to ally with you and players will think twice before doing so.", + "ally_donate": "Donate some of your troops to your ally. Used when they're low on troops and are being attacked, or when they need that extra power to crush an enemy.", + + "build_menu_title": "Build menu", + "build_name": "Name", + "build_icon": "Icon", + "build_desc": "Description", + + "build_city": "City", + "build_city_desc": "Increases your max population. Useful when you can't expand your territory or you're about to hit your population limit.", + "build_defense": "Defense Post", + "build_defense_desc": "Increases defenses around nearby borders. Attacks from enemies are slower and have more casualties.", + "build_port": "Port", + "build_port_desc": "Automatically sends trade ships between ports of your country and other countries (except if you clicked \"stop trade\" on them or they clicked \"stop trade on you\"), giving gold to both sides. Allows building Battleships. Can only be built near water.", + "build_warship": "Warship", + "build_warship_desc": "Patrols in an area, capturing trade ships and destroying enemy Warships and Boats. Spawns from the nearest Port and patrols the area you first clicked to build it.", + "build_silo": "Missile Silo", + "build_silo_desc": "Allows launching missiles.", + "build_sam": "SAM Launcher", + "build_sam_desc": "Has a 75% chance to intercept enemy missiles in its 100 pixel range. The SAM has a 7.5 second cooldown and cannot intercept MIRVs.", + "build_atom": "Atom Bomb", + "build_atom_desc": "Small explosive bomb that destroys territory, buildings, ships and boats. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.", + "build_hydrogen": "Hydrogen Bomb", + "build_hydrogen_desc": "Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.", + "build_mirv": "MIRV", + "build_mirv_desc": "The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.", + + "player_icons": "Player icons", + "icon_desc": "Examples of some of the ingame icons you will encounter and what they mean:", + "icon_crown": "Crown - This is the number 1 player in the leaderboard", + "icon_traitor": "Crossed swords - Traitor. This player attacked an ally.", + "icon_ally": "Handshake - Ally. This player is your ally." + } +} diff --git a/resources/lang/ja.json b/resources/lang/ja.json new file mode 100644 index 000000000..05ebc89a2 --- /dev/null +++ b/resources/lang/ja.json @@ -0,0 +1,81 @@ +{ + "main": { + "join_discord": "Discordサーバーに参加!", + "create_lobby": "ロビーを作成", + "join_lobby": "ロビーに参加", + "single_player": "シングルプレイヤー", + "instructions": "説明書", + "how_to_play": "遊び方", + "wiki": "ウィキ" + }, + "help_modal": { + "hotkeys": "ホットキー", + "table_key": "キー", + "table_action": "アクション", + "action_alt_view": "表示切替(地形/国家)", + "action_attack_altclick": "攻撃(左クリックがメニューの場合)", + "action_build": "建設メニューを開く", + "action_center": "カメラをプレイヤーに寄せる", + "action_zoom": "ズームアウト/イン", + "action_move_camera": "カメラ移動", + "action_ratio_change": "攻撃比率を増減", + "action_reset_gfx": "グラフィックをリセット", + "ui_section": "ゲームUI", + "ui_leaderboard": "リーダーボード", + "ui_leaderboard_desc": "上位プレイヤーの名前、占領率、資産を表示します。", + "ui_control": "コントロールパネル", + "ui_control_desc": "コントロールパネルには以下が含まれます:", + "ui_pop": "人口 - 現在のユニット数、最大人口、増加速度を表示。", + "ui_gold": "資産 - 所持金と増加速度を表示。", + "ui_troops_workers": "兵士と労働者 - 攻撃/防御/金生成のための配分。", + "ui_attack_ratio": "攻撃比率 - 攻撃時の使用兵士の割合。", + "ui_options": "オプション", + "ui_options_desc": "以下の項目が含まれます:", + "option_pause": "ゲームの一時停止(シングルプレイヤーのみ)", + "option_timer": "タイマー - ゲーム開始からの経過時間", + "option_exit": "終了ボタン", + "option_settings": "設定メニュー(表示/ダークモード/クリック設定など)", + "radial_title": "ラジアルメニュー", + "radial_desc": "右クリックまたはタップでメニューを開きます:", + "radial_build": "建設メニューを開く。", + "radial_info": "情報メニューを開く。", + "radial_boat": "ボートを送る(海にアクセスできる場合)。", + "radial_close": "メニューを閉じる。", + "info_title": "情報メニュー", + "info_enemy_desc": "選択プレイヤーの名前、資産、兵士数、裏切り者かどうかを表示。裏切り者とは、同盟を結んでいたプレイヤーを裏切り、攻撃したプレイヤーのことです。以下のアイコンは、以下のやりとりを表しています:", + "info_target": "ターゲットマークを付ける(攻撃協調用)。", + "info_alliance": "同盟を申し込む。", + "info_emoji": "絵文字を送る。", + "info_ally_panel": "同盟情報パネル", + "info_ally_desc": "同盟後に使える新しいアイコン:", + "ally_betray": "同盟を裏切る(アイコンが固定されます)。", + "ally_donate": "兵士を味方に寄付。", + "build_menu_title": "建設メニュー", + "build_name": "名前", + "build_icon": "アイコン", + "build_desc": "説明", + "build_city": "都市", + "build_city_desc": "最大人口を増加します。領土を拡張できない時や人口上限に近い時に有効です。", + "build_defense": "防衛ポスト", + "build_defense_desc": "国境の防御力を上げます。敵の攻撃が遅くなり、敵の被害が増加します。", + "build_port": "港", + "build_port_desc": "自国と他国の港の間で自動的に貿易船を送ります(相手か自分が貿易停止していない場合)。両者にゴールドをもたらします。バトルシップの建造も可能。水辺にのみ建設可能です。", + "build_warship": "戦艦", + "build_warship_desc": "周囲を巡回し、貿易船を拿捕したり、敵の戦艦やボートを破壊します。最寄りの港から出撃し、建設時に指定した場所を巡回します。", + "build_silo": "ミサイル格納庫", + "build_silo_desc": "ミサイルの発射を可能にします。", + "build_sam": "SAMランチャー", + "build_sam_desc": "100ピクセル範囲内の敵ミサイルを75%の確率で迎撃します。7.5秒のクールダウンがあり、MIRVには対応していません。", + "build_atom": "原子爆弾", + "build_atom_desc": "小型の爆弾で、領土・建物・船舶・ボートを破壊します。最寄りのミサイル格納庫から発射され、最初にクリックした場所に着弾します。", + "build_hydrogen": "水素爆弾", + "build_hydrogen_desc": "大型の爆弾。最寄りのミサイル格納庫から発射され、クリックした場所に着弾します。", + "build_mirv": "MIRV", + "build_mirv_desc": "ゲーム中最強の爆弾。複数の小型爆弾に分裂し、広範囲を攻撃します。最初に指定したプレイヤーのみを攻撃し、最寄りのミサイル格納庫から発射されます。", + "player_icons": "プレイヤーアイコン", + "icon_desc": "ゲーム内アイコンとその意味:", + "icon_crown": "王冠 - リーダーボード1位のプレイヤー", + "icon_traitor": "交差した剣 - 裏切り者(同盟を攻撃)", + "icon_ally": "握手 - 味方(同盟関係)" + } +} diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts index de76c943e..0dec60f7f 100644 --- a/src/client/HelpModal.ts +++ b/src/client/HelpModal.ts @@ -226,50 +226,50 @@ export class HelpModal extends LitElement { ×
-
Hotkeys
+
Hotkeys
- - + + - + - + - + - + - + - + - + - + - +
KeyActionKeyAction
SpaceAlternate view (terrain/countries)Alternate view (terrain/countries)
Shift + left clickAttack (when left click is set to open menu)Attack (when left click is set to open menu)
Ctrl + left clickOpen build menuOpen build menu
CCenter camera on playerCenter camera on player
Q / EZoom out/inZoom out/in
W / A / S / DMove cameraMove camera
1 / 2Decrease/Increase attack ratioDecrease/Increase attack ratio
Shift + scroll down / scroll upDecrease/Increase attack ratioDecrease/Increase attack ratio
ALT + RReset graphicsReset graphics
@@ -277,14 +277,14 @@ export class HelpModal extends LitElement {
-
Game UI
+
Game UI
-
Leaderboard
+
Leaderboard
Leaderboard
-

Shows the top players of the game and their names, % owned land and gold.

+

Shows the top players of the game and their names, % owned land and gold.

@@ -292,16 +292,16 @@ export class HelpModal extends LitElement {
-
Control panel
+
Control panel
Control panel
-

The control panel contains the following elements:

+

The control panel contains the following elements:

    -
  • Pop - The amount of units you have, your max population and the rate at which you gain them.
  • -
  • Gold - The amount of gold you have and the rate at which you gain it.
  • -
  • Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.
  • -
  • Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.
  • +
  • Pop - The amount of units you have, your max population and the rate at which you gain them.
  • +
  • Gold - The amount of gold you have and the rate at which you gain it.
  • +
  • Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.
  • +
  • Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider.
@@ -310,34 +310,34 @@ export class HelpModal extends LitElement {
-
Options
+
Options
Options
-

The following elements can be found inside:

+

The following elements can be found inside:

    -
  • Pause/Unpause the game - Only available in single player mode.
  • -
  • Timer - Time passed since the start of the game.
  • -
  • Exit button.
  • -
  • Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.
  • +
  • Pause/Unpause the game - Only available in single player mode.
  • +
  • Timer - Time passed since the start of the game.
  • +
  • Exit button.
  • +
  • Settings - Open the settings menu. Inside you can toggle the Alternate View, Dark Mode, Emojis and action on left click.

-
Radial menu
+
Radial menu
Radial menu
-

Right clicking (or touch on mobile) opens the radial menu. From there you can:

+

Right clicking (or touch on mobile) opens the radial menu. From there you can:

    -
  • - Open the build menu.
  • +
  • - Open the build menu.
  • - - Open the Info menu.
  • -
  • - Send a boat to attack at the selected location (only available if you have access to water).
  • -
  • - Close the menu.
  • + - Open the Info menu. +
  • - Send a boat to attack at the selected location (only available if you have access to water).
  • +
  • - Close the menu.
@@ -345,20 +345,20 @@ export class HelpModal extends LitElement {
-
Info menu
+
Info menu
-
Enemy info panel
+
Enemy info panel
Enemy info panel
-

+

Contains information such for the selected player name, gold, troops, and if the player is a traitor. Traitor is a player who betrayed and attacked a player who was in an alliance with them. The icons below represent the following interactions:

    -
  • - Place a target mark on the player, marking it for all allies, used to coordinate attacks.
  • -
  • - Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.
  • -
  • - Send an emoji to the player.
  • +
  • - Place a target mark on the player, marking it for all allies, used to coordinate attacks.
  • +
  • - Send an alliance request to the player. Allies can share resources and troops, but can't attack each other.
  • +
  • - Send an emoji to the player.
@@ -367,16 +367,16 @@ export class HelpModal extends LitElement {
-
Ally info panel
+
Ally info panel
Ally info panel
-

+

When you ally with a player, the following new icons become available:

    -
  • - Betray your ally, ending the alliance. You will now have a permanent icon stuck next to your name. Bots are less likely to ally with you and players will think twice before doing so.
  • -
  • - Donate some of your troops to your ally. Used when they're low on troops and are being attacked, or when they need that extra power to crush an enemy.
  • +
  • - Betray your ally, ending the alliance. You will now have a permanent icon stuck next to your name. Bots are less likely to ally with you and players will think twice before doing so.
  • +
  • - Donate some of your troops to your ally. Used when they're low on troops and are being attacked, or when they need that extra power to crush an enemy.
@@ -385,37 +385,37 @@ export class HelpModal extends LitElement {
-
Build menu
+
Build menu
- - - + + + - + - - + - - + - - + - - + - + - + - - + - + - + - + - + - +
NameIconDescriptionNameIconDescription
CityCity
+ Increases your max population. Useful when you can't expand your territory or you're about to hit your population limit.
Defense PostDefense Post
+ Increases defenses around nearby borders. Attacks from enemies are slower and have more casualties.
PortPort
+ Automatically sends trade ships between ports of your country and other countries (except if you clicked "stop trade" on them or they clicked "stop trade on you"), giving @@ -424,39 +424,39 @@ export class HelpModal extends LitElement {
WarshipWarship
+ Patrols in an area, capturing trade ships and destroying enemy Warships and Boats. Spawns from the nearest Port and patrols the area you first clicked to build it.
Missile SiloMissile Silo
Allows launching missiles.Allows launching missiles.
SAM LauncherSAM Launcher
Has a 75% chance to intercept enemy missiles in it's 100 pixel range. + Has a 75% chance to intercept enemy missiles in it's 100 pixel range. The SAM has a 7.5 second cooldown and can not intercept MIRVs.
Atom BombAtom Bomb
Small explosive bomb that destroys territory, buildings, ships and boats. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.Small explosive bomb that destroys territory, buildings, ships and boats. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.
Hydrogen BombHydrogen Bomb
Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.Large explosive bomb. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.
MIRVMIRV
The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.
@@ -465,21 +465,21 @@ export class HelpModal extends LitElement {
-
Player icons
-

Examples of some of the ingame icons you will encounter and what they mean:

+
Player icons
+

Examples of some of the ingame icons you will encounter and what they mean:

-
Crown - This is the number 1 player in the leaderboard
+
Crown - This is the number 1 player in the leaderboard
Number 1 player
-
Crossed swords - Traitor. This player attacked an ally.
+
Crossed swords - Traitor. This player attacked an ally.
Traitor
-
Handshake - Ally. This player is your ally.
+
Handshake - Ally. This player is your ally.
Ally
diff --git a/src/client/index.html b/src/client/index.html index 9a1e95eeb..6ede5b98f 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -249,7 +249,7 @@ alt="Discord" src="../../resources/icons/discord.svg" /> - Join the Discord! + Join the Discord!
@@ -262,12 +262,14 @@ >
+
+ +
@@ -348,12 +363,14 @@
How to Play Wiki +