Skip to content

Add player info on mouseover and add display options #1608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bab0da5
Add player info on mouse overlay and player info display options
scilone Jul 28, 2025
7ef802a
feedbacks + fr trad
scilone Jul 28, 2025
9af7927
refactor: improve mouse event handling in overlays
scilone Jul 29, 2025
5cf7ae2
refactor: update stat definitions to use codes
scilone Jul 29, 2025
406a52e
🌐 i18n: rollback French translations
scilone Jul 31, 2025
74526a7
Merge branch 'main' of https://github.com/openfrontio/OpenFrontIO int…
scilone Jul 31, 2025
689cdb0
Implement player info overlay management
scilone Jul 31, 2025
e706e3f
♻️ refactor: streamline player info management and overlays
scilone Aug 1, 2025
f3d296a
♻️ refactor: enhance event listener management in PlayerInfoOverlay
scilone Aug 1, 2025
6ed45e4
Merge branch 'main' into 1500-cursor-stats-overlay
scilone Aug 1, 2025
b2eeda2
Merge branch 'main' of https://github.com/openfrontio/OpenFrontIO int…
scilone Aug 5, 2025
7ec2d6c
Merge branch 'main' of github.com:openfrontio/OpenFrontIO into 1500-c…
scilone Aug 8, 2025
c07fb59
change default info display mode to overlay
scilone Aug 10, 2025
9316e58
Merge branch 'main' of github.com:openfrontio/OpenFrontIO into 1500-c…
scilone Aug 10, 2025
d0174b8
✨ Add force player info mouse overlay feature
scilone Aug 10, 2025
dec3861
✨ Update player info toggle keybinding to ControlLeft
scilone Aug 10, 2025
78f5f7a
✨ Add force player info mouse overlay feature
scilone Aug 11, 2025
46d842b
Merge branch 'main' of github.com:openfrontio/OpenFrontIO into 1500-c…
scilone Aug 11, 2025
ab689dd
🐛 Fix player info overlay initialization and positioning issues
scilone Aug 11, 2025
0307cda
Merge branch 'main' into 1500-cursor-stats-overlay
drillskibo Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ resources/.DS_Store
.env*
.DS_Store
.clinic/
.idea
CLAUDE.md
12 changes: 11 additions & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@
"view_options": "View Options",
"toggle_view": "Toggle View",
"toggle_view_desc": "Alternate view (terrain/countries)",
"force_player_info_mouse_overlay": "Force player info on cursor",
"force_player_info_mouse_overlay_desc": "Hold to display player information overlay at cursor position",
"attack_ratio_controls": "Attack Ratio Controls",
"attack_ratio_up": "Increase Attack Ratio",
"attack_ratio_up_desc": "Increase attack ratio by 10%",
Expand Down Expand Up @@ -341,7 +343,15 @@
"terrain_enabled": "Terrain view enabled",
"terrain_disabled": "Terrain view disabled",
"exit_game_label": "Exit Game",
"exit_game_info": "Return to main menu"
"exit_game_info": "Return to main menu",
"info_display_mode_label": "Information Display",
"info_display_mode_desc": "Choose where player information is displayed",
"info_display_overlay_only": "Top right corner only",
"info_display_mousehud_only": "Near cursor only",
"info_display_both": "Top right corner + cursor",
"info_display_overlay": "Overlay",
"info_display_mousehud": "Cursor",
"info_display_both_short": "Both"
},
"chat": {
"title": "Quick Chat",
Expand Down
20 changes: 20 additions & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export class AlternateViewEvent implements GameEvent {
constructor(public readonly alternateView: boolean) {}
}

export class ForcePlayerInfoMouseOverlayEvent implements GameEvent {
constructor(public readonly forcePlayerInfoMouseOverlay: boolean) {}
}

export class CloseViewEvent implements GameEvent {}

export class RefreshGraphicsEvent implements GameEvent {}
Expand Down Expand Up @@ -128,6 +132,7 @@ export class InputHandler {
private pointerDown: boolean = false;

private alternateView = false;
private forcePlayerInfoMouseOverlay = false;

private moveInterval: NodeJS.Timeout | null = null;
private activeKeys = new Set<string>();
Expand All @@ -146,6 +151,7 @@ export class InputHandler {
initialize() {
this.keybinds = {
toggleView: "Space",
forcePlayerInfoMouseOverlay: "ControlRight",
centerCamera: "KeyC",
moveUp: "KeyW",
moveDown: "KeyS",
Expand Down Expand Up @@ -262,6 +268,14 @@ export class InputHandler {
}
}

if (e.code === this.keybinds.forcePlayerInfoMouseOverlay) {
e.preventDefault();
if (!this.forcePlayerInfoMouseOverlay) {
this.forcePlayerInfoMouseOverlay = true;
this.eventBus.emit(new ForcePlayerInfoMouseOverlayEvent(true));
}
}

if (e.code === "Escape") {
e.preventDefault();
this.eventBus.emit(new CloseViewEvent());
Expand Down Expand Up @@ -300,6 +314,12 @@ export class InputHandler {
this.eventBus.emit(new AlternateViewEvent(false));
}

if (e.code === this.keybinds.forcePlayerInfoMouseOverlay) {
e.preventDefault();
this.forcePlayerInfoMouseOverlay = false;
this.eventBus.emit(new ForcePlayerInfoMouseOverlayEvent(false));
}

if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey) {
e.preventDefault();
this.eventBus.emit(new RefreshGraphicsEvent());
Expand Down
11 changes: 11 additions & 0 deletions src/client/UserSettingModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,17 @@ export class UserSettingModal extends LitElement {
@change=${this.handleKeybindChange}
></setting-keybind>

<setting-keybind
action="forcePlayerInfoMouseOverlay"
label=${translateText("user_setting.force_player_info_mouse_overlay")}
description=${translateText(
"user_setting.force_player_info_mouse_overlay_desc",
)}
defaultKey="ControlRight"
.value=${this.keybinds["forcePlayerInfoMouseOverlay"] ?? ""}
@change=${this.handleKeybindChange}
></setting-keybind>

<div class="text-center text-white text-base font-semibold mt-5 mb-2">
${translateText("user_setting.attack_ratio_controls")}
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
import { PlayerInfoMouseOverlay } from "./layers/PlayerInfoMouseOverlay";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer";
Expand Down Expand Up @@ -128,6 +129,18 @@ export function createRenderer(
playerInfo.eventBus = eventBus;
playerInfo.transform = transformHandler;
playerInfo.game = game;
playerInfo.userSettings = userSettings;

const mouseHUD = document.querySelector(
"mouse-hud",
) as PlayerInfoMouseOverlay;
if (!(mouseHUD instanceof PlayerInfoMouseOverlay)) {
console.error("mouse hud not found");
}
mouseHUD.eventBus = eventBus;
mouseHUD.transform = transformHandler;
mouseHUD.game = game;
mouseHUD.userSettings = userSettings;

const winModal = document.querySelector("win-modal") as WinModal;
if (!(winModal instanceof WinModal)) {
Expand Down Expand Up @@ -258,6 +271,7 @@ export function createRenderer(
gameRightSidebar,
controlPanel,
playerInfo,
mouseHUD,
winModal,
replayPanel,
settingsModal,
Expand Down
120 changes: 120 additions & 0 deletions src/client/graphics/layers/PlayerInfoManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { MouseMoveEvent } from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import {
HoverInfo,
OVERLAY_CONFIG,
PlayerInfoService,
} from "./PlayerInfoService";

export class PlayerInfoManager {
private static instance: PlayerInfoManager | null = null;
private readonly playerInfoService: PlayerInfoService;
private eventBus: EventBus;
private lastDataUpdate = 0;
private currentHoverInfo: HoverInfo | null = null;
private currentMousePosition = { x: 0, y: 0 };
private dataSubscribers: Set<(hoverInfo: HoverInfo) => void> = new Set();
private mouseSubscribers: Set<(x: number, y: number) => void> = new Set();
private mouseMoveCallback: ((event: MouseMoveEvent) => void) | null = null;
private isActive = false;

private constructor(
game: GameView,
transform: TransformHandler,
eventBus: EventBus,
) {
this.playerInfoService = new PlayerInfoService(game, transform);
this.eventBus = eventBus;
}

static getInstance(
game: GameView,
transform: TransformHandler,
eventBus: EventBus,
): PlayerInfoManager {
PlayerInfoManager.instance ??= new PlayerInfoManager(
game,
transform,
eventBus,
);
return PlayerInfoManager.instance;
}

init() {
if (this.isActive) return;

this.mouseMoveCallback = (e: MouseMoveEvent) => this.onMouseMove(e);
this.eventBus.on(MouseMoveEvent, this.mouseMoveCallback);
this.isActive = true;
}

destroy() {
if (this.mouseMoveCallback) {
this.eventBus.off(MouseMoveEvent, this.mouseMoveCallback);
this.mouseMoveCallback = null;
}
this.dataSubscribers.clear();
this.mouseSubscribers.clear();
this.isActive = false;
PlayerInfoManager.instance = null;
}

subscribeToData(callback: (hoverInfo: HoverInfo) => void) {
this.dataSubscribers.add(callback);
if (this.currentHoverInfo) {
callback(this.currentHoverInfo);
}
}

unsubscribeFromData(callback: (hoverInfo: HoverInfo) => void) {
this.dataSubscribers.delete(callback);
}

subscribeToMouse(callback: (x: number, y: number) => void) {
this.mouseSubscribers.add(callback);
callback(this.currentMousePosition.x, this.currentMousePosition.y);
}

unsubscribeFromMouse(callback: (x: number, y: number) => void) {
this.mouseSubscribers.delete(callback);
}

private async onMouseMove(event: MouseMoveEvent) {
this.currentMousePosition.x = event.x;
this.currentMousePosition.y = event.y;

this.notifyMouseSubscribers();

const now = Date.now();
if (now - this.lastDataUpdate < OVERLAY_CONFIG.updateThrottleMs) {
return;
}
this.lastDataUpdate = now;

this.currentHoverInfo = await this.playerInfoService.getHoverInfo(
event.x,
event.y,
);
this.notifyDataSubscribers();
}

private notifyDataSubscribers() {
if (this.currentHoverInfo) {
this.dataSubscribers.forEach((callback) =>
callback(this.currentHoverInfo!),
);
}
}

private notifyMouseSubscribers() {
this.mouseSubscribers.forEach((callback) =>
callback(this.currentMousePosition.x, this.currentMousePosition.y),
);
}

getPlayerInfoService(): PlayerInfoService {
return this.playerInfoService;
}
}
Loading
Loading