diff --git a/src/client/ColorInput.ts b/src/client/ColorInput.ts new file mode 100644 index 0000000000..77663500b3 --- /dev/null +++ b/src/client/ColorInput.ts @@ -0,0 +1,108 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { UserSettings } from "../core/game/UserSettings"; + +export const COLOR_MODE_CHANGED_EVENT = "color-mode-changed"; + +/** + * A small button that sits next to the Skin/Pattern button on the main menu. + * Clicking it cycles through color modes: Random → Custom → Team. + * In "custom" mode a native color picker is also shown. + */ +@customElement("color-input") +export class ColorInput extends LitElement { + @state() private mode: "random" | "custom" | "team" = "random"; + @state() private primaryColor = "#2196f3"; + + private userSettings = new UserSettings(); + + createRenderRoot() { + return this; + } + + connectedCallback() { + super.connectedCallback(); + this.mode = this.userSettings.colorMode(); + this.primaryColor = this.userSettings.customPrimaryColor(); + } + + private cycleMode() { + const next: Record = { + random: "custom", + custom: "team", + team: "random", + }; + const newMode = next[this.mode]; + this.mode = newMode; + this.userSettings.setColorMode(newMode); + window.dispatchEvent(new CustomEvent(COLOR_MODE_CHANGED_EVENT)); + } + + private onColorChange(e: Event) { + const color = (e.target as HTMLInputElement).value; + this.primaryColor = color; + this.userSettings.setCustomPrimaryColor(color); + // Use a slightly darker shade as secondary color + this.userSettings.setCustomSecondaryColor(color); + window.dispatchEvent(new CustomEvent(COLOR_MODE_CHANGED_EVENT)); + } + + private modeLabel(): string { + if (this.mode === "custom") return "Custom"; + if (this.mode === "team") return "Team"; + return "Random"; + } + + private modeIcon(): string { + if (this.mode === "custom") return "🎨"; + if (this.mode === "team") return "🤝"; + return "🎲"; + } + + render() { + return html` +
+ + + + + ${this.mode === "custom" + ? html` + + ` + : ""} +
+ `; + } +} diff --git a/src/client/components/PlayPage.ts b/src/client/components/PlayPage.ts index ef33aa1003..7c682b0172 100644 --- a/src/client/components/PlayPage.ts +++ b/src/client/components/PlayPage.ts @@ -1,6 +1,7 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; import { assetUrl } from "../../core/AssetUrls"; +import "../ColorInput"; import "./NewsBox"; @customElement("play-page") @@ -91,6 +92,11 @@ export class PlayPage extends LitElement { adaptive-size class="shrink-0 lg:hidden" > + + - + -
this.setSort("gold")} - > - ${translateText("leaderboard.gold")} - ${this._sortKey === "gold" - ? this._sortOrder === "asc" - ? "⬆️" - : "⬇️" - : ""} -
-
this.setSort("maxtroops")} - > - ${translateText("leaderboard.maxtroops")} - ${this._sortKey === "maxtroops" - ? this._sortOrder === "asc" - ? "⬆️" - : "⬇️" - : ""} -
+ + ${this._viewState === "default" ? html` +
this.setSort("gold")} + > + ${translateText("leaderboard.gold")} + ${this._sortKey === "gold" ? (this._sortOrder === "asc" ? "⬆️" : "⬇️") : ""} +
+
this.setSort("maxtroops")} + > + ${translateText("leaderboard.maxtroops")} + ${this._sortKey === "maxtroops" ? (this._sortOrder === "asc" ? "⬆️" : "⬇️") : ""} +
+ ` : html` +
this.setSort("betrayals")} + > + Betrayals + ${this._sortKey === "betrayals" ? (this._sortOrder === "asc" ? "⬆️" : "⬇️") : ""} +
+
this.setSort("alliances")} + > + Alliances + ${this._sortKey === "alliances" ? (this._sortOrder === "asc" ? "⬆️" : "⬇️") : ""} +
+ `} ${repeat( @@ -257,39 +282,53 @@ export class Leaderboard extends LitElement implements Layer { > ${player.score} -
- ${player.gold} -
-
- ${player.maxTroops} -
+ + ${this._viewState === "default" ? html` +
+ ${player.gold} +
+
+ ${player.maxTroops} +
+ ` : html` +
+ ${player.betrayals} +
+
+ ${player.alliances} +
+ `} `, )} - +
+ + +
`; } } diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 44bc83a855..cbfd4efcf0 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -265,28 +265,43 @@ export class PlayerView { } satisfies ColorPalette; } - if (this.team() === null) { - this._territoryColor = colord( - this.cosmetics.color?.color ?? - pattern?.colorPalette?.primaryColor ?? - defaultTerritoryColor.toHex(), - ); - } else { + const isMyPlayer = this.game.myClientID() === this.data.clientID; + const colorMode = isMyPlayer ? userSettings.colorMode() : "random"; + + if (colorMode === "custom" && this.team() === null) { + // Custom color only in solo/FFA — team games always use assigned team color + this._territoryColor = colord(userSettings.customPrimaryColor()); + } else if (colorMode === "team" || this.team() !== null) { + // Force team color (default game behavior) this._territoryColor = defaultTerritoryColor; + } else { + // "random" — normal game logic + if (this.team() === null) { + this._territoryColor = colord( + this.cosmetics.color?.color ?? + pattern?.colorPalette?.primaryColor ?? + defaultTerritoryColor.toHex(), + ); + } else { + this._territoryColor = defaultTerritoryColor; + } } this._structureColors = theme.structureColors(this._territoryColor); - const maybeFocusedBorderColor = - this.game.myClientID() === this.data.clientID + const maybeFocusedBorderColor = isMyPlayer ? theme.focusedBorderColor() : defaultBorderColor; - this._borderColor = new Colord( - pattern?.colorPalette?.secondaryColor ?? - this.cosmetics.color?.color ?? - maybeFocusedBorderColor.toHex(), - ); + if (colorMode === "custom" && this.team() === null) { + this._borderColor = new Colord(userSettings.customSecondaryColor()); + } else { + this._borderColor = new Colord( + pattern?.colorPalette?.secondaryColor ?? + this.cosmetics.color?.color ?? + maybeFocusedBorderColor.toHex(), + ); + } // Pre-compute all border color variants once const baseRgb = this._borderColor.toRgb(); @@ -620,6 +635,9 @@ export class PlayerView { isTraitor(): boolean { return this.data.isTraitor; } + betrayals(): number { + return this.data.betrayals; + } getTraitorRemainingTicks(): number { return Math.max(0, this.data.traitorRemainingTicks ?? 0); } diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index 9faff1a0c6..13b69eac73 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -237,6 +237,42 @@ export class UserSettings { this.setBool(DARK_MODE_KEY, !this.darkMode()); } + // Color mode: "random" = game default, "custom" = user picks, "team" = always use team color + colorMode(): "random" | "custom" | "team" { + const val = this.getString("settings.colorMode", "random"); + if (val === "custom" || val === "team") return val; + return "random"; + } + + setColorMode(mode: "random" | "custom" | "team"): void { + this.setString("settings.colorMode", mode); + } + + // Keep legacy helpers so GameView still compiles + customColorsEnabled(): boolean { + return this.colorMode() === "custom"; + } + + toggleCustomColorsEnabled() { + this.setColorMode(this.colorMode() === "custom" ? "random" : "custom"); + } + + customPrimaryColor(): string { + return this.getString("settings.customPrimaryColor", "#2196f3"); + } + + setCustomPrimaryColor(color: string): void { + this.setString("settings.customPrimaryColor", color); + } + + customSecondaryColor(): string { + return this.getString("settings.customSecondaryColor", "#1565c0"); + } + + setCustomSecondaryColor(color: string): void { + this.setString("settings.customSecondaryColor", color); + } + // For development only. Used for testing patterns, set in the console manually. getDevOnlyPattern(): PlayerPattern | undefined { const data = localStorage.getItem("dev-pattern") ?? undefined;