Features: Team Game Spawn Color Tint (#2303)

## Description:

This PR addresses issue #2302: where there is no obvious information
about the self-player's team during the spawn phase of the game.

Currently, during TEAM games (where there is a set number of teams
defined), the self player's spawn highlight color is white, while all
other players are shown with a team-based spawn highlight color. This
makes it difficult to discern who is a teammate, especially since the
current live version (v0.26.7) uses green/yellow for other players to
depict teammate/other team, respectively.

Technically, the same is true for Duos, Trios, and Quads games, although
this has been addressed separately with PR #2298 by reverting to
green/yellow for teammate/other team players.

This PR changes the color of the self player's breathing spawn highlight
ring to match their assigned team color (with `alpha=0.5` for
transparency). The breathing ring is semi transparent on top of the
existing white static semi-transparent ring, giving the breathing ring a
**tint** of the team color instead of the exact team color.

This allows a player to immediately identify their assigned team and
maintains overall consistency with FFA, Duos, Trios, and Quads game
modes.

See below for example implementation. In this screenshot, the self
player is on the red team as shown by the red tint to the breathing
spawn highlight ring. The screenshot also shows some of the static white
semi-transparent ring around the spawn location. The self player's
teammate is to the top left of the image with a solid red spawn
highlight. The non-player (nation) opponent on the blue team is shown to
the bottom left. In online games, nations are not present in team games.
In single player or private lobby games they are shown as here, without
a spawn color highlight and only a territory color.

<img width="402" height="292" alt="Team Spawn Color Tint"
src="https://github.com/user-attachments/assets/5696a408-a633-4ec8-bf93-c8afa8e2e121"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

GlacialDrift
This commit is contained in:
Mike Harris
2025-10-29 18:39:07 -05:00
committed by GitHub
parent bb5ccbfa2c
commit 4ee3cbc255
9 changed files with 590 additions and 574 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { Colord } from "colord";
import { colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { PlayerID } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -184,7 +184,7 @@ export class RailroadLayer implements Layer {
const recipient = owner.isPlayer() ? owner : null;
const color = recipient
? recipient.borderColor()
: new Colord({ r: 255, g: 255, b: 255, a: 1 });
: colord("rgba(255,255,255,1)");
this.context.fillStyle = color.toRgbString();
this.paintRailRects(this.context, x, y, railType);
}
@@ -256,6 +256,7 @@ export class SpriteFactory {
const tc = owner.territoryColor();
const bc = owner.borderColor();
// Potentially change logic here. Some TC/BC combinations do not provide good color contrast.
const darker = bc.luminance() < tc.luminance() ? bc : tc;
const lighter = bc.luminance() < tc.luminance() ? tc : bc;
+1 -1
View File
@@ -15,7 +15,7 @@ import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
const underConstructionColor = colord({ r: 150, g: 150, b: 150 });
const underConstructionColor = colord("rgb(150,150,150)");
// Base radius values and scaling factor for unit borders and territories
const BASE_BORDER_RADIUS = 16.5;
+30 -19
View File
@@ -6,6 +6,7 @@ import {
Cell,
ColoredTeams,
PlayerType,
Team,
UnitType,
} from "../../../core/game/Game";
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
@@ -197,15 +198,13 @@ export class TerritoryLayer implements Layer {
// In Team games, the spawn highlight color becomes that player's team color
// Optionally, this could be broken down to teammate or enemy and simplified to green and red, respectively
const team = human.team();
if (team !== null) {
if (teamColors.includes(team)) {
color = this.theme.teamColor(team);
if (team !== null && teamColors.includes(team)) {
color = this.theme.teamColor(team);
} else {
if (myPlayer.isFriendly(human)) {
color = this.theme.spawnHighlightTeamColor();
} else {
if (myPlayer.isFriendly(human)) {
color = this.theme.spawnHighlightTeamColor();
} else {
color = this.theme.spawnHighlightColor();
}
color = this.theme.spawnHighlightColor();
}
}
}
@@ -239,13 +238,24 @@ export class TerritoryLayer implements Layer {
const radius =
minRad + (maxRad - minRad) * (0.5 + 0.5 * Math.sin(this.borderAnimTime));
const baseColor = this.theme.spawnHighlightSelfColor(); //white
let teamColor: Colord | null = null;
const team: Team | null = focusedPlayer.team();
if (team !== null && Object.values(ColoredTeams).includes(team)) {
teamColor = this.theme.teamColor(team).alpha(0.5);
} else {
teamColor = baseColor;
}
this.drawBreathingRing(
center.x,
center.y,
minRad,
maxRad,
radius,
this.theme.spawnHighlightSelfColor(), // Always draw breathing ring with self spawn highlight color
baseColor, // Always draw white static semi-transparent ring
teamColor, // Pass the breathing ring color. White for FFA, Duos, Trios, Quads. Transparent team color for TEAM games.
);
}
@@ -582,7 +592,8 @@ export class TerritoryLayer implements Layer {
minRad: number,
maxRad: number,
radius: number,
color: Colord,
transparentColor: Colord,
breathingColor: Colord,
) {
const ctx = this.highlightContext;
if (!ctx) return;
@@ -590,17 +601,16 @@ export class TerritoryLayer implements Layer {
// Draw a semi-transparent ring around the starting location
ctx.beginPath();
// Transparency matches the highlight color provided
const transparent = color.toHex() + "00";
const c = color.toHex();
const transparent = transparentColor.alpha(0);
const radGrad = ctx.createRadialGradient(cx, cy, minRad, cx, cy, maxRad);
// Pixels with radius < minRad are transparent
radGrad.addColorStop(0, transparent);
radGrad.addColorStop(0, transparent.toRgbString());
// The ring then starts with solid highlight color
radGrad.addColorStop(0.01, c);
radGrad.addColorStop(0.1, c);
radGrad.addColorStop(0.01, transparentColor.toRgbString());
radGrad.addColorStop(0.1, transparentColor.toRgbString());
// The outer edge of the ring is transparent
radGrad.addColorStop(1, transparent);
radGrad.addColorStop(1, transparent.toRgbString());
// Draw an arc at the max radius and fill with the created radial gradient
ctx.arc(cx, cy, maxRad, 0, Math.PI * 2);
@@ -608,15 +618,16 @@ export class TerritoryLayer implements Layer {
ctx.closePath();
ctx.fill();
const breatheInner = breathingColor.alpha(0);
// Draw a solid ring around the starting location with outer radius = the breathing radius
ctx.beginPath();
const radGrad2 = ctx.createRadialGradient(cx, cy, minRad, cx, cy, radius);
// Pixels with radius < minRad are transparent
radGrad2.addColorStop(0, transparent);
radGrad2.addColorStop(0, breatheInner.toRgbString());
// The ring then starts with solid highlight color
radGrad2.addColorStop(0.01, c);
radGrad2.addColorStop(0.01, breathingColor.toRgbString());
// The ring is solid throughout
radGrad2.addColorStop(1, c);
radGrad2.addColorStop(1, breathingColor.toRgbString());
// Draw an arc at the current breathing radius and fill with the created "gradient"
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
+1 -1
View File
@@ -295,7 +295,7 @@ export class UnitLayer implements Layer {
private handleWarShipEvent(unit: UnitView) {
if (unit.targetUnitId()) {
this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 }));
this.drawSprite(unit, colord("rgb(200,0,0)"));
} else {
this.drawSprite(unit);
}
+6 -2
View File
@@ -1,4 +1,4 @@
import { Colord, extend } from "colord";
import { colord, Colord, extend } from "colord";
import labPlugin from "colord/plugins/lab";
import lchPlugin from "colord/plugins/lch";
import Color from "colorjs.io";
@@ -87,7 +87,11 @@ export class ColorAllocator {
assignTeamColor(team: Team): Colord {
const teamColors = this.getTeamColorVariations(team);
return teamColors[0];
const rgb = teamColors[0].toRgb();
rgb.r = Math.round(rgb.r);
rgb.g = Math.round(rgb.g);
rgb.b = Math.round(rgb.b);
return colord(rgb);
}
assignTeamPlayerColor(team: Team, playerId: string): Colord {
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -17,35 +17,35 @@ export class PastelTheme implements Theme {
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
private background = colord({ r: 60, g: 60, b: 60 });
private shore = colord({ r: 223, g: 187, b: 132 });
private background = colord("rgb(60,60,60)");
private shore = colord("rgb(204,203,158)");
private falloutColors = [
colord({ r: 120, g: 255, b: 71 }), // Original color
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
colord({ r: 110, g: 245, b: 65 }), // Slightly darker
colord({ r: 125, g: 255, b: 75 }), // Warmer tint
colord({ r: 115, g: 250, b: 68 }), // Cooler tint
colord("rgb(120,255,71)"), // Original color
colord("rgb(130,255,85)"), // Slightly lighter
colord("rgb(110,245,65)"), // Slightly darker
colord("rgb(125,255,75)"), // Warmer tint
colord("rgb(115,250,68)"), // Cooler tint
];
private water = colord({ r: 80, g: 76, b: 179 });
private shorelineWater = colord({ r: 100, g: 110, b: 255 });
private water = colord("rgb(70,132,180)");
private shorelineWater = colord("rgb(100,143,255)");
/** Alternate View colors for self, green */
private _selfColor = colord({ r: 0, g: 255, b: 0 });
private _selfColor = colord("rgb(0,255,0)");
/** Alternate View colors for allies, yellow */
private _allyColor = colord({ r: 255, g: 255, b: 0 });
private _allyColor = colord("rgb(255,255,0)");
/** Alternate View colors for neutral, gray */
private _neutralColor = colord({ r: 128, g: 128, b: 128 });
private _neutralColor = colord("rgb(128,128,128)");
/** Alternate View colors for enemies, red */
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
private _enemyColor = colord("rgb(255,0,0)");
/** Default spawn highlight colors for other players in FFA, yellow */
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
private _spawnHighlightColor = colord("rgb(255,213,79)");
/** Added non-default spawn highlight colors for self, full white */
private _spawnHighlightSelfColor = colord({ r: 255, g: 255, b: 255 });
private _spawnHighlightSelfColor = colord("rgb(255,255,255)");
/** Added non-default spawn highlight colors for teammates, green */
private _spawnHighlightTeamColor = colord({ r: 0, g: 255, b: 0 });
private _spawnHighlightTeamColor = colord("rgb(0,255,0)");
/** Added non-default spawn highlight colors for enemies, red */
private _spawnHighlightEnemyColor = colord({ r: 255, g: 0, b: 0 });
private _spawnHighlightEnemyColor = colord("rgb(255,0,0)");
teamColor(team: Team): Colord {
return this.teamColorAllocator.assignTeamColor(team);
@@ -81,7 +81,7 @@ export class PastelTheme implements Theme {
}
focusedBorderColor(): Colord {
return colord({ r: 230, g: 230, b: 230 });
return colord("rgb(230,230,230)");
}
textColor(player: PlayerView): string {
+3 -3
View File
@@ -4,10 +4,10 @@ import { GameMap, TileRef } from "../game/GameMap";
import { PastelTheme } from "./PastelTheme";
export class PastelThemeDark extends PastelTheme {
private darkShore = colord({ r: 134, g: 133, b: 88 });
private darkShore = colord("rgb(134,133,88)");
private darkWater = colord({ r: 14, g: 11, b: 30 });
private darkShorelineWater = colord({ r: 50, g: 50, b: 50 });
private darkWater = colord("rgb(14,11,30)");
private darkShorelineWater = colord("rgb(50,50,50)");
terrainColor(gm: GameMap, tile: TileRef): Colord {
const mag = gm.magnitude(tile);