From 0e3200d647e03fe8a3f4039280caa791bb1f6b66 Mon Sep 17 00:00:00 2001
From: scamiv <6170744+scamiv@users.noreply.github.com>
Date: Sun, 21 Dec 2025 23:57:42 +0100
Subject: [PATCH] Tint territory borders based on player relationships. (#2439)
## Description:
Add visual indicators to territory borders that reflect diplomatic
relationships between players, making it easier to identify relations at
a glance.
### Problem Statement
Currently, players must check diplomatic status through other UI
elements. There's no immediate visual feedback on the map showing which
borders represent embargo or friendly relationships.
### Benefits
1. **Improved Gameplay Clarity**: Quickly identify diplomatic
relationships without opening menus
2. **Strategic Awareness**: Visual feedback helps make tactical
decisions about border defense
### Proposed Solution
Tint territory borders based on neighbor relationships: embargo red,
friendly green
### Implementation Details
- Border variants are based on this._borderColor (theme/style handling
unchanged) computed in the constructor and stored in userSettings
UnitView
- Apply tinting to checkerboard for defended borders
- borderColor() checks all neighbors to determine the worst relationship
status
- Embargos take priority.
## 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
---
src/core/game/GameView.ts | 132 +++++++++++++++++++++++++++++++++++---
1 file changed, 123 insertions(+), 9 deletions(-)
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index d1d2fb83e..a35416364 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -41,6 +41,10 @@ import { UserSettings } from "./UserSettings";
const userSettings: UserSettings = new UserSettings();
+const FRIENDLY_TINT_TARGET = { r: 0, g: 255, b: 0, a: 1 };
+const EMBARGO_TINT_TARGET = { r: 255, g: 0, b: 0, a: 1 };
+const BORDER_TINT_RATIO = 0.35;
+
export class UnitView {
public _wasUpdated = true;
public lastPos: TileRef[] = [];
@@ -186,7 +190,14 @@ export class PlayerView {
private _borderColor: Colord;
// Update here to include structure light and dark colors
private _structureColors: { light: Colord; dark: Colord };
- private _defendedBorderColors: { light: Colord; dark: Colord };
+
+ // Pre-computed border color variants
+ private _borderColorNeutral: Colord;
+ private _borderColorFriendly: Colord;
+ private _borderColorEmbargo: Colord;
+ private _borderColorDefendedNeutral: { light: Colord; dark: Colord };
+ private _borderColorDefendedFriendly: { light: Colord; dark: Colord };
+ private _borderColorDefendedEmbargo: { light: Colord; dark: Colord };
constructor(
private game: GameView,
@@ -249,10 +260,57 @@ export class PlayerView {
maybeFocusedBorderColor.toHex(),
);
- this._defendedBorderColors = this.game
- .config()
- .theme()
- .defendedBorderColors(this._borderColor);
+ // Pre-compute all border color variants once
+ const theme = this.game.config().theme();
+ const baseRgb = this._borderColor.toRgb();
+
+ // Neutral is just the base color
+ this._borderColorNeutral = this._borderColor;
+
+ // Compute friendly tint
+ this._borderColorFriendly = colord({
+ r: Math.round(
+ baseRgb.r * (1 - BORDER_TINT_RATIO) +
+ FRIENDLY_TINT_TARGET.r * BORDER_TINT_RATIO,
+ ),
+ g: Math.round(
+ baseRgb.g * (1 - BORDER_TINT_RATIO) +
+ FRIENDLY_TINT_TARGET.g * BORDER_TINT_RATIO,
+ ),
+ b: Math.round(
+ baseRgb.b * (1 - BORDER_TINT_RATIO) +
+ FRIENDLY_TINT_TARGET.b * BORDER_TINT_RATIO,
+ ),
+ a: baseRgb.a,
+ });
+
+ // Compute embargo tint
+ this._borderColorEmbargo = colord({
+ r: Math.round(
+ baseRgb.r * (1 - BORDER_TINT_RATIO) +
+ EMBARGO_TINT_TARGET.r * BORDER_TINT_RATIO,
+ ),
+ g: Math.round(
+ baseRgb.g * (1 - BORDER_TINT_RATIO) +
+ EMBARGO_TINT_TARGET.g * BORDER_TINT_RATIO,
+ ),
+ b: Math.round(
+ baseRgb.b * (1 - BORDER_TINT_RATIO) +
+ EMBARGO_TINT_TARGET.b * BORDER_TINT_RATIO,
+ ),
+ a: baseRgb.a,
+ });
+
+ // Pre-compute defended variants
+ this._borderColorDefendedNeutral = theme.defendedBorderColors(
+ this._borderColorNeutral,
+ );
+ this._borderColorDefendedFriendly = theme.defendedBorderColors(
+ this._borderColorFriendly,
+ );
+ this._borderColorDefendedEmbargo = theme.defendedBorderColors(
+ this._borderColorEmbargo,
+ );
this.decoder =
pattern === undefined
@@ -275,18 +333,74 @@ export class PlayerView {
return this._structureColors;
}
+ /**
+ * Border color for a tile:
+ * - Tints by neighbor relations (embargo → red, friendly → green, else neutral).
+ * - If defended, applies theme checkerboard to the tinted color.
+ */
borderColor(tile?: TileRef, isDefended: boolean = false): Colord {
- if (tile === undefined || !isDefended) {
+ if (tile === undefined) {
return this._borderColor;
}
+ const { hasEmbargo, hasFriendly } = this.borderRelationFlags(tile);
+
+ let baseColor: Colord;
+ let defendedColors: { light: Colord; dark: Colord };
+
+ if (hasEmbargo) {
+ baseColor = this._borderColorEmbargo;
+ defendedColors = this._borderColorDefendedEmbargo;
+ } else if (hasFriendly) {
+ baseColor = this._borderColorFriendly;
+ defendedColors = this._borderColorDefendedFriendly;
+ } else {
+ baseColor = this._borderColorNeutral;
+ defendedColors = this._borderColorDefendedNeutral;
+ }
+
+ if (!isDefended) {
+ return baseColor;
+ }
+
const x = this.game.x(tile);
const y = this.game.y(tile);
const lightTile =
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
- return lightTile
- ? this._defendedBorderColors.light
- : this._defendedBorderColors.dark;
+ return lightTile ? defendedColors.light : defendedColors.dark;
+ }
+
+ /**
+ * Border relation flags for a tile, used by both CPU and WebGL renderers.
+ */
+ borderRelationFlags(tile: TileRef): {
+ hasEmbargo: boolean;
+ hasFriendly: boolean;
+ } {
+ const mySmallID = this.smallID();
+ let hasEmbargo = false;
+ let hasFriendly = false;
+
+ for (const n of this.game.neighbors(tile)) {
+ if (!this.game.hasOwner(n)) {
+ continue;
+ }
+
+ const otherOwner = this.game.owner(n);
+ if (!otherOwner.isPlayer() || otherOwner.smallID() === mySmallID) {
+ continue;
+ }
+
+ if (this.hasEmbargo(otherOwner)) {
+ hasEmbargo = true;
+ break;
+ }
+
+ if (this.isFriendly(otherOwner) || otherOwner.isFriendly(this)) {
+ hasFriendly = true;
+ }
+ }
+ return { hasEmbargo, hasFriendly };
}
async actions(tile?: TileRef): Promise {