From 665a8c382364d99d3a5d0342851daef4d4c2d00d Mon Sep 17 00:00:00 2001 From: Readixyee <49241765+Readixyee@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:03:15 +0100 Subject: [PATCH 1/8] Executions dont switch owner (#326) when an building is taken over by another player the execution for it doesnt change its owner this makes it so when a sam is captured it tries to intercept your own nukes and doesnt intercept the ones by the previous player this change makes executions of buildings automaticly switch their owner --- src/core/execution/CityExecution.ts | 6 +++--- src/core/execution/ConstructionExecution.ts | 8 ++++---- src/core/execution/DefensePostExecution.ts | 6 +++--- src/core/execution/MissileSiloExecution.ts | 8 ++++---- src/core/execution/MoveWarshipExecution.ts | 5 ----- src/core/execution/PortExecution.ts | 8 ++++---- src/core/execution/SAMLauncherExecution.ts | 8 ++++---- src/core/execution/SAMMissileExecution.ts | 3 --- src/core/execution/ShellExecution.ts | 3 --- src/core/execution/TradeShipExecution.ts | 4 ---- src/core/execution/WarshipExecution.ts | 9 ++------- src/core/game/Game.ts | 2 -- src/core/game/PlayerImpl.ts | 5 ----- 13 files changed, 24 insertions(+), 51 deletions(-) diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts index d6cbaf2c6..3b64f88eb 100644 --- a/src/core/execution/CityExecution.ts +++ b/src/core/execution/CityExecution.ts @@ -44,10 +44,10 @@ export class CityExecution implements Execution { this.active = false; return; } - } - owner(): Player { - return null; + if (this.player != this.city.owner()) { + this.player = this.city.owner(); + } } isActive(): boolean { diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 0f5914f1d..cc54cdba5 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -76,6 +76,10 @@ export class ConstructionExecution implements Execution { return; } + if (this.player != this.construction.owner()) { + this.player = this.construction.owner(); + } + if (this.ticksUntilComplete == 0) { this.player = this.construction.owner(); this.construction.delete(false); @@ -123,10 +127,6 @@ export class ConstructionExecution implements Execution { } } - owner(): Player { - return null; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 74001526b..5f8811e66 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -45,10 +45,10 @@ export class DefensePostExecution implements Execution { this.active = false; return; } - } - owner(): Player { - return null; + if (this.player != this.post.owner()) { + this.player = this.post.owner(); + } } isActive(): boolean { diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index fb4547542..f9ae6f3d5 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -42,11 +42,11 @@ export class MissileSiloExecution implements Execution { return; } this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile); - } - } - owner(): Player { - return null; + if (this.player != this.silo.owner()) { + this.player = this.silo.owner(); + } + } } isActive(): boolean { diff --git a/src/core/execution/MoveWarshipExecution.ts b/src/core/execution/MoveWarshipExecution.ts index 711c051ab..77d2fbe50 100644 --- a/src/core/execution/MoveWarshipExecution.ts +++ b/src/core/execution/MoveWarshipExecution.ts @@ -25,11 +25,6 @@ export class MoveWarshipExecution implements Execution { this.active = false; } - owner(): Player { - const warship = this.mg.units().find((u) => u.id() == this.unitId); - return warship ? warship.owner() : null; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 5aafe9cee..547a05229 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -66,6 +66,10 @@ export class PortExecution implements Execution { return; } + if (this._owner != this.port.owner().id()) { + this._owner = this.port.owner().id(); + } + const totalNbOfPorts = this.mg.units(UnitType.Port).length; if ( !this.random.chance(this.mg.config().tradeShipSpawnRate(totalNbOfPorts)) @@ -88,10 +92,6 @@ export class PortExecution implements Execution { ); } - owner(): Player { - return null; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index be5d4cfe7..50d8e6a90 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -57,6 +57,10 @@ export class SAMLauncherExecution implements Execution { return; } + if (this.player != this.post.owner()) { + this.player = this.post.owner(); + } + if (!this.pseudoRandom) { this.pseudoRandom = new PseudoRandom(this.post.id()); } @@ -133,10 +137,6 @@ export class SAMLauncherExecution implements Execution { } } - owner(): Player { - return null; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 1ea88acd4..b21f41f7a 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -85,9 +85,6 @@ export class SAMMissileExecution implements Execution { } } - owner(): Player { - return null; - } isActive(): boolean { return this.active; } diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 559c0efd2..544ad0efa 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -63,9 +63,6 @@ export class ShellExecution implements Execution { } } - owner(): Player { - return null; - } isActive(): boolean { return this.active; } diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 2f18e3b9e..2c85a0d3c 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -151,10 +151,6 @@ export class TradeShipExecution implements Execution { return; } - owner(): Player { - return null; - } - isActive(): boolean { return this.active; } diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 36fe7a8a9..ddaf65c02 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -11,7 +11,6 @@ import { import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { PseudoRandom } from "../PseudoRandom"; -import { distSort, distSortUnit } from "../Util"; import { consolex } from "../Consolex"; import { TileRef } from "../game/GameMap"; import { ShellExecution } from "./ShellExecution"; @@ -151,7 +150,7 @@ export class WarshipExecution implements Execution { !this.alreadySentShell.has(unit) && (unit.type() !== UnitType.TradeShip || hasPort) && (unit.type() !== UnitType.TradeShip || - unit.dstPort()?.owner() !== this.owner()), + unit.dstPort()?.owner() !== this._owner), ); this.target = @@ -229,7 +228,7 @@ export class WarshipExecution implements Execution { ); switch (result.type) { case PathFindResultType.Completed: - this.owner().captureUnit(this.target); + this._owner.captureUnit(this.target); this.target = null; return; case PathFindResultType.NextTile: @@ -244,10 +243,6 @@ export class WarshipExecution implements Execution { } } - owner(): Player { - return this._owner; - } - isActive(): boolean { return this.active; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 215dc8f8b..496cc3496 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -156,7 +156,6 @@ export interface Execution { activeDuringSpawnPhase(): boolean; init(mg: Game, ticks: number): void; tick(ticks: number): void; - owner(): Player; } export interface Attack { @@ -375,7 +374,6 @@ export interface Player { executeRetreat(attackID: string): void; // Misc - executions(): Execution[]; toUpdate(): PlayerUpdate; playerProfile(): PlayerProfile; canBoat(tile: TileRef): boolean; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index a76c64490..1501b5e1b 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -285,11 +285,6 @@ export class PlayerImpl implements Player { isAlive(): boolean { return this._tiles.size > 0; } - executions(): Execution[] { - return this.mg - .executions() - .filter((exec) => exec.owner().id() == this.id()); - } incomingAllianceRequests(): AllianceRequest[] { return this.mg.allianceRequests.filter((ar) => ar.recipient() == this); From f2193edc7cd012e8db4bc7c07a9860cbc1329ce1 Mon Sep 17 00:00:00 2001 From: Ilan Schemoul Date: Fri, 28 Mar 2025 00:09:58 +0100 Subject: [PATCH 2/8] priortize ally ports and close ports (#335) 2x more likely to trade with ports belonging to an ally OR close. 3x more likely to trade with ports belonging to an ally AND close. Add trading tests --- src/core/configuration/Config.ts | 4 +++ src/core/configuration/DefaultConfig.ts | 8 +++++ src/core/execution/PortExecution.ts | 22 ++++++------- src/core/game/Game.ts | 1 + src/core/game/PlayerImpl.ts | 34 ++++++++++++++++++++ tests/Warship.test.ts | 40 +++++++++++++++++------- tests/testdata/half_land_half_ocean.png | Bin 96 -> 108 bytes tests/util/TestConfig.ts | 15 +++++++++ 8 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 1f0b50bc8..1f12f454b 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -85,6 +85,10 @@ export interface Config { tilesPerTickUsed: number; }; attackAmount(attacker: Player, defender: Player | TerraNullius): number; + radiusPortSpawn(): number; + // When computing likelihood of trading for any given port, the X closest port + // are twice more likely to be selected. X is determined below. + proximityBonusPortsNb(totalPorts: number): number; maxPopulation(player: Player | PlayerView): number; cityPopulationIncrease(): number; boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 56d6c4bd5..e8dd7cdfd 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -474,6 +474,14 @@ export class DefaultConfig implements Config { return Math.floor(attacker.troops() / 5); } + radiusPortSpawn() { + return 20; + } + + proximityBonusPortsNb(totalPorts: number) { + return within(totalPorts / 3, 4, totalPorts); + } + attackAmount(attacker: Player, defender: Player | TerraNullius) { if (attacker.type() == PlayerType.Bot) { return attacker.troops() / 20; diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 547a05229..e6ff3962c 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -38,7 +38,6 @@ export class PortExecution implements Execution { tick(ticks: number): void { if (this.port == null) { - // TODO: use canBuild const tile = this.tile; const player = this.mg.player(this._owner); if (!player.canBuild(UnitType.Port, tile)) { @@ -46,12 +45,15 @@ export class PortExecution implements Execution { this.active = false; return; } - const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20))) - .filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) == player) - .sort( - (a, b) => - this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile), - ); + const spawns = Array.from( + this.mg.bfs( + tile, + manhattanDistFN(tile, this.mg.config().radiusPortSpawn()), + ), + ).sort( + (a, b) => + this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile), + ); if (spawns.length == 0) { consolex.warn(`cannot find spawn for port`); @@ -77,10 +79,8 @@ export class PortExecution implements Execution { return; } - const ports = this.mg - .players() - .filter((p) => p != this.port.owner() && p.canTrade(this.port.owner())) - .flatMap((p) => p.units(UnitType.Port)); + const ports = this.owner().tradingPorts(this.port); + if (ports.length == 0) { return; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 496cc3496..201ede85a 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -377,6 +377,7 @@ export interface Player { toUpdate(): PlayerUpdate; playerProfile(): PlayerProfile; canBoat(tile: TileRef): boolean; + tradingPorts(port: Unit): Unit[]; } export interface Game extends GameMap { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 1501b5e1b..b5f7edcd4 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -953,4 +953,38 @@ export class PlayerImpl implements Player { return false; } } + + // It's a probability list, so if an element appears twice it's because it's + // twice more likely to be picked later. + tradingPorts(port: Unit): Unit[] { + let ports = this.mg + .players() + .filter((p) => p != port.owner() && p.canTrade(port.owner())) + .flatMap((p) => p.units(UnitType.Port)) + .sort((p1, p2) => { + return ( + this.mg.manhattanDist(port.tile(), p1.tile()) - + this.mg.manhattanDist(port.tile(), p2.tile()) + ); + }); + + // Make close ports twice more likely by putting them again + for ( + let i = 0; + i < this.mg.config().proximityBonusPortsNb(ports.length); + i++ + ) { + ports.push(ports[i]); + } + + // Make ally ports twice more likely by putting them again + this.mg + .players() + .filter((p) => p != port.owner() && p.canTrade(port.owner())) + .filter((p) => p.isAlliedWith(port.owner())) + .flatMap((p) => p.units(UnitType.Port)) + .forEach((p) => ports.push(p)); + + return ports; + } } diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts index aa6d23b6e..b0ce7f98f 100644 --- a/tests/Warship.test.ts +++ b/tests/Warship.test.ts @@ -9,6 +9,7 @@ import { SpawnExecution } from "../src/core/execution/SpawnExecution"; import { setup } from "./util/Setup"; import { constructionExecution } from "./util/utils"; +const coastX = 7; let game: Game; let player1: Player; let player2: Player; @@ -36,10 +37,15 @@ describe("Warship", () => { ); game.addPlayer(player_2_info, 1000); - const spawnTile = game.map().ref(0, 0); game.addExecution( - new SpawnExecution(game.player(player_1_info.id).info(), spawnTile), - new SpawnExecution(game.player(player_2_info.id).info(), spawnTile), + new SpawnExecution( + game.player(player_1_info.id).info(), + game.ref(coastX, 10), + ), + new SpawnExecution( + game.player(player_2_info.id).info(), + game.ref(coastX, 15), + ), ); while (game.inSpawnPhase()) { @@ -53,8 +59,12 @@ describe("Warship", () => { test("Warship heals only if player has port", async () => { const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; - const port = player1.buildUnit(UnitType.Port, 0, game.ref(0, 0)); - const warship = player1.buildUnit(UnitType.Warship, 0, game.ref(7, 7)); + const port = player1.buildUnit(UnitType.Port, 0, game.ref(coastX, 10)); + const warship = player1.buildUnit( + UnitType.Warship, + 0, + game.ref(coastX + 1, 10), + ); game.executeNextTick(); @@ -71,15 +81,19 @@ describe("Warship", () => { }); test("Warship captures trade if player has port", async () => { - constructionExecution(game, player1.id(), 0, 0, UnitType.Port); - constructionExecution(game, player1.id(), 7, 7, UnitType.Warship); + constructionExecution(game, player1.id(), coastX, 10, UnitType.Port); + constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship); // Warship need one more tick (for warship exec to actually build warship) game.executeNextTick(); expect(player1.units(UnitType.Warship)).toHaveLength(1); // Cannot buildExec with trade ship as it's not buildable (but // we can obviously directly add it to the player) - const tradeShip = player2.buildUnit(UnitType.TradeShip, 0, game.ref(6, 6)); + const tradeShip = player2.buildUnit( + UnitType.TradeShip, + 0, + game.ref(coastX + 1, 7), + ); expect(tradeShip.owner().id()).toBe(player2.id()); // Let plenty of time for A* to execute @@ -90,14 +104,18 @@ describe("Warship", () => { }); test("Warship do not capture trade if player has no port", async () => { - constructionExecution(game, player1.id(), 0, 0, UnitType.Port); - constructionExecution(game, player1.id(), 7, 7, UnitType.Warship); + constructionExecution(game, player1.id(), coastX, 10, UnitType.Port); + constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship); expect(player1.units(UnitType.Warship)).toHaveLength(1); player1.units(UnitType.Port)[0].delete(); // Cannot buildExec with trade ship as it's not buildable (but // we can obviously directly add it to the player) - const tradeShip = player2.buildUnit(UnitType.TradeShip, 0, game.ref(6, 6)); + const tradeShip = player2.buildUnit( + UnitType.TradeShip, + 0, + game.ref(coastX + 1, 11), + ); expect(tradeShip.owner().id()).toBe(player2.id()); // Let plenty of time for A* to execute diff --git a/tests/testdata/half_land_half_ocean.png b/tests/testdata/half_land_half_ocean.png index 059596dfa08e46fbd24289dd21041a8a9133993a..67f40bfaa894ab96904273b841c5948eb89b176a 100755 GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|R6JcALoEE0 zP5z$wKA(}_;_UBE!TC0iS1|G{PCQ^B#l~}3iHAX=$U*R%^PX0qdInEdKbLh*2~7+P E0EX)wh5!Hn literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|Bt2amLo|Yu rOMadIFrSh0O># Date: Fri, 28 Mar 2025 00:39:37 +0100 Subject: [PATCH 3/8] embargo if attacked (#353) ## Responsibility Acknowledgment - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through basic manual testing: - Significantly slows down the entire development process - Blocks releases and new features for all contributors - Harms the project's reliability and reputation - Places additional debugging burden on maintainers who already have limited time - Diverts valuable resources away from new development and improvements - Makes me seen as a loser - Makes me more likely to go to hell --- src/core/execution/AttackExecution.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 198bc176e..08dc212d2 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -81,6 +81,10 @@ export class AttackExecution implements Execution { ? mg.terraNullius() : mg.player(this._targetID); + if (this.target && this.target.isPlayer()) { + (this.target as Player).addEmbargo(this._owner.id()); + } + if (this._owner == this.target) { console.error(`Player ${this._owner} cannot attack itself`); this.active = false; From 1551f7b6460c466da61f92483c1f166f2ea4c0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guzm=C3=A1n?= Date: Fri, 28 Mar 2025 00:42:53 +0100 Subject: [PATCH 4/8] Add Spanish translation (#356) Basic Spanish translation. --- resources/lang/es.json | 165 +++++++++++++++++++++++++++++++++++++++++ src/client/index.html | 1 + 2 files changed, 166 insertions(+) create mode 100644 resources/lang/es.json diff --git a/resources/lang/es.json b/resources/lang/es.json new file mode 100644 index 000000000..f873ab24b --- /dev/null +++ b/resources/lang/es.json @@ -0,0 +1,165 @@ +{ + "lang": { + "en": "Spanish", + "native": "Español", + "svg": "es" + }, + "main": { + "join_discord": "¡Únete al Discord!", + "create_lobby": "Crear Partida Privada", + "join_lobby": "Unirse a una Partida Privada", + "single_player": "Un jugador", + "instructions": "Instrucciones", + "how_to_play": "Cómo jugar", + "wiki": "Wiki" + }, + "help_modal": { + "hotkeys": "Teclas rápidas", + "table_key": "Tecla", + "table_action": "Accción", + "action_alt_view": "Vista alternativa (terreno/países)", + "action_attack_altclick": "Atacar (cuando el clic izquierdo abre el menú)", + "action_build": "Abrir el menú de construcción", + "action_center": "Centrar la cámara en el jugador", + "action_zoom": "Aumentar/Disminuir zoom", + "action_move_camera": "Mover la cámara", + "action_ratio_change": "Aumentar/Disminuir el ratio de ataque", + "action_reset_gfx": "Resetear los gráficos", + "ui_section": "Interfaz del Juego", + "ui_leaderboard": "Clasificación", + "ui_leaderboard_desc": "Muestra los mejores jugadores de la partida junto a sus nombres, % de propiedad y oro.", + "ui_control": "Panel de control", + "ui_control_desc": "El panel de control contiene los siguientes elementos:", + "ui_pop": "Población - La cantidad de unidades que posees, tu población máxima y la velocidad a la que esta aumenta.", + "ui_gold": "Oro - La cantidad de oro que tienes y la velocidad a la que lo obtienes.", + "ui_troops_workers": "Tropas y Trabajadores - La cantidad de tropas y trabajadores asignados. Las tropas se usan para atacar o defender ataques enemigos. Los trabajadores se usan para generar oro. Puedes ajustar el número de tropas y trabajadores con la barra deslizante.", + "ui_attack_ratio": "Ratio de ataque - La cantidad de tropas que se usarán cuando ataques. Puedes ajustar la cantidad usando la barra deslizante.", + "ui_options": "Opciones", + "ui_options_desc": "Los siguientes elementos se encuentran aquí:", + "option_pause": "Pausar/Reanudar la partida - Solo disponible en el modo de un jugador.", + "option_timer": "Temporizador - Tiempo transcurrido desde el inicio de la partida.", + "option_exit": "Botón de salir.", + "option_settings": "Ajustes - Abrir el menú de ajustes. Dentro de él se puede activar la Vista Alternativa, el Modo Oscuro, los Emojis y la Accción del clic izquierdo.", + "radial_title": "Menú radial", + "radial_desc": "Clic derecho (o tocar en móviles) abre el menú radial. Desde él puedes:", + "radial_build": "Abrir el menú de construcción.", + "radial_info": "Abrir el menú de información.", + "radial_boat": "Mandar un barco a la ubicación seleccionada (sólo está disponible si tienes acceso a agua).", + "radial_close": "Cerrar el menú.", + "info_title": "Menú de información", + "info_enemy_panel": "Panel de información de enemigo", + "info_enemy_desc": "Contiene infomación como el nombre del jugador seleccionado, su oro, tropas, así como si éste es un traidor. Un traidor es un jugador que ha roto una alianza con otro jugador y lo ha atacado. Los iconos representan las siguientes interacciones:", + "info_target": "Pones una diana en el jugador seleccionado, marcándolo para todos los aliados. Usado para coordinar ataques.", + "info_alliance": "Enviar una solicitud de alianza al jugador seleccionado. Los aliados pueden compartir recursos y tropas, pero no se pueden atacar entre ellos.", + "info_emoji": "Enviar un emoji al jugador seleccionado.", + "info_ally_panel": "Panel de información de aliado", + "info_ally_desc": "Cuando tienes una alianza con otro jugador, las siguientes interacciones estarán disponibles:", + "ally_betray": "Traicionar a tu aliado, finalizando la alianza. A partir de ese momento tendrás un símbolo de traior junto a tu nombre. Será menos probable que los bots se alíen contigo y el resto de jugadores se lo pensará dos veces antes de hacerlo.", + "ally_donate": "Donar parte de tus tropas al jugador aliado. Se puede usar cuando disponen de pocas tropas y están siendo atacados, o cuando necesitan más fuerza para vencer a un enemigo", + "build_menu_title": "Menú de construcción", + "build_name": "Nombre", + "build_icon": "Icono", + "build_desc": "Descripción", + "build_city": "Ciudad", + "build_city_desc": "Aumenta la población máxima. Es útil cuando no puedes expandir tu territorio o estás cerca de alcanzar el límite máximo de población.", + "build_defense": "Puesto de defensa", + "build_defense_desc": "Aumenta la defensa en las fronteras cercanas. Los ataques enemigos serán más lentos y tendrán más bajas.", + "build_port": "Puerto", + "build_port_desc": "Envía barcos mercantes automáticamente entre tu país y otros países (a menos que hayas seleccionado \"detener el comercio\" en otro jugador, o éste lo haya seleccionado en ti), proporcionando oro a ambos bandos. Permite construir barcos de guerra. Sólo se puede construir junto a agua.", + "build_warship": "Barco de guerra", + "build_warship_desc": "Patrulla un area, capturando barcos mercantes y destruyendo Barcos de guerra y Botes enemigos. Aparece desde el puerto más cercano y patrulla el área en el que se clicó al construírlo.", + "build_silo": "Silo de misiles", + "build_silo_desc": "Permite lanzar misiles.", + "build_sam": "Misil tierra-aire", + "build_sam_desc": "Tiene un 75% de probabilidad de interceptar misiles enemigos en un radio de 100 píxeles. Tiene un tiempo de recarga de 7.5 segundos y no puede interceptar MIRVs.", + "build_atom": "Bomba atómica", + "build_atom_desc": "Una bomba pequeña que destruye territorio, construcciones, barcos y botes. Aparece desde el Silo más cercano e impacta en el punto en que se clicó al construirla.", + "build_hydrogen": "Bomba de hidrógeno", + "build_hydrogen_desc": "Una bomba grande. Aparece desde el Silo más cercano e impacta en el punto en que se clicó al construirla.", + "build_mirv": "MIRV", + "build_mirv_desc": "La bomba más poderosa del juego. Se divide en bombas menores que cubren una gran cantidad de territorio. Solo daña al jugador sobre el que se clicó al construirla. Aparece desde el Silo más cercano e impacta en el punto en que se clicó al construirla.", + "player_icons": "Iconos de jugadores", + "icon_desc": "Ejemplos de los iconos que te encontrarás y lo que significan:", + "icon_crown": "Corona - Líder. Este jugador es el número 1 de la partida.", + "icon_traitor": "Espadas cruzadas - Traidor. Este jugador ha traicionado y atacado a un aliado.", + "icon_ally": "Apretón de manos - Aliado. Este jugador es tu aliado." + }, + "single_modal": { + "title": "Un jugador", + "map": "Mapa", + "difficulty": "Dificultad", + "allow_alliances": "Permitir alianzas", + "options_title": "Opciones", + "bots": "Bots: ", + "bots_disabled": "Deshabilitados", + "disable_nations": "Deshabilitar Naciones", + "instant_build": "Construcción instantánea", + "infinite_gold": "Oro infinito", + "infinite_troops": "Tropas infinitas", + "disable_nukes": "Deshabilitar bombas", + "start": "Iniciar Partida" + }, + "map": { + "world": "Mundo", + "europe": "Europa", + "mena": "Oriente Medio y Norte de África", + "northamerica": "Norteamérica", + "oceania": "Oceanía", + "blacksea": "Mar negro", + "africa": "África", + "asia": "Asia", + "mars": "Marte", + "southamerica": "Sudamérica", + "britannia": "Britania", + "gatewaytotheatlantic": "Salida al Atlántico", + "australia": "Australia", + "random": "Aleatorio", + "iceland": "Islandia", + "pangaea": "Pangea" + }, + "private_lobby": { + "title": "Unirse a Partida Privada", + "enter_id": "Introduce ID de la Partida", + "player": "Jugador", + "players": "Jugadores", + "join_lobby": "Unirse a Partida", + "checking": "Comprobando Partida...", + "not_found": "No se ha encontrado Partida. Comprueba el ID e inténtalo de nuevo.", + "error": "Ha ocurrido un error. Inténtalo de nuevo.", + "joined_waiting": "¡Te has unico a la Partida! Esperando a que comience el juego..." + }, + "public_lobby": { + "join": "Unirse a la próxima Partida", + "waiting": "jugadores en espera" + }, + "username": { + "enter_username": "Introduce tu nombre de usuario", + "not_string": "El nombre de usuario debe ser una cadena de texto.", + "too_short": "El nombre de usuario tiene que tener al menos {min} carácteres.", + "too_long": "El nombre de usuario no puede superar los {max} carácteres.", + "invalid_chars": "El nombre de usuario sólo puede contener letras, números, espacios, barras bajas y [corchetes]." + }, + "host_modal": { + "title": "Partida Privada", + "map": "Mapa", + "difficulty": "Difficultad", + "options_title": "Opciones", + "bots": "Bots: ", + "bots_disabled": "Deshabilitados", + "disable_nations": "Deshabilitar Naciones", + "instant_build": "Construcción instantánea", + "infinite_gold": "Oro infinito", + "infinite_troops": "Tropas infinitas", + "disable_nukes": "Deshabilitar bombas", + "player": "Jugador", + "players": "Jugadores", + "waiting": "jugadores en espera...", + "start": "Iniciar Partida" + }, + "difficulty": { + "Relaxed": "Relajadoa", + "Balanced": "Balanceada", + "Intense": "Intensa", + "Impossible": "Imposible" + } +} diff --git a/src/client/index.html b/src/client/index.html index 89b369caa..8a03739ad 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -270,6 +270,7 @@ + From 60be8b236ade273c8d3163b03a3d5fabcf7deccf Mon Sep 17 00:00:00 2001 From: Ilan Schemoul Date: Fri, 28 Mar 2025 01:34:30 +0100 Subject: [PATCH 5/8] fix compilation (#359) ## Please complete the following: just a compilation bug fix. npm run dev is how i test ## Please put your Discord username so you can be contacted if a bug or regression is found: respectful pinguin --- src/core/execution/PortExecution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index e6ff3962c..bc5674ff6 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -79,7 +79,7 @@ export class PortExecution implements Execution { return; } - const ports = this.owner().tradingPorts(this.port); + const ports = this.player().tradingPorts(this.port); if (ports.length == 0) { return; From 099dc74a3683a1c350fe3f0230b45d8b36bf156b Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 27 Mar 2025 17:52:10 -0700 Subject: [PATCH 6/8] fix webpack: always copy over static contents (#360) The problem with the file check, was it only checked if a file was missing and not if it changed. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- webpack.config.js | 87 ++++++----------------------------------------- 1 file changed, 10 insertions(+), 77 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 6a71ba148..3f5a0fc3a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,5 @@ import path from "path"; import { fileURLToPath } from "url"; -import fs from "fs/promises"; import HtmlWebpackPlugin from "html-webpack-plugin"; import webpack from "webpack"; import CopyPlugin from "copy-webpack-plugin"; @@ -8,40 +7,6 @@ import CopyPlugin from "copy-webpack-plugin"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -async function checkResourcesCopied(sourceDir, targetDir) { - async function checkDir(source, target) { - let items; - try { - items = await fs.readdir(source, { withFileTypes: true }); - } catch (error) { - console.error(`Error reading directory ${source}:`, error); - return false; - } - for (const item of items) { - const sourcePath = path.join(source, item.name); - const targetPath = path.join(target, item.name); - if (item.isDirectory()) { - try { - await fs.access(targetPath); - } catch (error) { - // Target directory does not exist. - return false; - } - const exists = await checkDir(sourcePath, targetPath); - if (!exists) return false; - } else if (item.isFile()) { - try { - await fs.access(targetPath); - } catch (error) { - // Target file does not exist. - return false; - } - } - } - return true; - } - return checkDir(sourceDir, targetDir); -} export default async (env, argv) => { const isProduction = argv.mode === "production"; @@ -154,48 +119,16 @@ export default async (env, argv) => { new webpack.DefinePlugin({ "process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"), }), - ...(await (async () => { - if (isProduction) { - return [ - new CopyPlugin({ - patterns: [ - { - from: "resources", - to: ".", - noErrorOnMissing: true, - }, - ], - options: { concurrency: 100 }, - }), - ]; - } else { - const resourcesDir = path.resolve(__dirname, "resources"); - const targetDir = path.resolve(__dirname, "static"); - const allExist = await checkResourcesCopied(resourcesDir, targetDir); - if (allExist) { - console.log( - "[CopyPlugin] Skipped: All resources already exist in static/.", - ); - return []; // Skip CopyPlugin if all resources are present. - } else { - console.log( - "[CopyPlugin] Copying missing resources to static/ ...", - ); - return [ - new CopyPlugin({ - patterns: [ - { - from: "resources", - to: ".", - noErrorOnMissing: true, - }, - ], - options: { concurrency: 100 }, - }), - ]; - } - } - })()), + new CopyPlugin({ + patterns: [ + { + from: path.resolve(__dirname, "resources"), + to: path.resolve(__dirname, "static"), + noErrorOnMissing: true, + }, + ], + options: { concurrency: 100 }, + }), ], optimization: { // Add optimization configuration for better caching From 9ed1fe865c3c10a1cb2dd0ea20b7d1859fe4326f Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 27 Mar 2025 19:08:46 -0700 Subject: [PATCH 7/8] bugfix: ports not spawning on coastline --- src/core/execution/PortExecution.ts | 20 +++----------------- src/core/game/PlayerImpl.ts | 7 ++++++- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index bc5674ff6..a2c226936 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -40,27 +40,13 @@ export class PortExecution implements Execution { if (this.port == null) { const tile = this.tile; const player = this.mg.player(this._owner); - if (!player.canBuild(UnitType.Port, tile)) { + const spawn = player.canBuild(UnitType.Port, tile); + if (spawn === false) { consolex.warn(`player ${player} cannot build port at ${this.tile}`); this.active = false; return; } - const spawns = Array.from( - this.mg.bfs( - tile, - manhattanDistFN(tile, this.mg.config().radiusPortSpawn()), - ), - ).sort( - (a, b) => - this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile), - ); - - if (spawns.length == 0) { - consolex.warn(`cannot find spawn for port`); - this.active = false; - return; - } - this.port = player.buildUnit(UnitType.Port, 0, spawns[0]); + this.port = player.buildUnit(UnitType.Port, 0, spawn); } if (!this.port.isActive()) { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index b5f7edcd4..56cd55fce 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -730,7 +730,12 @@ export class PlayerImpl implements Player { } portSpawn(tile: TileRef): TileRef | false { - const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20))) + const spawns = Array.from( + this.mg.bfs( + tile, + manhattanDistFN(tile, this.mg.config().radiusPortSpawn()), + ), + ) .filter((t) => this.mg.owner(t) == this && this.mg.isOceanShore(t)) .sort( (a, b) => From d8fe41de7a980e54392bb2cb5e128557fe4815c2 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 27 Mar 2025 20:43:56 -0700 Subject: [PATCH 8/8] teams (#349) --- resources/lang/en.json | 5 + src/client/ClientGameRunner.ts | 20 +- src/client/HostLobbyModal.ts | 136 ++++--- src/client/LocalServer.ts | 8 +- src/client/SinglePlayerModal.ts | 37 +- src/client/Transport.ts | 5 +- src/client/graphics/layers/EventsDisplay.ts | 2 +- src/client/graphics/layers/NameLayer.ts | 6 +- .../graphics/layers/PlayerInfoOverlay.ts | 6 +- src/client/graphics/layers/StructureLayer.ts | 6 +- src/client/graphics/layers/TerritoryLayer.ts | 6 +- src/client/graphics/layers/UILayer.ts | 2 +- src/client/graphics/layers/UnitLayer.ts | 28 +- src/client/graphics/layers/WinModal.ts | 42 ++- src/core/Schemas.ts | 12 +- src/core/Util.ts | 6 +- src/core/configuration/Colors.ts | 352 ++++++++++++++++++ src/core/configuration/Config.ts | 10 +- src/core/configuration/PastelTheme.ts | 250 +------------ src/core/configuration/PastelThemeDark.ts | 24 +- src/core/execution/BotExecution.ts | 4 +- src/core/execution/FakeHumanExecution.ts | 13 +- src/core/execution/PlayerExecution.ts | 2 +- src/core/execution/SAMLauncherExecution.ts | 3 +- src/core/execution/TransportShipExecution.ts | 2 +- src/core/execution/WarshipExecution.ts | 2 +- src/core/execution/WinCheckExecution.ts | 34 +- .../alliance/AllianceRequestExecution.ts | 2 +- .../alliance/AllianceRequestReplyExecution.ts | 2 +- src/core/game/Game.ts | 25 +- src/core/game/GameImpl.ts | 27 +- src/core/game/GameUpdates.ts | 6 +- src/core/game/GameView.ts | 14 + src/core/game/PlayerImpl.ts | 40 +- src/server/GameManager.ts | 3 +- src/server/GameServer.ts | 11 +- src/server/Worker.ts | 1 + 37 files changed, 767 insertions(+), 387 deletions(-) create mode 100644 src/core/configuration/Colors.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index a37127485..16b9c5c7e 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -138,6 +138,7 @@ "title": "Private Lobby", "map": "Map", "difficulty": "Difficulty", + "mode": "Mode", "options_title": "Options", "bots": "Bots: ", "bots_disabled": "Disabled", @@ -156,5 +157,9 @@ "Balanced": "Balanced", "Intense": "Intense", "Impossible": "Impossible" + }, + "game_mode": { + "ffa": "Free for All", + "teams": "Teams" } } diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index b281d3965..3fff5b342 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -1,4 +1,10 @@ -import { PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game"; +import { + PlayerID, + GameMapType, + Difficulty, + GameType, + TeamName, +} from "../core/game/Game"; import { EventBus } from "../core/EventBus"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; import { InputHandler, MouseUpEvent } from "./InputHandler"; @@ -165,6 +171,15 @@ export class ClientGameRunner { clientID: this.lobby.clientID, }, ]; + let winner: ClientID | TeamName | null = null; + if (update.winnerType == "player") { + winner = this.gameView + .playerBySmallID(update.winner as number) + .clientID(); + } else { + winner = update.winner as TeamName; + } + const record = createGameRecord( this.lobby.gameID, this.lobby.gameConfig, @@ -173,7 +188,8 @@ export class ClientGameRunner { [], LocalPersistantStats.startTime(), Date.now(), - this.gameView.playerBySmallID(update.winnerID).id(), + winner, + update.winnerType, update.allPlayersStats, ); LocalPersistantStats.endGame(record); diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 2b55c8cf0..cf8e7ddb0 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -1,6 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, query, property, state } from "lit/decorators.js"; -import { Difficulty, GameMapType, GameType } from "../core/game/Game"; +import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; import { GameConfig, GameInfo } from "../core/Schemas"; import { consolex } from "../core/Consolex"; import "./components/Difficulties"; @@ -22,6 +22,7 @@ export class HostLobbyModal extends LitElement { @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs = false; + @state() private gameMode: GameMode = GameMode.FFA; @state() private disableNukes: boolean = false; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @@ -135,31 +136,54 @@ export class HostLobbyModal extends LitElement { - -
-
- ${translateText("host_modal.options_title")} + +
+
${translateText("host_modal.mode")}
+
+
this.handleGameModeSelection(GameMode.FFA)} + > +
+ ${translateText("game_mode.ffa")} +
-
- +
this.handleGameModeSelection(GameMode.Team)} + > +
+ ${translateText("game_mode.teams")} +
+
+
+
+ + +
+
+ ${translateText("host_modal.options_title")} +
+
+
- -
-
- ${this.players.length} - ${ - this.players.length === 1 - ? translateText("host_modal.player") - : translateText("host_modal.players") - } -
- -
- ${this.players.map( - (player) => html`${player}`, - )} -
-
- - +
+ +
+ ${this.players.map( + (player) => html`${player}`, + )} +
- + + +
+ `; } @@ -380,6 +404,11 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } + private async handleGameModeSelection(value: GameMode) { + this.gameMode = value; + this.putGameConfig(); + } + private async putGameConfig() { const config = await getServerConfigFromClient(); const response = await fetch( @@ -398,6 +427,7 @@ export class HostLobbyModal extends LitElement { infiniteGold: this.infiniteGold, infiniteTroops: this.infiniteTroops, instantBuild: this.instantBuild, + gameMode: this.gameMode, } as GameConfig), }, ); @@ -456,7 +486,7 @@ export class HostLobbyModal extends LitElement { }) .then((response) => response.json()) .then((data: GameInfo) => { - console.log(`got response: ${data}`); + console.log(`got game info response: ${JSON.stringify(data)}`); this.players = data.clients.map((p) => p.username); }); } diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 0a6885543..e2ad73e6b 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -6,6 +6,7 @@ import { ClientID, ClientMessage, ClientMessageSchema, + ClientSendWinnerMessage, GameConfig, GameID, GameRecordSchema, @@ -33,7 +34,7 @@ export class LocalServer { private paused = false; - private winner: ClientID | null = null; + private winner: ClientSendWinnerMessage = null; private allPlayersStats: AllPlayersStats = {}; constructor( @@ -124,7 +125,7 @@ export class LocalServer { } } if (clientMsg.type == "winner") { - this.winner = clientMsg.winner; + this.winner = clientMsg; this.allPlayersStats = clientMsg.allPlayersStats; } } @@ -164,7 +165,8 @@ export class LocalServer { this.turns, this.startedAt, Date.now(), - this.winner, + this.winner?.winner, + this.winner?.winnerType, this.allPlayersStats, ); if (!saveFullGame) { diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 0e304b3b6..e8fd90549 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -1,6 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; -import { Difficulty, GameMapType, GameType } from "../core/game/Game"; +import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; import { generateID as generateID } from "../core/Util"; import { consolex } from "../core/Consolex"; import "./components/Difficulties"; @@ -28,6 +28,7 @@ export class SinglePlayerModal extends LitElement { @state() private infiniteTroops: boolean = false; @state() private instantBuild: boolean = false; @state() private useRandomMap: boolean = false; + @state() private gameMode: GameMode = GameMode.FFA; render() { return html` @@ -107,6 +108,33 @@ export class SinglePlayerModal extends LitElement {
+ +
+
${translateText("host_modal.mode")}
+
+
this.handleGameModeSelection(GameMode.FFA)} + > +
+ ${translateText("game_mode.ffa")} +
+
+
this.handleGameModeSelection(GameMode.Team)} + > +
+ ${translateText("game_mode.teams")} +
+
+
+
+
@@ -122,7 +150,7 @@ export class SinglePlayerModal extends LitElement { step="1" @input=${this.handleBotsChange} @change=${this.handleBotsChange} - .value="${this.bots}" + .value="${String(this.bots)}" />
${translateText("single_modal.bots")}${this @@ -277,6 +305,10 @@ export class SinglePlayerModal extends LitElement { this.disableNukes = Boolean((e.target as HTMLInputElement).checked); } + private handleGameModeSelection(value: GameMode) { + this.gameMode = value; + } + private getRandomMap(): GameMapType { const maps = Object.values(GameMapType); const randIdx = Math.floor(Math.random() * maps.length); @@ -300,6 +332,7 @@ export class SinglePlayerModal extends LitElement { gameConfig: { gameMap: this.selectedMap, gameType: GameType.Singleplayer, + gameMode: this.gameMode, difficulty: this.selectedDifficulty, disableNPCs: this.disableNPCs, disableNukes: this.disableNukes, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index aeb553593..e66b9d08a 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -9,6 +9,7 @@ import { Player, PlayerID, PlayerType, + TeamName, Tick, UnitType, } from "../core/game/Game"; @@ -124,8 +125,9 @@ export class SendSetTargetTroopRatioEvent implements GameEvent { export class SendWinnerEvent implements GameEvent { constructor( - public readonly winner: ClientID, + public readonly winner: ClientID | TeamName, public readonly allPlayersStats: AllPlayersStats, + public readonly winnerType: "player" | "team", ) {} } export class SendHashEvent implements GameEvent { @@ -492,6 +494,7 @@ export class Transport { gameID: this.lobbyConfig.gameID, winner: event.winner, allPlayersStats: event.allPlayersStats, + winnerType: event.winnerType, }); this.sendMsg(JSON.stringify(msg)); } else { diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index cc8c305a0..287375da9 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -304,7 +304,7 @@ export class EventsDisplay extends LitElement implements Layer { onTargetPlayerEvent(event: TargetPlayerUpdate) { const other = this.game.playerBySmallID(event.playerID) as PlayerView; const myPlayer = this.game.playerByClientID(this.clientID) as PlayerView; - if (!myPlayer || !myPlayer.isAlliedWith(other)) return; + if (!myPlayer || !myPlayer.isFriendly(other)) return; const target = this.game.playerBySmallID(event.targetID) as PlayerView; diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 2b4f56fc3..fc675671a 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -195,7 +195,7 @@ export class NameLayer implements Layer { nameDiv.appendChild(flagImg); } nameDiv.classList.add("player-name"); - nameDiv.style.color = this.theme.textColor(player.info()); + nameDiv.style.color = this.theme.textColor(player); nameDiv.style.fontFamily = this.theme.font(); nameDiv.style.whiteSpace = "nowrap"; nameDiv.style.textOverflow = "ellipsis"; @@ -213,7 +213,7 @@ export class NameLayer implements Layer { troopsDiv.classList.add("player-troops"); troopsDiv.setAttribute("translate", "no"); troopsDiv.textContent = renderTroops(player.troops()); - troopsDiv.style.color = this.theme.textColor(player.info()); + troopsDiv.style.color = this.theme.textColor(player); troopsDiv.style.fontFamily = this.theme.font(); troopsDiv.style.zIndex = "3"; troopsDiv.style.marginTop = "-5%"; @@ -242,7 +242,7 @@ export class NameLayer implements Layer { // Calculate base size and scale const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size)); render.fontSize = Math.max(4, Math.floor(baseSize * 0.4)); - render.fontColor = this.theme.textColor(render.player.info()); + render.fontColor = this.theme.textColor(render.player); // Screen space calculations const size = this.transformHandler.scale * baseSize; diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index e495d3d26..3b8678017 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -163,7 +163,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { private renderPlayerInfo(player: PlayerView) { const myPlayer = this.myPlayer(); - const isAlly = myPlayer?.isAlliedWith(player); + const isFriendly = myPlayer?.isFriendly(player); let relationHtml = null; const attackingTroops = player .outgoingAttacks() @@ -198,7 +198,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { return html`
@@ -244,7 +244,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { private renderUnitInfo(unit: UnitView) { const isAlly = (unit.owner() == this.myPlayer() || - this.myPlayer()?.isAlliedWith(unit.owner())) ?? + this.myPlayer()?.isFriendly(unit.owner())) ?? false; return html` diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 3bb61bba6..87190a303 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -188,7 +188,7 @@ export class StructureLayer implements Layer { new Cell(this.game.x(tile), this.game.y(tile)), unit.type() == UnitType.Construction ? underConstructionColor - : this.theme.territoryColor(unit.owner().info()), + : this.theme.territoryColor(unit.owner()), 130, ); } @@ -234,7 +234,7 @@ export class StructureLayer implements Layer { if (!unit.isActive()) return; - let borderColor = this.theme.borderColor(unit.owner().info()); + let borderColor = this.theme.borderColor(unit.owner()); if (unitType == UnitType.SAMLauncher && unit.isSamCooldown()) { borderColor = reloadingColor; } else if (unit.type() == UnitType.Construction) { @@ -257,7 +257,7 @@ export class StructureLayer implements Layer { height: number, unit: UnitView, ) { - let color = this.theme.borderColor(unit.owner().info()); + let color = this.theme.borderColor(unit.owner()); if (unit.type() == UnitType.Construction) { color = underConstructionColor; } diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index bd9be5383..b77693e51 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -251,14 +251,14 @@ export class TerritoryLayer implements Layer { this.paintCell( this.game.x(tile), this.game.y(tile), - this.theme.defendedBorderColor(owner.info()), + this.theme.defendedBorderColor(owner), 255, ); } else { this.paintCell( this.game.x(tile), this.game.y(tile), - this.theme.borderColor(owner.info()), + this.theme.borderColor(owner), 255, ); } @@ -266,7 +266,7 @@ export class TerritoryLayer implements Layer { this.paintCell( this.game.x(tile), this.game.y(tile), - this.theme.territoryColor(owner.info()), + this.theme.territoryColor(owner), 150, ); } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index b743f7687..15fdec017 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -136,7 +136,7 @@ export class UILayer implements Layer { baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount; // Get the unit's owner color for the box - const ownerColor = this.theme.territoryColor(unit.owner().info()); + const ownerColor = this.theme.territoryColor(unit.owner()); // Create a brighter version of the owner color for the selection const selectionColor = ownerColor.lighten(0.2); diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 0e06dba0a..e648947cf 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -194,7 +194,7 @@ export class UnitLayer implements Layer { if (this.myPlayer == unit.owner()) { return Relationship.Self; } - if (this.myPlayer.isAlliedWith(unit.owner())) { + if (this.myPlayer.isFriendly(unit.owner())) { return Relationship.Ally; } return Relationship.Enemy; @@ -248,7 +248,7 @@ export class UnitLayer implements Layer { return; } - let outerColor = this.theme.territoryColor(unit.owner().info()); + let outerColor = this.theme.territoryColor(unit.owner()); if (unit.warshipTargetId()) { const targetOwner = this.game .units() @@ -276,7 +276,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -290,7 +290,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.territoryColor(unit.owner().info()), + this.theme.territoryColor(unit.owner()), 255, ); } @@ -316,14 +316,14 @@ export class UnitLayer implements Layer { this.game.x(unit.tile()), this.game.y(unit.tile()), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); this.paintCell( this.game.x(unit.lastTile()), this.game.y(unit.lastTile()), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -358,7 +358,7 @@ export class UnitLayer implements Layer { this.game.x(unit.tile()), this.game.y(unit.tile()), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -408,7 +408,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -426,7 +426,7 @@ export class UnitLayer implements Layer { this.game.x(unit.tile()), this.game.y(unit.tile()), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -453,7 +453,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.territoryColor(unit.owner().info()), + this.theme.territoryColor(unit.owner()), 255, ); } @@ -467,7 +467,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -498,7 +498,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.territoryColor(unit.owner().info()), + this.theme.territoryColor(unit.owner()), 150, ); } @@ -512,7 +512,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.borderColor(unit.owner().info()), + this.theme.borderColor(unit.owner()), 255, ); } @@ -526,7 +526,7 @@ export class UnitLayer implements Layer { this.game.x(t), this.game.y(t), rel, - this.theme.territoryColor(unit.owner().info()), + this.theme.territoryColor(unit.owner()), 255, ); } diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 40e0cc7d9..067b18983 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -1,6 +1,6 @@ import { LitElement, html, css } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { Player } from "../../../core/game/Game"; +import { Player, TeamName } from "../../../core/game/Game"; import { ClientID } from "../../../core/Schemas"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; @@ -218,18 +218,38 @@ export class WinModal extends LitElement implements Layer { this.show(); } this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => { - const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView; - this.eventBus.emit( - new SendWinnerEvent(winner.clientID(), wu.allPlayersStats), - ); - if (winner == this.game.myPlayer()) { - this._title = "You Won!"; - this.won = true; + if (wu.winnerType === "team") { + this.eventBus.emit( + new SendWinnerEvent( + wu.winner as TeamName, + wu.allPlayersStats, + "team", + ), + ); + if (wu.winner == this.game.myPlayer()?.teamName()) { + this._title = "Your team won!"; + this.won = true; + } else { + this._title = `${wu.winner} team has won!`; + this.won = false; + } + this.show(); } else { - this._title = `${winner.name()} has won!`; - this.won = false; + const winner = this.game.playerBySmallID( + wu.winner as number, + ) as PlayerView; + this.eventBus.emit( + new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"), + ); + if (winner == this.game.myPlayer()) { + this._title = "You Won!"; + this.won = true; + } else { + this._title = `${winner.name()} has won!`; + this.won = false; + } + this.show(); } - this.show(); }); } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index e430d63b2..fc5421d61 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -3,8 +3,10 @@ import { AllPlayers, Difficulty, GameMapType, + GameMode, GameType, PlayerType, + TeamName, UnitType, } from "./game/Game"; @@ -107,6 +109,7 @@ const GameConfigSchema = z.object({ gameMap: z.nativeEnum(GameMapType), difficulty: z.nativeEnum(Difficulty), gameType: z.nativeEnum(GameType), + gameMode: z.nativeEnum(GameMode), disableNPCs: z.boolean(), disableNukes: z.boolean(), bots: z.number().int().min(0).max(400), @@ -345,8 +348,9 @@ const ClientBaseMessageSchema = z.object({ export const ClientSendWinnerSchema = ClientBaseMessageSchema.extend({ type: z.literal("winner"), - winner: ID.nullable(), + winner: ID.or(z.nativeEnum(TeamName)).nullable(), allPlayersStats: AllPlayersStatsSchema, + winnerType: z.enum(["player", "team"]), }); export const ClientHashSchema = ClientBaseMessageSchema.extend({ @@ -404,7 +408,11 @@ export const GameRecordSchema = z.object({ date: SafeString, num_turns: z.number(), turns: z.array(TurnSchema), - winner: ID.nullable(), + winner: z + .union([ID, z.nativeEnum(TeamName)]) + .nullable() + .optional(), + winnerType: z.enum(["player", "team"]).nullable().optional(), allPlayersStats: z.record(ID, PlayerStatsSchema), version: z.enum(["v0.0.1"]), gitCommit: z.string().nullable().optional(), diff --git a/src/core/Util.ts b/src/core/Util.ts index 438da4988..c86c52439 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from "uuid"; import twemoji from "twemoji"; import DOMPurify from "dompurify"; -import { Cell, Game, Player, Unit } from "./game/Game"; +import { Cell, Game, Player, TeamName, Unit } from "./game/Game"; import { AllPlayersStats, ClientID, @@ -255,7 +255,8 @@ export function createGameRecord( turns: Turn[], start: number, end: number, - winner: ClientID | null, + winner: ClientID | TeamName | null, + winnerType: "player" | "team" | null, allPlayersStats: AllPlayersStats, ): GameRecord { const record: GameRecord = { @@ -289,6 +290,7 @@ export function createGameRecord( ); record.num_turns = turns.length; record.winner = winner; + record.winnerType = winnerType; return record; } diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts new file mode 100644 index 000000000..6354359f0 --- /dev/null +++ b/src/core/configuration/Colors.ts @@ -0,0 +1,352 @@ +import { colord, Colord } from "colord"; + +export const territoryColors: Colord[] = [ + colord({ r: 230, g: 100, b: 100 }), // Bright Red + colord({ r: 100, g: 180, b: 230 }), // Sky Blue + colord({ r: 230, g: 180, b: 80 }), // Golden Yellow + colord({ r: 180, g: 100, b: 230 }), // Purple + colord({ r: 80, g: 200, b: 120 }), // Emerald Green + colord({ r: 230, g: 130, b: 180 }), // Pink + colord({ r: 100, g: 160, b: 80 }), // Olive Green + colord({ r: 230, g: 150, b: 100 }), // Peach + colord({ r: 80, g: 130, b: 190 }), // Navy Blue + colord({ r: 210, g: 210, b: 100 }), // Lime Yellow + colord({ r: 190, g: 100, b: 130 }), // Maroon + colord({ r: 100, g: 210, b: 210 }), // Turquoise + colord({ r: 210, g: 140, b: 80 }), // Light Orange + colord({ r: 150, g: 110, b: 190 }), // Lavender + colord({ r: 180, g: 210, b: 120 }), // Light Green + colord({ r: 210, g: 100, b: 160 }), // Hot Pink + colord({ r: 100, g: 140, b: 110 }), // Sea Green + colord({ r: 230, g: 180, b: 180 }), // Light Pink + colord({ r: 120, g: 120, b: 190 }), // Periwinkle + colord({ r: 190, g: 170, b: 100 }), // Sand + colord({ r: 100, g: 180, b: 160 }), // Aquamarine + colord({ r: 210, g: 160, b: 200 }), // Orchid + colord({ r: 170, g: 190, b: 100 }), // Yellow Green + colord({ r: 100, g: 130, b: 150 }), // Steel Blue + colord({ r: 230, g: 140, b: 140 }), // Salmon + colord({ r: 140, g: 180, b: 220 }), // Light Blue + colord({ r: 200, g: 160, b: 110 }), // Tan + colord({ r: 180, g: 130, b: 180 }), // Plum + colord({ r: 130, g: 200, b: 130 }), // Light Sea Green + colord({ r: 220, g: 120, b: 120 }), // Coral + colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue + colord({ r: 200, g: 200, b: 140 }), // Khaki + colord({ r: 160, g: 120, b: 160 }), // Purple Gray + colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green + colord({ r: 200, g: 130, b: 110 }), // Dark Salmon + colord({ r: 130, g: 170, b: 190 }), // Cadet Blue + colord({ r: 190, g: 180, b: 160 }), // Tan Gray + colord({ r: 170, g: 140, b: 190 }), // Medium Purple + colord({ r: 160, g: 190, b: 160 }), // Pale Green + colord({ r: 190, g: 150, b: 130 }), // Rosy Brown + colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray + colord({ r: 180, g: 170, b: 140 }), // Dark Khaki + colord({ r: 150, g: 130, b: 150 }), // Thistle + colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green + colord({ r: 190, g: 140, b: 150 }), // Puce + colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine + colord({ r: 180, g: 160, b: 180 }), // Mauve + colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green + colord({ r: 170, g: 150, b: 170 }), // Dusty Rose + colord({ r: 100, g: 180, b: 230 }), // Sky Blue + colord({ r: 230, g: 180, b: 80 }), // Golden Yellow + colord({ r: 180, g: 100, b: 230 }), // Purple + colord({ r: 80, g: 200, b: 120 }), // Emerald Green + colord({ r: 230, g: 130, b: 180 }), // Pink + colord({ r: 100, g: 160, b: 80 }), // Olive Green + colord({ r: 230, g: 150, b: 100 }), // Peach + colord({ r: 80, g: 130, b: 190 }), // Navy Blue + colord({ r: 210, g: 210, b: 100 }), // Lime Yellow + colord({ r: 190, g: 100, b: 130 }), // Maroon + colord({ r: 100, g: 210, b: 210 }), // Turquoise + colord({ r: 210, g: 140, b: 80 }), // Light Orange + colord({ r: 150, g: 110, b: 190 }), // Lavender + colord({ r: 180, g: 210, b: 120 }), // Light Green + colord({ r: 210, g: 100, b: 160 }), // Hot Pink + colord({ r: 100, g: 140, b: 110 }), // Sea Green + colord({ r: 230, g: 180, b: 180 }), // Light Pink + colord({ r: 120, g: 120, b: 190 }), // Periwinkle + colord({ r: 190, g: 170, b: 100 }), // Sand + colord({ r: 100, g: 180, b: 160 }), // Aquamarine + colord({ r: 210, g: 160, b: 200 }), // Orchid + colord({ r: 170, g: 190, b: 100 }), // Yellow Green + colord({ r: 100, g: 130, b: 150 }), // Steel Blue + colord({ r: 230, g: 140, b: 140 }), // Salmon + colord({ r: 140, g: 180, b: 220 }), // Light Blue + colord({ r: 200, g: 160, b: 110 }), // Tan + colord({ r: 180, g: 130, b: 180 }), // Plum + colord({ r: 130, g: 200, b: 130 }), // Light Sea Green + colord({ r: 220, g: 120, b: 120 }), // Coral + colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue + colord({ r: 200, g: 200, b: 140 }), // Khaki + colord({ r: 160, g: 120, b: 160 }), // Purple Gray + colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green + colord({ r: 200, g: 130, b: 110 }), // Dark Salmon + colord({ r: 130, g: 170, b: 190 }), // Cadet Blue + colord({ r: 190, g: 180, b: 160 }), // Tan Gray + colord({ r: 170, g: 140, b: 190 }), // Medium Purple + colord({ r: 160, g: 190, b: 160 }), // Pale Green + colord({ r: 190, g: 150, b: 130 }), // Rosy Brown + colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray + colord({ r: 180, g: 170, b: 140 }), // Dark Khaki + colord({ r: 150, g: 130, b: 150 }), // Thistle + colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green + colord({ r: 190, g: 140, b: 150 }), // Puce + colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine + colord({ r: 180, g: 160, b: 180 }), // Mauve + colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green + colord({ r: 170, g: 150, b: 170 }), // Dusty Rose +]; + +export const humanColors: Colord[] = [ + // Original set + colord({ r: 235, g: 75, b: 75 }), // Bright Red + colord({ r: 67, g: 190, b: 84 }), // Fresh Green + colord({ r: 59, g: 130, b: 246 }), // Royal Blue + colord({ r: 245, g: 158, b: 11 }), // Amber + colord({ r: 236, g: 72, b: 153 }), // Deep Pink + colord({ r: 48, g: 178, b: 180 }), // Teal + colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple + colord({ r: 251, g: 191, b: 36 }), // Marigold + colord({ r: 74, g: 222, b: 128 }), // Mint + colord({ r: 239, g: 68, b: 68 }), // Crimson + colord({ r: 34, g: 197, b: 94 }), // Emerald + colord({ r: 96, g: 165, b: 250 }), // Sky Blue + colord({ r: 249, g: 115, b: 22 }), // Tangerine + colord({ r: 192, g: 132, b: 252 }), // Lavender + colord({ r: 45, g: 212, b: 191 }), // Turquoise + colord({ r: 244, g: 114, b: 182 }), // Rose + colord({ r: 132, g: 204, b: 22 }), // Lime + colord({ r: 56, g: 189, b: 248 }), // Light Blue + colord({ r: 234, g: 179, b: 8 }), // Sunflower + colord({ r: 217, g: 70, b: 239 }), // Fuchsia + colord({ r: 16, g: 185, b: 129 }), // Sea Green + colord({ r: 251, g: 146, b: 60 }), // Light Orange + colord({ r: 147, g: 51, b: 234 }), // Bright Purple + colord({ r: 79, g: 70, b: 229 }), // Indigo + colord({ r: 245, g: 101, b: 101 }), // Coral + colord({ r: 134, g: 239, b: 172 }), // Light Green + colord({ r: 59, g: 130, b: 246 }), // Cerulean + colord({ r: 253, g: 164, b: 175 }), // Salmon Pink + colord({ r: 147, g: 197, b: 253 }), // Powder Blue + colord({ r: 252, g: 211, b: 77 }), // Golden + colord({ r: 190, g: 92, b: 251 }), // Amethyst + colord({ r: 82, g: 183, b: 136 }), // Jade + colord({ r: 248, g: 113, b: 113 }), // Warm Red + colord({ r: 99, g: 202, b: 253 }), // Azure + colord({ r: 240, g: 171, b: 252 }), // Orchid + colord({ r: 163, g: 230, b: 53 }), // Yellow Green + colord({ r: 234, g: 88, b: 12 }), // Burnt Orange + colord({ r: 125, g: 211, b: 252 }), // Crystal Blue + colord({ r: 251, g: 113, b: 133 }), // Watermelon + colord({ r: 52, g: 211, b: 153 }), // Spearmint + colord({ r: 167, g: 139, b: 250 }), // Periwinkle + colord({ r: 245, g: 158, b: 11 }), // Honey + colord({ r: 110, g: 231, b: 183 }), // Seafoam + colord({ r: 233, g: 213, b: 255 }), // Light Lilac + colord({ r: 202, g: 138, b: 4 }), // Rich Gold + colord({ r: 151, g: 255, b: 187 }), // Fresh Mint + colord({ r: 220, g: 38, b: 38 }), // Ruby + colord({ r: 124, g: 58, b: 237 }), // Royal Purple + colord({ r: 45, g: 212, b: 191 }), // Ocean + colord({ r: 252, g: 165, b: 165 }), // Peach + + // Additional 50 colors + colord({ r: 179, g: 136, b: 255 }), // Light Purple + colord({ r: 133, g: 77, b: 14 }), // Chocolate + colord({ r: 52, g: 211, b: 153 }), // Aquamarine + colord({ r: 234, g: 179, b: 8 }), // Mustard + colord({ r: 236, g: 72, b: 153 }), // Hot Pink + colord({ r: 147, g: 197, b: 253 }), // Sky + colord({ r: 249, g: 115, b: 22 }), // Pumpkin + colord({ r: 167, g: 139, b: 250 }), // Iris + colord({ r: 16, g: 185, b: 129 }), // Pine + colord({ r: 251, g: 146, b: 60 }), // Mango + colord({ r: 192, g: 132, b: 252 }), // Wisteria + colord({ r: 79, g: 70, b: 229 }), // Sapphire + colord({ r: 245, g: 101, b: 101 }), // Salmon + colord({ r: 134, g: 239, b: 172 }), // Spring Green + colord({ r: 59, g: 130, b: 246 }), // Ocean Blue + colord({ r: 253, g: 164, b: 175 }), // Rose Gold + colord({ r: 16, g: 185, b: 129 }), // Forest + colord({ r: 252, g: 211, b: 77 }), // Sunshine + colord({ r: 190, g: 92, b: 251 }), // Grape + colord({ r: 82, g: 183, b: 136 }), // Eucalyptus + colord({ r: 248, g: 113, b: 113 }), // Cherry + colord({ r: 99, g: 202, b: 253 }), // Arctic + colord({ r: 240, g: 171, b: 252 }), // Lilac + colord({ r: 163, g: 230, b: 53 }), // Chartreuse + colord({ r: 234, g: 88, b: 12 }), // Rust + colord({ r: 125, g: 211, b: 252 }), // Ice Blue + colord({ r: 251, g: 113, b: 133 }), // Strawberry + colord({ r: 52, g: 211, b: 153 }), // Sage + colord({ r: 167, g: 139, b: 250 }), // Violet + colord({ r: 245, g: 158, b: 11 }), // Apricot + colord({ r: 110, g: 231, b: 183 }), // Mint Green + colord({ r: 233, g: 213, b: 255 }), // Thistle + colord({ r: 202, g: 138, b: 4 }), // Bronze + colord({ r: 151, g: 255, b: 187 }), // Pistachio + colord({ r: 220, g: 38, b: 38 }), // Fire Engine + colord({ r: 124, g: 58, b: 237 }), // Electric Purple + colord({ r: 45, g: 212, b: 191 }), // Caribbean + colord({ r: 252, g: 165, b: 165 }), // Melon + colord({ r: 168, g: 85, b: 247 }), // Byzantium + colord({ r: 74, g: 222, b: 128 }), // Kelly Green + colord({ r: 239, g: 68, b: 68 }), // Cardinal + colord({ r: 34, g: 197, b: 94 }), // Shamrock + colord({ r: 96, g: 165, b: 250 }), // Marina + colord({ r: 249, g: 115, b: 22 }), // Carrot + colord({ r: 192, g: 132, b: 252 }), // Heliotrope + colord({ r: 45, g: 212, b: 191 }), // Lagoon + colord({ r: 244, g: 114, b: 182 }), // Bubble Gum + colord({ r: 132, g: 204, b: 22 }), // Apple + colord({ r: 56, g: 189, b: 248 }), // Electric Blue + colord({ r: 234, g: 179, b: 8 }), // Daffodil +]; + +export const redShades = [ + colord({ r: 210, g: 0, b: 0 }), // Most distinct colors sorted by brightness + colord({ r: 225, g: 0, b: 0 }), + colord({ r: 240, g: 0, b: 0 }), + colord({ r: 255, g: 0, b: 0 }), + colord({ r: 210, g: 21, b: 21 }), + colord({ r: 225, g: 23, b: 23 }), + colord({ r: 240, g: 25, b: 25 }), + colord({ r: 255, g: 26, b: 26 }), + colord({ r: 210, g: 42, b: 42 }), + colord({ r: 225, g: 45, b: 45 }), + colord({ r: 240, g: 48, b: 48 }), + colord({ r: 255, g: 51, b: 51 }), + colord({ r: 195, g: 59, b: 59 }), + colord({ r: 210, g: 63, b: 63 }), + colord({ r: 225, g: 68, b: 68 }), + colord({ r: 240, g: 72, b: 72 }), + colord({ r: 255, g: 77, b: 77 }), + colord({ r: 180, g: 72, b: 72 }), + colord({ r: 195, g: 78, b: 78 }), + colord({ r: 210, g: 84, b: 84 }), + colord({ r: 225, g: 90, b: 90 }), + colord({ r: 240, g: 96, b: 96 }), + colord({ r: 255, g: 102, b: 102 }), + colord({ r: 165, g: 83, b: 83 }), + colord({ r: 180, g: 90, b: 90 }), + colord({ r: 195, g: 98, b: 98 }), + colord({ r: 210, g: 105, b: 105 }), + colord({ r: 225, g: 113, b: 113 }), + colord({ r: 240, g: 120, b: 120 }), + colord({ r: 255, g: 128, b: 128 }), + colord({ r: 150, g: 90, b: 90 }), + colord({ r: 165, g: 99, b: 99 }), + colord({ r: 180, g: 108, b: 108 }), + colord({ r: 195, g: 117, b: 117 }), + colord({ r: 210, g: 126, b: 126 }), + colord({ r: 225, g: 135, b: 135 }), + colord({ r: 240, g: 144, b: 144 }), + colord({ r: 255, g: 153, b: 153 }), + colord({ r: 135, g: 95, b: 95 }), + colord({ r: 150, g: 105, b: 105 }), + colord({ r: 165, g: 116, b: 116 }), + colord({ r: 180, g: 126, b: 126 }), + colord({ r: 195, g: 137, b: 137 }), + colord({ r: 210, g: 147, b: 147 }), + colord({ r: 225, g: 158, b: 158 }), + colord({ r: 240, g: 168, b: 168 }), + colord({ r: 255, g: 179, b: 179 }), + colord({ r: 120, g: 108, b: 108 }), + colord({ r: 135, g: 122, b: 122 }), + colord({ r: 150, g: 135, b: 135 }), + colord({ r: 165, g: 149, b: 149 }), + colord({ r: 180, g: 162, b: 162 }), + colord({ r: 195, g: 176, b: 176 }), + colord({ r: 210, g: 189, b: 189 }), + colord({ r: 225, g: 203, b: 203 }), + colord({ r: 240, g: 216, b: 216 }), + colord({ r: 255, g: 230, b: 230 }), + colord({ r: 195, g: 0, b: 0 }), + colord({ r: 195, g: 20, b: 20 }), + colord({ r: 195, g: 39, b: 39 }), + colord({ r: 180, g: 54, b: 54 }), + colord({ r: 165, g: 66, b: 66 }), + colord({ r: 150, g: 75, b: 75 }), + colord({ r: 135, g: 81, b: 81 }), + colord({ r: 120, g: 84, b: 84 }), + colord({ r: 135, g: 108, b: 108 }), + colord({ r: 150, g: 120, b: 120 }), + colord({ r: 165, g: 132, b: 132 }), + colord({ r: 180, g: 144, b: 144 }), +]; + +export const blueShades = [ + colord({ r: 0, g: 0, b: 120 }), // Most distinct colors sorted by brightness + colord({ r: 0, g: 0, b: 135 }), + colord({ r: 0, g: 0, b: 150 }), + colord({ r: 0, g: 0, b: 165 }), + colord({ r: 0, g: 0, b: 180 }), + colord({ r: 0, g: 0, b: 195 }), + colord({ r: 0, g: 0, b: 210 }), + colord({ r: 0, g: 0, b: 225 }), + colord({ r: 0, g: 0, b: 240 }), + colord({ r: 0, g: 0, b: 255 }), + colord({ r: 12, g: 12, b: 120 }), + colord({ r: 14, g: 14, b: 135 }), + colord({ r: 15, g: 15, b: 150 }), + colord({ r: 17, g: 17, b: 165 }), + colord({ r: 18, g: 18, b: 180 }), + colord({ r: 20, g: 20, b: 195 }), + colord({ r: 21, g: 21, b: 210 }), + colord({ r: 23, g: 23, b: 225 }), + colord({ r: 25, g: 25, b: 240 }), + colord({ r: 26, g: 26, b: 255 }), + colord({ r: 24, g: 24, b: 120 }), + colord({ r: 27, g: 27, b: 135 }), + colord({ r: 30, g: 30, b: 150 }), + colord({ r: 33, g: 33, b: 165 }), + colord({ r: 36, g: 36, b: 180 }), + colord({ r: 39, g: 39, b: 195 }), + colord({ r: 42, g: 42, b: 210 }), + colord({ r: 45, g: 45, b: 225 }), + colord({ r: 48, g: 48, b: 240 }), + colord({ r: 51, g: 51, b: 255 }), + colord({ r: 36, g: 36, b: 120 }), + colord({ r: 41, g: 41, b: 135 }), + colord({ r: 45, g: 45, b: 150 }), + colord({ r: 50, g: 50, b: 165 }), + colord({ r: 54, g: 54, b: 180 }), + colord({ r: 59, g: 59, b: 195 }), + colord({ r: 63, g: 63, b: 210 }), + colord({ r: 68, g: 68, b: 225 }), + colord({ r: 72, g: 72, b: 240 }), + colord({ r: 77, g: 77, b: 255 }), + colord({ r: 48, g: 48, b: 120 }), + colord({ r: 54, g: 54, b: 135 }), + colord({ r: 60, g: 60, b: 150 }), + colord({ r: 66, g: 66, b: 165 }), + colord({ r: 72, g: 72, b: 180 }), + colord({ r: 78, g: 78, b: 195 }), + colord({ r: 84, g: 84, b: 210 }), + colord({ r: 90, g: 90, b: 225 }), + colord({ r: 96, g: 96, b: 240 }), + colord({ r: 102, g: 102, b: 255 }), + colord({ r: 60, g: 60, b: 120 }), + colord({ r: 68, g: 68, b: 135 }), + colord({ r: 75, g: 75, b: 150 }), + colord({ r: 83, g: 83, b: 165 }), + colord({ r: 90, g: 90, b: 180 }), + colord({ r: 98, g: 98, b: 195 }), + colord({ r: 105, g: 105, b: 210 }), + colord({ r: 113, g: 113, b: 225 }), + colord({ r: 120, g: 120, b: 240 }), + colord({ r: 128, g: 128, b: 255 }), + colord({ r: 72, g: 72, b: 120 }), + colord({ r: 81, g: 81, b: 135 }), + colord({ r: 90, g: 90, b: 150 }), + colord({ r: 99, g: 99, b: 165 }), + colord({ r: 108, g: 108, b: 180 }), + colord({ r: 117, g: 117, b: 195 }), + colord({ r: 126, g: 126, b: 210 }), + colord({ r: 135, g: 135, b: 225 }), + colord({ r: 144, g: 144, b: 240 }), + colord({ r: 153, g: 153, b: 255 }), +]; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 1f12f454b..33f3dd3a3 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -113,15 +113,15 @@ export interface Config { } export interface Theme { - territoryColor(playerInfo: PlayerInfo): Colord; - specialBuildingColor(playerInfo: PlayerInfo): Colord; - borderColor(playerInfo: PlayerInfo): Colord; - defendedBorderColor(playerInfo: PlayerInfo): Colord; + territoryColor(playerInfo: PlayerView): Colord; + specialBuildingColor(playerInfo: PlayerView): Colord; + borderColor(playerInfo: PlayerView): Colord; + defendedBorderColor(playerInfo: PlayerView): Colord; terrainColor(gm: GameMap, tile: TileRef): Colord; backgroundColor(): Colord; falloutColor(): Colord; font(): string; - textColor(playerInfo: PlayerInfo): string; + textColor(playerInfo: PlayerView): string; // unit color for alternate view selfColor(): Colord; allyColor(): Colord; diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index a1c3a1ecc..062a6ff4c 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,17 +1,11 @@ import { Colord, colord, random } from "colord"; -import { - Game, - PlayerID, - PlayerInfo, - PlayerType, - TerrainType, -} from "../game/Game"; +import { PlayerType, TeamName, TerrainType } from "../game/Game"; import { Theme } from "./Config"; -import { time } from "console"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; +import { blueShades, humanColors, redShades, territoryColors } from "./Colors"; export const pastelTheme = new (class implements Theme { private rand = new PseudoRandom(123); @@ -29,235 +23,31 @@ export const pastelTheme = new (class implements Theme { private water = colord({ r: 70, g: 132, b: 180 }); private shorelineWater = colord({ r: 100, g: 143, b: 255 }); - private territoryColors: Colord[] = [ - colord({ r: 230, g: 100, b: 100 }), // Bright Red - colord({ r: 100, g: 180, b: 230 }), // Sky Blue - colord({ r: 230, g: 180, b: 80 }), // Golden Yellow - colord({ r: 180, g: 100, b: 230 }), // Purple - colord({ r: 80, g: 200, b: 120 }), // Emerald Green - colord({ r: 230, g: 130, b: 180 }), // Pink - colord({ r: 100, g: 160, b: 80 }), // Olive Green - colord({ r: 230, g: 150, b: 100 }), // Peach - colord({ r: 80, g: 130, b: 190 }), // Navy Blue - colord({ r: 210, g: 210, b: 100 }), // Lime Yellow - colord({ r: 190, g: 100, b: 130 }), // Maroon - colord({ r: 100, g: 210, b: 210 }), // Turquoise - colord({ r: 210, g: 140, b: 80 }), // Light Orange - colord({ r: 150, g: 110, b: 190 }), // Lavender - colord({ r: 180, g: 210, b: 120 }), // Light Green - colord({ r: 210, g: 100, b: 160 }), // Hot Pink - colord({ r: 100, g: 140, b: 110 }), // Sea Green - colord({ r: 230, g: 180, b: 180 }), // Light Pink - colord({ r: 120, g: 120, b: 190 }), // Periwinkle - colord({ r: 190, g: 170, b: 100 }), // Sand - colord({ r: 100, g: 180, b: 160 }), // Aquamarine - colord({ r: 210, g: 160, b: 200 }), // Orchid - colord({ r: 170, g: 190, b: 100 }), // Yellow Green - colord({ r: 100, g: 130, b: 150 }), // Steel Blue - colord({ r: 230, g: 140, b: 140 }), // Salmon - colord({ r: 140, g: 180, b: 220 }), // Light Blue - colord({ r: 200, g: 160, b: 110 }), // Tan - colord({ r: 180, g: 130, b: 180 }), // Plum - colord({ r: 130, g: 200, b: 130 }), // Light Sea Green - colord({ r: 220, g: 120, b: 120 }), // Coral - colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue - colord({ r: 200, g: 200, b: 140 }), // Khaki - colord({ r: 160, g: 120, b: 160 }), // Purple Gray - colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green - colord({ r: 200, g: 130, b: 110 }), // Dark Salmon - colord({ r: 130, g: 170, b: 190 }), // Cadet Blue - colord({ r: 190, g: 180, b: 160 }), // Tan Gray - colord({ r: 170, g: 140, b: 190 }), // Medium Purple - colord({ r: 160, g: 190, b: 160 }), // Pale Green - colord({ r: 190, g: 150, b: 130 }), // Rosy Brown - colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray - colord({ r: 180, g: 170, b: 140 }), // Dark Khaki - colord({ r: 150, g: 130, b: 150 }), // Thistle - colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green - colord({ r: 190, g: 140, b: 150 }), // Puce - colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine - colord({ r: 180, g: 160, b: 180 }), // Mauve - colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green - colord({ r: 170, g: 150, b: 170 }), // Dusty Rose - colord({ r: 100, g: 180, b: 230 }), // Sky Blue - colord({ r: 230, g: 180, b: 80 }), // Golden Yellow - colord({ r: 180, g: 100, b: 230 }), // Purple - colord({ r: 80, g: 200, b: 120 }), // Emerald Green - colord({ r: 230, g: 130, b: 180 }), // Pink - colord({ r: 100, g: 160, b: 80 }), // Olive Green - colord({ r: 230, g: 150, b: 100 }), // Peach - colord({ r: 80, g: 130, b: 190 }), // Navy Blue - colord({ r: 210, g: 210, b: 100 }), // Lime Yellow - colord({ r: 190, g: 100, b: 130 }), // Maroon - colord({ r: 100, g: 210, b: 210 }), // Turquoise - colord({ r: 210, g: 140, b: 80 }), // Light Orange - colord({ r: 150, g: 110, b: 190 }), // Lavender - colord({ r: 180, g: 210, b: 120 }), // Light Green - colord({ r: 210, g: 100, b: 160 }), // Hot Pink - colord({ r: 100, g: 140, b: 110 }), // Sea Green - colord({ r: 230, g: 180, b: 180 }), // Light Pink - colord({ r: 120, g: 120, b: 190 }), // Periwinkle - colord({ r: 190, g: 170, b: 100 }), // Sand - colord({ r: 100, g: 180, b: 160 }), // Aquamarine - colord({ r: 210, g: 160, b: 200 }), // Orchid - colord({ r: 170, g: 190, b: 100 }), // Yellow Green - colord({ r: 100, g: 130, b: 150 }), // Steel Blue - colord({ r: 230, g: 140, b: 140 }), // Salmon - colord({ r: 140, g: 180, b: 220 }), // Light Blue - colord({ r: 200, g: 160, b: 110 }), // Tan - colord({ r: 180, g: 130, b: 180 }), // Plum - colord({ r: 130, g: 200, b: 130 }), // Light Sea Green - colord({ r: 220, g: 120, b: 120 }), // Coral - colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue - colord({ r: 200, g: 200, b: 140 }), // Khaki - colord({ r: 160, g: 120, b: 160 }), // Purple Gray - colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green - colord({ r: 200, g: 130, b: 110 }), // Dark Salmon - colord({ r: 130, g: 170, b: 190 }), // Cadet Blue - colord({ r: 190, g: 180, b: 160 }), // Tan Gray - colord({ r: 170, g: 140, b: 190 }), // Medium Purple - colord({ r: 160, g: 190, b: 160 }), // Pale Green - colord({ r: 190, g: 150, b: 130 }), // Rosy Brown - colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray - colord({ r: 180, g: 170, b: 140 }), // Dark Khaki - colord({ r: 150, g: 130, b: 150 }), // Thistle - colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green - colord({ r: 190, g: 140, b: 150 }), // Puce - colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine - colord({ r: 180, g: 160, b: 180 }), // Mauve - colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green - colord({ r: 170, g: 150, b: 170 }), // Dusty Rose - ]; - - private humanColors: Colord[] = [ - // Original set - colord({ r: 235, g: 75, b: 75 }), // Bright Red - colord({ r: 67, g: 190, b: 84 }), // Fresh Green - colord({ r: 59, g: 130, b: 246 }), // Royal Blue - colord({ r: 245, g: 158, b: 11 }), // Amber - colord({ r: 236, g: 72, b: 153 }), // Deep Pink - colord({ r: 48, g: 178, b: 180 }), // Teal - colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple - colord({ r: 251, g: 191, b: 36 }), // Marigold - colord({ r: 74, g: 222, b: 128 }), // Mint - colord({ r: 239, g: 68, b: 68 }), // Crimson - colord({ r: 34, g: 197, b: 94 }), // Emerald - colord({ r: 96, g: 165, b: 250 }), // Sky Blue - colord({ r: 249, g: 115, b: 22 }), // Tangerine - colord({ r: 192, g: 132, b: 252 }), // Lavender - colord({ r: 45, g: 212, b: 191 }), // Turquoise - colord({ r: 244, g: 114, b: 182 }), // Rose - colord({ r: 132, g: 204, b: 22 }), // Lime - colord({ r: 56, g: 189, b: 248 }), // Light Blue - colord({ r: 234, g: 179, b: 8 }), // Sunflower - colord({ r: 217, g: 70, b: 239 }), // Fuchsia - colord({ r: 16, g: 185, b: 129 }), // Sea Green - colord({ r: 251, g: 146, b: 60 }), // Light Orange - colord({ r: 147, g: 51, b: 234 }), // Bright Purple - colord({ r: 79, g: 70, b: 229 }), // Indigo - colord({ r: 245, g: 101, b: 101 }), // Coral - colord({ r: 134, g: 239, b: 172 }), // Light Green - colord({ r: 59, g: 130, b: 246 }), // Cerulean - colord({ r: 253, g: 164, b: 175 }), // Salmon Pink - colord({ r: 147, g: 197, b: 253 }), // Powder Blue - colord({ r: 252, g: 211, b: 77 }), // Golden - colord({ r: 190, g: 92, b: 251 }), // Amethyst - colord({ r: 82, g: 183, b: 136 }), // Jade - colord({ r: 248, g: 113, b: 113 }), // Warm Red - colord({ r: 99, g: 202, b: 253 }), // Azure - colord({ r: 240, g: 171, b: 252 }), // Orchid - colord({ r: 163, g: 230, b: 53 }), // Yellow Green - colord({ r: 234, g: 88, b: 12 }), // Burnt Orange - colord({ r: 125, g: 211, b: 252 }), // Crystal Blue - colord({ r: 251, g: 113, b: 133 }), // Watermelon - colord({ r: 52, g: 211, b: 153 }), // Spearmint - colord({ r: 167, g: 139, b: 250 }), // Periwinkle - colord({ r: 245, g: 158, b: 11 }), // Honey - colord({ r: 110, g: 231, b: 183 }), // Seafoam - colord({ r: 233, g: 213, b: 255 }), // Light Lilac - colord({ r: 202, g: 138, b: 4 }), // Rich Gold - colord({ r: 151, g: 255, b: 187 }), // Fresh Mint - colord({ r: 220, g: 38, b: 38 }), // Ruby - colord({ r: 124, g: 58, b: 237 }), // Royal Purple - colord({ r: 45, g: 212, b: 191 }), // Ocean - colord({ r: 252, g: 165, b: 165 }), // Peach - - // Additional 50 colors - colord({ r: 179, g: 136, b: 255 }), // Light Purple - colord({ r: 133, g: 77, b: 14 }), // Chocolate - colord({ r: 52, g: 211, b: 153 }), // Aquamarine - colord({ r: 234, g: 179, b: 8 }), // Mustard - colord({ r: 236, g: 72, b: 153 }), // Hot Pink - colord({ r: 147, g: 197, b: 253 }), // Sky - colord({ r: 249, g: 115, b: 22 }), // Pumpkin - colord({ r: 167, g: 139, b: 250 }), // Iris - colord({ r: 16, g: 185, b: 129 }), // Pine - colord({ r: 251, g: 146, b: 60 }), // Mango - colord({ r: 192, g: 132, b: 252 }), // Wisteria - colord({ r: 79, g: 70, b: 229 }), // Sapphire - colord({ r: 245, g: 101, b: 101 }), // Salmon - colord({ r: 134, g: 239, b: 172 }), // Spring Green - colord({ r: 59, g: 130, b: 246 }), // Ocean Blue - colord({ r: 253, g: 164, b: 175 }), // Rose Gold - colord({ r: 16, g: 185, b: 129 }), // Forest - colord({ r: 252, g: 211, b: 77 }), // Sunshine - colord({ r: 190, g: 92, b: 251 }), // Grape - colord({ r: 82, g: 183, b: 136 }), // Eucalyptus - colord({ r: 248, g: 113, b: 113 }), // Cherry - colord({ r: 99, g: 202, b: 253 }), // Arctic - colord({ r: 240, g: 171, b: 252 }), // Lilac - colord({ r: 163, g: 230, b: 53 }), // Chartreuse - colord({ r: 234, g: 88, b: 12 }), // Rust - colord({ r: 125, g: 211, b: 252 }), // Ice Blue - colord({ r: 251, g: 113, b: 133 }), // Strawberry - colord({ r: 52, g: 211, b: 153 }), // Sage - colord({ r: 167, g: 139, b: 250 }), // Violet - colord({ r: 245, g: 158, b: 11 }), // Apricot - colord({ r: 110, g: 231, b: 183 }), // Mint Green - colord({ r: 233, g: 213, b: 255 }), // Thistle - colord({ r: 202, g: 138, b: 4 }), // Bronze - colord({ r: 151, g: 255, b: 187 }), // Pistachio - colord({ r: 220, g: 38, b: 38 }), // Fire Engine - colord({ r: 124, g: 58, b: 237 }), // Electric Purple - colord({ r: 45, g: 212, b: 191 }), // Caribbean - colord({ r: 252, g: 165, b: 165 }), // Melon - colord({ r: 168, g: 85, b: 247 }), // Byzantium - colord({ r: 74, g: 222, b: 128 }), // Kelly Green - colord({ r: 239, g: 68, b: 68 }), // Cardinal - colord({ r: 34, g: 197, b: 94 }), // Shamrock - colord({ r: 96, g: 165, b: 250 }), // Marina - colord({ r: 249, g: 115, b: 22 }), // Carrot - colord({ r: 192, g: 132, b: 252 }), // Heliotrope - colord({ r: 45, g: 212, b: 191 }), // Lagoon - colord({ r: 244, g: 114, b: 182 }), // Bubble Gum - colord({ r: 132, g: 204, b: 22 }), // Apple - colord({ r: 56, g: 189, b: 248 }), // Electric Blue - colord({ r: 234, g: 179, b: 8 }), // Daffodil - ]; - private _selfColor = colord({ r: 0, g: 255, b: 0 }); private _allyColor = colord({ r: 255, g: 255, b: 0 }); private _enemyColor = colord({ r: 255, g: 0, b: 0 }); private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); - territoryColor(playerInfo: PlayerInfo): Colord { - if (playerInfo.playerType == PlayerType.Human) { - return this.humanColors[ - simpleHash(playerInfo.name) % this.humanColors.length - ]; + territoryColor(player: PlayerView): Colord { + if (player.teamName() == TeamName.Red) { + return redShades[simpleHash(player.id()) % redShades.length]; } - return this.territoryColors[ - simpleHash(playerInfo.name) % this.territoryColors.length - ]; + if (player.teamName() == TeamName.Blue) { + return blueShades[simpleHash(player.id()) % blueShades.length]; + } + if (player.info().playerType == PlayerType.Human) { + return humanColors[simpleHash(player.id()) % humanColors.length]; + } + return territoryColors[simpleHash(player.id()) % territoryColors.length]; } - textColor(playerInfo: PlayerInfo): string { - return playerInfo.playerType == PlayerType.Human ? "#000000" : "#4D4D4D"; + textColor(player: PlayerView): string { + return player.info().playerType == PlayerType.Human ? "#000000" : "#4D4D4D"; } - specialBuildingColor(playerInfo: PlayerInfo): Colord { - const tc = this.territoryColor(playerInfo).rgba; + specialBuildingColor(player: PlayerView): Colord { + const tc = this.territoryColor(player).rgba; return colord({ r: Math.max(tc.r - 50, 0), g: Math.max(tc.g - 50, 0), @@ -265,16 +55,16 @@ export const pastelTheme = new (class implements Theme { }); } - borderColor(playerInfo: PlayerInfo): Colord { - const tc = this.territoryColor(playerInfo).rgba; + borderColor(player: PlayerView): Colord { + const tc = this.territoryColor(player).rgba; return colord({ r: Math.max(tc.r - 40, 0), g: Math.max(tc.g - 40, 0), b: Math.max(tc.b - 40, 0), }); } - defendedBorderColor(playerInfo: PlayerInfo): Colord { - const bc = this.borderColor(playerInfo).rgba; + defendedBorderColor(player: PlayerView): Colord { + const bc = this.borderColor(player).rgba; return colord({ r: Math.max(bc.r - 40, 0), g: Math.max(bc.g - 40, 0), diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index a7b030889..725e6b283 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -241,23 +241,23 @@ export const pastelThemeDark = new (class implements Theme { private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); - territoryColor(playerInfo: PlayerInfo): Colord { - if (playerInfo.playerType == PlayerType.Human) { + territoryColor(player: PlayerView): Colord { + if (player.info().playerType == PlayerType.Human) { return this.humanColors[ - simpleHash(playerInfo.name) % this.humanColors.length + simpleHash(player.info().name) % this.humanColors.length ]; } return this.territoryColors[ - simpleHash(playerInfo.name) % this.territoryColors.length + simpleHash(player.info().name) % this.territoryColors.length ]; } - textColor(playerInfo: PlayerInfo): string { - return playerInfo.playerType == PlayerType.Human ? "#ffffff" : "#e6e6e6"; + textColor(player: PlayerView): string { + return player.info().playerType == PlayerType.Human ? "#ffffff" : "#e6e6e6"; } - specialBuildingColor(playerInfo: PlayerInfo): Colord { - const tc = this.territoryColor(playerInfo).rgba; + specialBuildingColor(player: PlayerView): Colord { + const tc = this.territoryColor(player).rgba; return colord({ r: Math.max(tc.r - 50, 0), g: Math.max(tc.g - 50, 0), @@ -265,16 +265,16 @@ export const pastelThemeDark = new (class implements Theme { }); } - borderColor(playerInfo: PlayerInfo): Colord { - const tc = this.territoryColor(playerInfo).rgba; + borderColor(player: PlayerView): Colord { + const tc = this.territoryColor(player).rgba; return colord({ r: Math.max(tc.r - 40, 0), g: Math.max(tc.g - 40, 0), b: Math.max(tc.b - 40, 0), }); } - defendedBorderColor(playerInfo: PlayerInfo): Colord { - const bc = this.borderColor(playerInfo).rgba; + defendedBorderColor(player: PlayerView): Colord { + const bc = this.borderColor(player).rgba; return colord({ r: Math.max(bc.r - 40, 0), g: Math.max(bc.g - 40, 0), diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 956fb90a8..cda58140b 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -54,7 +54,7 @@ export class BotExecution implements Execution { .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; if (traitors.length > 0) { const toAttack = this.random.randElement(traitors); - const odds = this.bot.isAlliedWith(toAttack) ? 6 : 3; + const odds = this.bot.isFriendly(toAttack) ? 6 : 3; if (this.random.chance(odds)) { this.sendAttack(toAttack); return; @@ -85,7 +85,7 @@ export class BotExecution implements Execution { const owner = this.mg.owner(toAttack); if (owner.isPlayer()) { - if (this.bot.isAlliedWith(owner)) { + if (this.bot.isFriendly(owner)) { return; } if (owner.type() == PlayerType.FakeHuman) { diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 87a5eea29..8b90528da 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -192,7 +192,7 @@ export class FakeHumanExecution implements Execution { } private shouldAttack(other: Player): boolean { - if (this.player.isAlliedWith(other)) { + if (this.player.isFriendly(other)) { if (this.shouldDiscourageAttack(other)) { return this.random.chance(200); } @@ -271,12 +271,11 @@ export class FakeHumanExecution implements Execution { } } - if (this.player.isAlliedWith(this.enemy)) { - this.enemy = null; - return; - } - if (this.enemy) { + if (this.player.isFriendly(this.enemy)) { + this.enemy = null; + return; + } this.maybeSendNuke(this.enemy); if (this.player.sharesBorderWith(this.enemy)) { this.sendAttack(this.enemy); @@ -537,7 +536,7 @@ export class FakeHumanExecution implements Execution { } if ( this.mg.owner(dst).isPlayer() && - this.player.isAlliedWith(this.mg.owner(dst) as Player) + this.player.isFriendly(this.mg.owner(dst) as Player) ) { continue; } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 4b0a5a64b..0a5d55864 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -116,7 +116,7 @@ export class PlayerExecution implements Execution { const main = clusters.shift(); this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main); const surroundedBy = this.surroundedBySamePlayer(main); - if (surroundedBy && !this.player.isAlliedWith(surroundedBy)) { + if (surroundedBy && !this.player.isFriendly(surroundedBy)) { this.removeCluster(main); } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 50d8e6a90..16b241bbd 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -72,8 +72,7 @@ export class SAMLauncherExecution implements Execution { ]) .filter( ({ unit }) => - unit.owner() !== this.player && - !unit.owner().isAlliedWith(this.player), + unit.owner() !== this.player && !this.player.isFriendly(unit.owner()), ); this.target = diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 14eaa6419..1ba358e19 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -152,7 +152,7 @@ export class TransportShipExecution implements Execution { this.active = false; return; } - if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) { + if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) { this.target.addTroops(this.troops); } else { this.attacker.conquer(this.dst); diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index ddaf65c02..c958b4d20 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -146,7 +146,7 @@ export class WarshipExecution implements Execution { ({ unit }) => unit.owner() !== this.warship.owner() && unit !== this.warship && - !unit.owner().isAlliedWith(this.warship.owner()) && + !unit.owner().isFriendly(this.warship.owner()) && !this.alreadySentShell.has(unit) && (unit.type() !== UnitType.TradeShip || hasPort) && (unit.type() !== UnitType.TradeShip || diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index 1b2645c02..edf5a1e5d 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -1,5 +1,5 @@ import { EventBus, GameEvent } from "../EventBus"; -import { Execution, Game, Player, PlayerID } from "../game/Game"; +import { Execution, Game, GameMode, Player, Team } from "../game/Game"; export class WinEvent implements GameEvent { constructor(public readonly winner: Player) {} @@ -20,6 +20,14 @@ export class WinCheckExecution implements Execution { if (ticks % 10 != 0) { return; } + if (this.mg.config().gameConfig().gameMode == GameMode.FFA) { + this.checkWinnerFFA(); + } else { + this.checkWinnerTeam(); + } + } + + checkWinnerFFA(): void { const sorted = this.mg .players() .sort((a, b) => b.numTilesOwned() - a.numTilesOwned()); @@ -39,6 +47,30 @@ export class WinCheckExecution implements Execution { } } + checkWinnerTeam(): void { + const teamToTiles = new Map(); + for (const player of this.mg.players()) { + teamToTiles.set( + player.team(), + (teamToTiles.get(player.team()) ?? 0) + player.numTilesOwned(), + ); + } + const sorted = Array.from(teamToTiles.entries()).sort( + (a, b) => b[1] - a[1], + ); + if (sorted.length == 0) { + return; + } + const max = sorted[0]; + const numTilesWithoutFallout = + this.mg.numLandTiles() - this.mg.numTilesWithFallout(); + const percentage = (max[1] / numTilesWithoutFallout) * 100; + if (percentage > this.mg.config().percentageTilesOwnedToWin()) { + this.mg.setWinner(max[0].name, this.mg.stats().stats()); + console.log(`${max[0].name} has won the game`); + this.active = false; + } + } owner(): Player { return null; } diff --git a/src/core/execution/alliance/AllianceRequestExecution.ts b/src/core/execution/alliance/AllianceRequestExecution.ts index 21128d840..3703c96ba 100644 --- a/src/core/execution/alliance/AllianceRequestExecution.ts +++ b/src/core/execution/alliance/AllianceRequestExecution.ts @@ -40,7 +40,7 @@ export class AllianceRequestExecution implements Execution { } tick(ticks: number): void { - if (this.requestor.isAlliedWith(this.recipient)) { + if (this.requestor.isFriendly(this.recipient)) { consolex.warn("already allied"); } else if (!this.requestor.canSendAllianceRequest(this.recipient)) { consolex.warn("recent or pending alliance request"); diff --git a/src/core/execution/alliance/AllianceRequestReplyExecution.ts b/src/core/execution/alliance/AllianceRequestReplyExecution.ts index d0c0d4688..04a79b316 100644 --- a/src/core/execution/alliance/AllianceRequestReplyExecution.ts +++ b/src/core/execution/alliance/AllianceRequestReplyExecution.ts @@ -40,7 +40,7 @@ export class AllianceRequestReplyExecution implements Execution { } tick(ticks: number): void { - if (this.requestor.isAlliedWith(this.recipient)) { + if (this.requestor.isFriendly(this.recipient)) { consolex.warn("already allied"); } else { const request = this.requestor diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 201ede85a..4a920164d 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -38,6 +38,11 @@ export enum Difficulty { Impossible = "Impossible", } +export enum TeamName { + Red = "Red", + Blue = "Blue", +} + export enum GameMapType { World = "World", Europe = "Europe", @@ -62,6 +67,15 @@ export enum GameType { Private = "Private", } +export enum GameMode { + FFA = "Free For All", + Team = "Team", +} + +export interface Team { + name: TeamName; +} + export interface UnitInfo { cost: (player: Player | PlayerView) => Gold; // Determines if its owner changes when its tile is conquered. @@ -95,6 +109,7 @@ export const nukeTypes = [ UnitType.MIRVWarhead, UnitType.MIRV, ] as UnitType[]; + export type NukeType = (typeof nukeTypes)[number]; export enum Relation { @@ -327,8 +342,10 @@ export interface Player { allRelationsSorted(): { player: Player; relation: Relation }[]; updateRelation(other: Player, delta: number): void; decayRelations(): void; - - // Alliances + isOnSameTeam(other: Player): boolean; + // Either allied or on same team. + isFriendly(other: Player): boolean; + team(): Team | null; incomingAllianceRequests(): AllianceRequest[]; outgoingAllianceRequests(): AllianceRequest[]; alliances(): MutableAlliance[]; @@ -400,11 +417,13 @@ export interface Game extends GameMap { terraNullius(): TerraNullius; owner(ref: TileRef): Player | TerraNullius; + teams(): Team[]; + // Game State ticks(): Tick; inSpawnPhase(): boolean; executeNextTick(): GameUpdates; - setWinner(winner: Player, allPlayersStats: AllPlayersStats): void; + setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void; config(): Config; // Units diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index e80d0bc72..6725bcd2e 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -16,6 +16,9 @@ import { GameUpdates, TerrainType, EmojiMessage, + Team, + GameMode, + TeamName, } from "./Game"; import { GameUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; @@ -32,6 +35,7 @@ import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; import { UnitGrid } from "./UnitGrid"; import { StatsImpl } from "./StatsImpl"; import { Stats } from "./Stats"; +import { simpleHash } from "../Util"; export function createGame( gameMap: GameMap, @@ -70,6 +74,8 @@ export class GameImpl implements Game { private _stats: StatsImpl = new StatsImpl(); + private teams_: Team[] = []; + constructor( private _map: GameMap, private miniGameMap: GameMap, @@ -89,6 +95,9 @@ export class GameImpl implements Game { ), ); this.unitGrid = new UnitGrid(this._map); + if (this._config.gameConfig().gameMode == GameMode.Team) { + this.teams_ = [{ name: TeamName.Red }, { name: TeamName.Blue }]; + } } isOnEdgeOfMap(ref: TileRef): boolean { return this._map.isOnEdgeOfMap(ref); @@ -322,6 +331,7 @@ export class GameImpl implements Game { this.nextPlayerID, playerInfo, manpower, + this.maybeAssignTeam(playerInfo), ); this._playersBySmallID.push(player); this.nextPlayerID++; @@ -329,6 +339,14 @@ export class GameImpl implements Game { return player; } + private maybeAssignTeam(player: PlayerInfo): Team | null { + if (this._config.gameConfig().gameMode != GameMode.Team) { + return null; + } + const rand = simpleHash(player.id); + return this.teams_[rand % this.teams_.length]; + } + player(id: PlayerID | null): Player { if (!this._players.has(id)) { throw new Error(`Player with id ${id} not found`); @@ -513,14 +531,19 @@ export class GameImpl implements Game { }); } - setWinner(winner: Player, allPlayersStats: AllPlayersStats): void { + setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void { this.addUpdate({ type: GameUpdateType.Win, - winnerID: winner.smallID(), + winner: typeof winner === "string" ? winner : winner.smallID(), + winnerType: typeof winner === "string" ? "team" : "player", allPlayersStats, }); } + teams(): Team[] { + return this.teams_; + } + displayMessage( message: string, type: MessageType, diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 9461a05b4..cea31f9f9 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -8,6 +8,7 @@ import { NameViewData, PlayerID, PlayerType, + TeamName, Tick, UnitType, } from "./Game"; @@ -93,6 +94,7 @@ export interface PlayerUpdate { name: string; displayName: string; id: PlayerID; + teamName?: TeamName; smallID: number; playerType: PlayerType; isAlive: boolean; @@ -159,7 +161,9 @@ export interface DisplayMessageUpdate { export interface WinUpdate { type: GameUpdateType.Win; allPlayersStats: AllPlayersStats; - winnerID: number; + // Player id or team name. + winner: number | TeamName; + winnerType: "player" | "team"; } export interface HashUpdate { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 3a4a75936..f598d816a 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -6,6 +6,7 @@ import { Player, PlayerActions, PlayerProfile, + TeamName, } from "./Game"; import { AttackUpdate, PlayerUpdate } from "./GameUpdates"; import { UnitUpdate } from "./GameUpdates"; @@ -168,6 +169,9 @@ export class PlayerView { id(): PlayerID { return this.data.id; } + teamName(): TeamName { + return this.data.teamName; + } type(): PlayerType { return this.data.playerType; } @@ -210,6 +214,16 @@ export class PlayerView { return this.data.allies.some((n) => other.smallID() == n); } + isOnSameTeam(other: PlayerView): boolean { + return ( + this.data.teamName != null && this.data.teamName == other.data.teamName + ); + } + + isFriendly(other: PlayerView): boolean { + return this.isAlliedWith(other) || this.isOnSameTeam(other); + } + isRequestingAllianceWith(other: PlayerView) { return this.data.outgoingAllianceRequests.some((id) => other.id() == id); } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 56cd55fce..6bcb93076 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -19,6 +19,7 @@ import { PlayerProfile, Attack, UnitSpecificInfos, + Team, } from "./Game"; import { AttackUpdate, PlayerUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; @@ -100,6 +101,7 @@ export class PlayerImpl implements Player { private _smallID: number, private readonly playerInfo: PlayerInfo, startTroops: number, + private _team: Team | null, ) { this._flag = playerInfo.flag; this._name = sanitizeUsername(playerInfo.name); @@ -125,6 +127,7 @@ export class PlayerImpl implements Player { name: this.name(), displayName: this.displayName(), id: this.id(), + teamName: this.team()?.name, smallID: this.smallID(), playerType: this.type(), isAlive: this.isAlive(), @@ -324,7 +327,7 @@ export class PlayerImpl implements Player { if (other == this) { return false; } - if (this.isAlliedWith(other)) { + if (this.isFriendly(other)) { return false; } @@ -426,7 +429,7 @@ export class PlayerImpl implements Player { if (this == other) { return false; } - if (this.isAlliedWith(other)) { + if (this.isFriendly(other)) { return false; } for (const t of this.targets_) { @@ -500,7 +503,7 @@ export class PlayerImpl implements Player { } canDonate(recipient: Player): boolean { - if (!this.isAlliedWith(recipient)) { + if (!this.isFriendly(recipient)) { return false; } for (const donation of this.sentDonations) { @@ -555,6 +558,24 @@ export class PlayerImpl implements Player { .filter((other) => other != this && this.canTrade(other)); } + team(): Team | null { + return this._team; + } + + isOnSameTeam(other: Player): boolean { + if (other == this) { + return false; + } + if (this.team() == null || other.team() == null) { + return false; + } + return this._team == other.team(); + } + + isFriendly(other: Player): boolean { + return this.isOnSameTeam(other) || this.isAlliedWith(other); + } + gold(): Gold { return Number(this._gold); } @@ -837,7 +858,7 @@ export class PlayerImpl implements Player { if (other == this) { return false; } - if (other.isPlayer() && this.allianceWith(other)) { + if (other.isPlayer() && this.isFriendly(other)) { return false; } @@ -930,12 +951,13 @@ export class PlayerImpl implements Player { if (this.mg.owner(tile) == this) { return false; } - if ( - this.mg.hasOwner(tile) && - this.isAlliedWith(this.mg.owner(tile) as Player) - ) { - return false; + if (this.mg.hasOwner(tile)) { + const other = this.mg.owner(tile) as Player; + if (this.isFriendly(other)) { + return false; + } } + if (!this.mg.isLand(tile)) { return false; } diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 08049c22a..ec88ec343 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -2,7 +2,7 @@ import { ServerConfig } from "../core/configuration/Config"; import { GameConfig, GameID } from "../core/Schemas"; import { Client } from "./Client"; import { GamePhase, GameServer } from "./GameServer"; -import { Difficulty, GameMapType, GameType } from "../core/game/Game"; +import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; import { Logger } from "winston"; export class GameManager { @@ -38,6 +38,7 @@ export class GameManager { infiniteGold: false, infiniteTroops: false, instantBuild: false, + gameMode: GameMode.FFA, bots: 400, ...gameConfig, }); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 2663186c7..b133191e1 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -5,6 +5,7 @@ import { ClientID, ClientMessage, ClientMessageSchema, + ClientSendWinnerMessage, GameConfig, GameInfo, Intent, @@ -45,7 +46,7 @@ export class GameServer { private lastPingUpdate = 0; - private winner: ClientID | null = null; + private winner: ClientSendWinnerMessage = null; // This field is currently only filled at victory private allPlayersStats: AllPlayersStats = {}; @@ -86,6 +87,9 @@ export class GameServer { if (gameConfig.instantBuild != null) { this.gameConfig.instantBuild = gameConfig.instantBuild; } + if (gameConfig.gameMode != null) { + this.gameConfig.gameMode = gameConfig.gameMode; + } } public addClient(client: Client, lastTurn: number) { @@ -171,7 +175,7 @@ export class GameServer { client.hashes.set(clientMsg.turnNumber, clientMsg.hash); } if (clientMsg.type == "winner") { - this.winner = clientMsg.winner; + this.winner = clientMsg; this.allPlayersStats = clientMsg.allPlayersStats; } } catch (error) { @@ -318,7 +322,8 @@ export class GameServer { this.turns, this._startTime, Date.now(), - this.winner, + this.winner.winner, + this.winner.winnerType, this.allPlayersStats, ), ); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index d4def7141..04180e072 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -165,6 +165,7 @@ export function startWorker() { bots: req.body.bots, disableNPCs: req.body.disableNPCs, disableNukes: req.body.disableNukes, + gameMode: req.body.gameMode, }); res.status(200).json({ success: true }); }),