Focused player border highlight (#304)

Tried to implement feature to outline player under mouse cursor.
Intention is to improve gameplay and provide a way to estimate players
territories for strategic planning / avoid attacking wrong players with
confusingly similar colors.

When you stop cursor at the player - his borders will be drawn in white
color.
To focus on other player - click or move cursor to other player. 
To hide outline - click on empty space (water, land). 
"Focus" UI feature also triggers outline for the target player (easier
to see who exactly is that you are looking at).


![1](https://github.com/user-attachments/assets/f7bda876-4b20-456c-9463-d2cfbb53ad5a)

![2](https://github.com/user-attachments/assets/f5cd6d12-fd52-4890-b3ee-caba9ff17753)
![FUN 2025-03-20 23 06
23](https://github.com/user-attachments/assets/f0e6e343-9442-4d89-9b7a-4bd01c6e00d0)
![FUN 2025-03-20 23 07
51](https://github.com/user-attachments/assets/2cc748e1-bc66-4834-82ad-22a4184a3006)


Done via getting player under mouse cursor, setting it as global
focusedPlayer and painting borders in white color.
Tried to maintain minimal changes and utilize existing rendering queue.
Also added hover delays to avoid excessive redraws and provide better
experience. Redraws only happens when focusedPlayer changes - one time
for old focused player to clean outline and one time for new focused
player.
This commit is contained in:
tropptr-torrptrop
2025-03-29 23:41:28 +02:00
committed by GitHub
parent 849d612314
commit 72016f3dd4
12 changed files with 214 additions and 8 deletions
+71 -2
View File
@@ -3,11 +3,13 @@ import {
GameMapType,
Difficulty,
GameType,
Unit,
UnitType,
TeamName,
} from "../core/game/Game";
import { EventBus } from "../core/EventBus";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent } from "./InputHandler";
import { InputHandler, MouseUpEvent, MouseMoveEvent } from "./InputHandler";
import {
ClientID,
GameConfig,
@@ -34,12 +36,21 @@ import { WorkerClient } from "../core/worker/WorkerClient";
import { consolex, initRemoteSender } from "../core/Consolex";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
import { GameView, PlayerView } from "../core/game/GameView";
import { GameView, PlayerView, UnitView } from "../core/game/GameView";
import { GameUpdateViewData } from "../core/game/GameUpdates";
import { UserSettings } from "../core/game/UserSettings";
import { LocalPersistantStats } from "./LocalPersistantStats";
import { createGameRecord } from "../core/Util";
import { getPersistentIDFromCookie } from "./Main";
import { TileRef } from "../core/game/GameMap";
function distSortUnitWorld(tile: TileRef, game: GameView) {
return (a: Unit | UnitView, b: Unit | UnitView) => {
return (
game.euclideanDist(tile, a.tile()) - game.euclideanDist(tile, b.tile())
);
};
}
export interface LobbyConfig {
serverConfig: ServerConfig;
@@ -152,6 +163,10 @@ export class ClientGameRunner {
private turnsSeen = 0;
private hasJoined = false;
private lastMousePosition: { x: number; y: number } | null = null;
private mouseHoverTimer: number | null = null;
private readonly HOVER_DELAY = 200;
constructor(
private lobby: LobbyConfig,
private eventBus: EventBus,
@@ -199,6 +214,7 @@ export class ClientGameRunner {
consolex.log("starting client game");
this.isActive = true;
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e));
this.eventBus.on(MouseMoveEvent, (e) => this.onMouseMove(e));
this.renderer.initialize();
this.input.initialize();
@@ -328,8 +344,61 @@ export class ClientGameRunner {
),
);
}
const owner = this.gameView.owner(tile);
if (owner.isPlayer()) {
this.gameView.setFocusedPlayer(owner as PlayerView);
} else {
this.gameView.setFocusedPlayer(null);
}
});
}
private onMouseMove(event: MouseMoveEvent) {
this.lastMousePosition = { x: event.x, y: event.y };
this.clearHoverTimer();
this.mouseHoverTimer = window.setTimeout(() => {
this.checkTileUnderCursor();
}, this.HOVER_DELAY);
}
private clearHoverTimer() {
if (this.mouseHoverTimer !== null) {
clearTimeout(this.mouseHoverTimer);
this.mouseHoverTimer = null;
}
}
private checkTileUnderCursor() {
if (!this.lastMousePosition || !this.renderer.transformHandler) return;
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
this.lastMousePosition.x,
this.lastMousePosition.y,
);
if (!cell || !this.gameView.isValidCoord(cell.x, cell.y)) {
return;
}
const tile = this.gameView.ref(cell.x, cell.y);
if (this.gameView.isLand(tile)) {
const owner = this.gameView.owner(tile);
if (owner.isPlayer()) {
this.gameView.setFocusedPlayer(owner as PlayerView);
}
} else {
const units = this.gameView
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
.filter((u) => this.gameView.euclideanDist(tile, u.tile()) < 50)
.sort(distSortUnitWorld(tile, this.gameView));
if (units.length > 0) {
this.gameView.setFocusedPlayer(units[0].owner() as PlayerView);
}
}
}
}
function showErrorModal(
+1
View File
@@ -142,6 +142,7 @@ export class TransformHandler {
}
onGoToPlayer(event: GoToPlayerEvent) {
this.game.setFocusedPlayer(event.player);
this.clearTarget();
this.target = new Cell(
event.player.nameLocation().x,
+29 -2
View File
@@ -50,6 +50,8 @@ export class TerritoryLayer implements Layer {
private refreshRate = 50;
private lastRefresh = 0;
private lastFocusedPlayer: PlayerView | null = null;
constructor(
private game: GameView,
private eventBus: EventBus,
@@ -83,6 +85,17 @@ export class TerritoryLayer implements Layer {
}
});
const focusedPlayer = this.game.focusedPlayer();
if (focusedPlayer !== this.lastFocusedPlayer) {
if (this.lastFocusedPlayer) {
this.enqueuePlayerBorder(this.lastFocusedPlayer);
}
if (focusedPlayer) {
this.enqueuePlayerBorder(focusedPlayer);
}
this.lastFocusedPlayer = focusedPlayer;
}
if (!this.game.inSpawnPhase()) {
return;
}
@@ -239,6 +252,7 @@ export class TerritoryLayer implements Layer {
}
const owner = this.game.owner(tile) as PlayerView;
if (this.game.isBorder(tile)) {
const playerIsFocused = owner && this.game.focusedPlayer() == owner;
if (
this.game
.nearbyUnits(
@@ -248,17 +262,23 @@ export class TerritoryLayer implements Layer {
)
.filter((u) => u.unit.owner() == owner).length > 0
) {
const useDefendedBorderColor = playerIsFocused
? this.theme.focusedDefendedBorderColor()
: this.theme.defendedBorderColor(owner);
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.defendedBorderColor(owner),
useDefendedBorderColor,
255,
);
} else {
const useBorderColor = playerIsFocused
? this.theme.focusedBorderColor()
: this.theme.borderColor(owner);
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.borderColor(owner),
useBorderColor,
255,
);
}
@@ -294,6 +314,13 @@ export class TerritoryLayer implements Layer {
});
}
async enqueuePlayerBorder(player: PlayerView) {
const playerBorderTiles = await player.borderTiles();
playerBorderTiles.borderTiles.forEach((tile: TileRef) => {
this.enqueueTile(tile);
});
}
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
this.clearCell(cell);
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();