diff --git a/resources/lang/en.json b/resources/lang/en.json index 1d7b1625b0..dc31a82332 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -570,6 +570,11 @@ "attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)", "territory_patterns_label": "🏳️ Territory Skins", "territory_patterns_desc": "Choose whether to display territory skin designs in game", + "territory_highlight_label": "Territory Highlight", + "territory_highlight_desc": "Highlight the territory of the player under your cursor", + "territory_highlight_always": "Always", + "territory_highlight_on_key": "On Key Press (default H, customizable via key settings)", + "territory_highlight_never": "Never", "coordinate_grid_label": "Coordinate Grid", "coordinate_grid_desc": "Toggle the alphanumeric grid overlay", "attacking_troops_overlay_label": "Attacking Troops Overlay", @@ -584,6 +589,9 @@ "view_options": "View Options", "toggle_view": "Toggle View", "toggle_view_desc": "Alternate view (terrain/countries)", + "highlight_territory": "Highlight Territory", + "highlight_territory_desc": "Hold to highlight the hovered player's territory", + "highlight_territory_disabled": "Enable \"On Key Press\" mode in basic settings to configure this keybind", "build_controls": "Build Controls", "build_city": "Build City", "build_city_desc": "Build a City under your cursor.", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 1782d43d25..5944b5b79e 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -144,6 +144,10 @@ export class ToggleCoordinateGridEvent implements GameEvent { constructor(public readonly enabled: boolean) {} } +export class TerritoryHighlightKeyEvent implements GameEvent { + constructor(public readonly active: boolean) {} +} + export class TickMetricsEvent implements GameEvent { constructor( public readonly tickExecutionDuration?: number, @@ -165,6 +169,7 @@ export class InputHandler { private pointerDown: boolean = false; private alternateView = false; + private highlightKeyHeld = false; private moveInterval: NodeJS.Timeout | null = null; private activeKeys = new Set(); @@ -214,6 +219,10 @@ export class InputHandler { this.alternateView = false; this.eventBus.emit(new AlternateViewEvent(false)); } + if (this.highlightKeyHeld) { + this.highlightKeyHeld = false; + this.eventBus.emit(new TerritoryHighlightKeyEvent(false)); + } this.pointerDown = false; this.pointers.clear(); }); @@ -289,6 +298,15 @@ export class InputHandler { } } + if ( + this.keybindMatchesEvent(e, this.keybinds.highlightTerritory) && + !this.highlightKeyHeld + ) { + e.preventDefault(); + this.highlightKeyHeld = true; + this.eventBus.emit(new TerritoryHighlightKeyEvent(true)); + } + if ( this.keybindMatchesEvent(e, this.keybinds.coordinateGrid) && !e.repeat @@ -384,6 +402,12 @@ export class InputHandler { this.eventBus.emit(new AlternateViewEvent(false)); } + if (this.keybindMatchesEvent(e, this.keybinds.highlightTerritory)) { + e.preventDefault(); + this.highlightKeyHeld = false; + this.eventBus.emit(new TerritoryHighlightKeyEvent(false)); + } + const resetKey = this.keybinds.resetGfx ?? "KeyR"; if (e.code === resetKey && this.isAltKeyHeld(e)) { e.preventDefault(); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 77a0f33d12..9f566a391b 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -309,6 +309,14 @@ export class UserSettingModal extends BaseModal { ); } + private changeTerritoryHighlight(e: CustomEvent<{ value: number | string }>) { + const value = e.detail?.value; + if (value !== "never" && value !== "always" && value !== "onKeyPress") + return; + this.userSettings.setTerritoryHighlight(value); + this.requestUpdate(); + } + private togglePerformanceOverlay() { this.userSettings.togglePerformanceOverlay(); } @@ -429,6 +437,31 @@ export class UserSettingModal extends BaseModal { @change=${this.handleKeybindChange} > + ${this.userSettings.territoryHighlight() === "onKeyPress" + ? html`` + : html`
+
+ +
+ ${translateText("user_setting.highlight_territory_disabled")} +
+
+
`} +

@@ -833,6 +866,28 @@ export class UserSettingModal extends BaseModal { @change=${this.toggleTerritoryPatterns} > + + + { + const value = e.detail; + if (value === "always" || value === "onKeyPress") { + this.highlightMode = value; + } else { + this.highlightMode = "never"; + } + this.updateHighlightedTerritory(); + }) as EventListener; private lastDragTime = 0; private nodrawDragDuration = 200; private lastMousePosition: { x: number; y: number } | null = null; @@ -63,6 +81,7 @@ export class TerritoryLayer implements Layer { ) { this.theme = game.config().theme(); this.cachedTerritoryPatternsEnabled = undefined; + this.highlightMode = new UserSettings().territoryHighlight(); } shouldTransform(): boolean { @@ -328,11 +347,22 @@ export class TerritoryLayer implements Layer { this.eventBus.on(MouseOverEvent, (e) => this.onMouseOver(e)); this.eventBus.on(AlternateViewEvent, (e) => { this.alternativeView = e.alternateView; + this.updateHighlightedTerritory(); + }); + this.eventBus.on(TerritoryHighlightKeyEvent, (e) => { + this.highlightKeyHeld = e.active; + this.updateHighlightedTerritory(); }); this.eventBus.on(DragEvent, (e) => { // TODO: consider re-enabling this on mobile or low end devices for smoother dragging. // this.lastDragTime = Date.now(); }); + + globalThis.addEventListener?.( + `${USER_SETTINGS_CHANGED_EVENT}:${TERRITORY_HIGHLIGHT_KEY}`, + this.onHighlightSettingChanged, + ); + this.redraw(); } @@ -342,7 +372,17 @@ export class TerritoryLayer implements Layer { } private updateHighlightedTerritory() { - if (!this.alternativeView) { + const shouldHighlight = + this.highlightMode === "always" || + (this.highlightMode === "onKeyPress" && this.highlightKeyHeld) || + this.alternativeView; + + if (!shouldHighlight) { + if (this.highlightedTerritory) { + const prev = this.highlightedTerritory; + this.highlightedTerritory = null; + this.updateHighlightAlpha(prev, null); + } return; } @@ -368,17 +408,31 @@ export class TerritoryLayer implements Layer { } if (previousTerritory?.id() !== this.highlightedTerritory?.id()) { - const territories: PlayerView[] = []; - if (previousTerritory) { - territories.push(previousTerritory); - } - if (this.highlightedTerritory) { - territories.push(this.highlightedTerritory); - } - this.redrawBorder(...territories); + this.updateHighlightAlpha(previousTerritory, this.highlightedTerritory); } } + private updateHighlightAlpha( + oldPlayer: PlayerView | null, + newPlayer: PlayerView | null, + ) { + const data = this.imageData.data; + const oldSmallID = oldPlayer?.smallID() ?? -1; + const newSmallID = newPlayer?.smallID() ?? -1; + this.game.forEachTile((tile) => { + const offset = tile * 4; + const alpha = data[offset + 3]; + // Only update non-border territory fill tiles (alpha 150 or 230) + if (alpha !== 150 && alpha !== 230) return; + const tileOwner = this.game.ownerID(tile); + if (tileOwner === newSmallID) { + data[offset + 3] = 230; + } else if (alpha === 230 && tileOwner === oldSmallID) { + data[offset + 3] = 150; + } + }); + } + private getTerritoryAtCell(cell: { x: number; y: number }) { const tile = this.game.ref(cell.x, cell.y); if (!tile) { @@ -559,15 +613,12 @@ export class TerritoryLayer implements Layer { return; } const owner = this.game.owner(tile) as PlayerView; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const isHighlighted = this.highlightedTerritory && - this.highlightedTerritory.id() === owner.id(); + this.highlightedTerritory.smallID() === owner.smallID(); const myPlayer = this.game.myPlayer(); if (this.game.isBorder(tile)) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const playerIsFocused = owner && this.game.focusedPlayer() === owner; if (myPlayer) { const alternativeColor = this.alternateViewColor(owner); this.paintTile(this.alternativeImageData, tile, alternativeColor, 255); @@ -589,7 +640,8 @@ export class TerritoryLayer implements Layer { // Alternative view only shows borders. this.clearAlternativeTile(tile); - this.paintTile(this.imageData, tile, owner.territoryColor(tile), 150); + const alpha = isHighlighted ? 230 : 150; + this.paintTile(this.imageData, tile, owner.territoryColor(tile), alpha); } } diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index 158d108942..ff72e4138f 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -34,6 +34,7 @@ export function getDefaultKeybinds(isMac: boolean): Record { pauseGame: "KeyP", gameSpeedUp: "Period", gameSpeedDown: "Comma", + highlightTerritory: "KeyH", }; } @@ -43,6 +44,8 @@ export const FLAG_KEY = "flag"; export const COLOR_KEY = "settings.territoryColor"; export const DARK_MODE_KEY = "settings.darkMode"; export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay"; +export type TerritoryHighlightMode = "always" | "onKeyPress" | "never"; +export const TERRITORY_HIGHLIGHT_KEY = "settings.territoryHighlight"; export const KEYBINDS_KEY = "settings.keybinds"; export class UserSettings { @@ -221,6 +224,16 @@ export class UserSettings { this.setBool("settings.territoryPatterns", !this.territoryPatterns()); } + territoryHighlight(): TerritoryHighlightMode { + const value = this.getString(TERRITORY_HIGHLIGHT_KEY, "never"); + if (value === "always" || value === "onKeyPress") return value; + return "never"; + } + + setTerritoryHighlight(value: TerritoryHighlightMode) { + this.setString(TERRITORY_HIGHLIGHT_KEY, value); + } + toggleDarkMode() { this.setBool(DARK_MODE_KEY, !this.darkMode()); }