Merge branch 'openfrontio:main' into main

This commit is contained in:
Mittanicz
2025-03-28 10:10:30 +01:00
committed by GitHub
54 changed files with 1073 additions and 546 deletions
+5
View File
@@ -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"
}
}
+165
View File
@@ -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"
}
}
+18 -2
View File
@@ -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);
+83 -53
View File
@@ -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 {
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
${translateText("host_modal.options_title")}
<!-- Game Mode Selection -->
<div class="options-section">
<div class="option-title">${translateText("host_modal.mode")}</div>
<div class="option-cards">
<div
class="option-card ${this.gameMode === GameMode.FFA ? "selected" : ""}"
@click=${() => this.handleGameModeSelection(GameMode.FFA)}
>
<div class="option-card-title">
${translateText("game_mode.ffa")}
</div>
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots == 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
<div
class="option-card ${this.gameMode === GameMode.Team ? "selected" : ""}"
@click=${() => this.handleGameModeSelection(GameMode.Team)}
>
<div class="option-card-title">
${translateText("game_mode.teams")}
</div>
</div>
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
${translateText("host_modal.options_title")}
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots == 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
<label
for="host-modal-disable-npcs"
@@ -244,37 +268,37 @@ export class HostLobbyModal extends LitElement {
</div>
</div>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${
this.players.length === 1
? translateText("host_modal.player")
: translateText("host_modal.players")
}
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${
this.players.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")
? translateText("host_modal.player")
: translateText("host_modal.players")
}
</button>
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
</o-modal>
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
>
${
this.players.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")
}
</button>
</div>
</o-modal>
`;
}
@@ -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);
});
}
+5 -3
View File
@@ -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) {
+35 -2
View File
@@ -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 {
</div>
</div>
<!-- Game Mode Selection -->
<div class="options-section">
<div class="option-title">${translateText("host_modal.mode")}</div>
<div class="option-cards">
<div
class="option-card ${this.gameMode === GameMode.FFA
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.FFA)}
>
<div class="option-card-title">
${translateText("game_mode.ffa")}
</div>
</div>
<div
class="option-card ${this.gameMode === GameMode.Team
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.Team)}
>
<div class="option-card-title">
${translateText("game_mode.teams")}
</div>
</div>
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
@@ -122,7 +150,7 @@ export class SinglePlayerModal extends LitElement {
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("single_modal.bots")}</span>${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,
+4 -1
View File
@@ -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 {
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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;
@@ -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`
<div class="p-2">
<div
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex ${isAlly
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex ${isFriendly
? "text-green-500"
: "text-white"}"
>
@@ -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`
+3 -3
View File
@@ -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;
}
+3 -3
View File
@@ -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,
);
}
+1 -1
View File
@@ -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);
+14 -14
View File
@@ -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,
);
}
+31 -11
View File
@@ -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();
});
}
+1
View File
@@ -270,6 +270,7 @@
<option value="fr">Français</option>
<option value="nl">Nederlands</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
</div>
</div>
+10 -2
View File
@@ -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(),
+4 -2
View File
@@ -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;
}
+352
View File
@@ -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 }),
];
+9 -5
View File
@@ -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;
@@ -109,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;
+8
View File
@@ -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;
+20 -230
View File
@@ -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),
+12 -12
View File
@@ -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),
+4
View File
@@ -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;
+2 -2
View File
@@ -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) {
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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;
}
+3 -3
View File
@@ -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 {
+6 -7
View File
@@ -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;
}
+4 -4
View File
@@ -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 {
@@ -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;
}
+1 -1
View File
@@ -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);
}
+9 -23
View File
@@ -38,27 +38,15 @@ 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)) {
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, 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),
);
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()) {
@@ -66,6 +54,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))
@@ -73,10 +65,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.player().tradingPorts(this.port);
if (ports.length == 0) {
return;
}
@@ -88,10 +78,6 @@ export class PortExecution implements Execution {
);
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+5 -6
View File
@@ -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());
}
@@ -68,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 =
@@ -133,10 +136,6 @@ export class SAMLauncherExecution implements Execution {
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -85,9 +85,6 @@ export class SAMMissileExecution implements Execution {
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
-3
View File
@@ -63,9 +63,6 @@ export class ShellExecution implements Execution {
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
-4
View File
@@ -151,10 +151,6 @@ export class TradeShipExecution implements Execution {
return;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+1 -1
View File
@@ -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);
+3 -8
View File
@@ -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";
@@ -147,11 +146,11 @@ 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 ||
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;
}
+33 -1
View File
@@ -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<Team, number>();
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;
}
@@ -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");
@@ -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
+23 -5
View File
@@ -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 {
@@ -156,7 +171,6 @@ export interface Execution {
activeDuringSpawnPhase(): boolean;
init(mg: Game, ticks: number): void;
tick(ticks: number): void;
owner(): Player;
}
export interface Attack {
@@ -328,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[];
@@ -375,10 +391,10 @@ export interface Player {
executeRetreat(attackID: string): void;
// Misc
executions(): Execution[];
toUpdate(): PlayerUpdate;
playerProfile(): PlayerProfile;
canBoat(tile: TileRef): boolean;
tradingPorts(port: Unit): Unit[];
}
export interface Game extends GameMap {
@@ -401,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
+25 -2
View File
@@ -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,
+5 -1
View File
@@ -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 {
+14
View File
@@ -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);
}
+71 -15
View File
@@ -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(),
@@ -285,11 +288,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);
@@ -329,7 +327,7 @@ export class PlayerImpl implements Player {
if (other == this) {
return false;
}
if (this.isAlliedWith(other)) {
if (this.isFriendly(other)) {
return false;
}
@@ -431,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_) {
@@ -505,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) {
@@ -560,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);
}
@@ -735,7 +751,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) =>
@@ -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;
}
@@ -958,4 +980,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;
}
}
+2 -1
View File
@@ -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,
});
+8 -3
View File
@@ -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,
),
);
+1
View File
@@ -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 });
}),
+29 -11
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

After

Width:  |  Height:  |  Size: 108 B

+15
View File
@@ -1,7 +1,22 @@
import { DefaultConfig } from "../../src/core/configuration/DefaultConfig";
export class TestConfig extends DefaultConfig {
_proximityBonusPortsNb: number = 0;
samHittingChance(): number {
return 1;
}
radiusPortSpawn(): number {
return 1;
}
proximityBonusPortsNb(totalPorts: number): number {
return this._proximityBonusPortsNb;
}
// Specific to TestConfig
setProximityBonusPortsNb(nb: number): void {
this._proximityBonusPortsNb = nb;
}
}
+10 -77
View File
@@ -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