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 }); }),