diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 263999c61..bcc0285a8 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -1,218 +1,204 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { Game } from "../core/game/Game"; - export class MouseUpEvent implements GameEvent { - constructor( - public readonly x: number, - public readonly y: number, - ) { } + constructor(public readonly x: number, public readonly y: number) {} } export class MouseDownEvent implements GameEvent { - constructor( - public readonly x: number, - public readonly y: number, - ) { } + constructor(public readonly x: number, public readonly y: number) {} } export class MouseMoveEvent implements GameEvent { - constructor( - public readonly x: number, - public readonly y: number, - ) { } + constructor(public readonly x: number, public readonly y: number) {} } export class ContextMenuEvent implements GameEvent { - constructor( - public readonly x: number, - public readonly y: number, - ) { } + constructor(public readonly x: number, public readonly y: number) {} } export class ZoomEvent implements GameEvent { - constructor( - public readonly x: number, - public readonly y: number, - public readonly delta: number - ) { } + constructor( + public readonly x: number, + public readonly y: number, + public readonly delta: number + ) {} } export class DragEvent implements GameEvent { - constructor( - public readonly deltaX: number, - public readonly deltaY: number, - ) { } + constructor(public readonly deltaX: number, public readonly deltaY: number) {} } export class AlternateViewEvent implements GameEvent { - constructor(public readonly alternateView: boolean) { } + constructor(public readonly alternateView: boolean) {} } -export class RefreshGraphicsEvent implements GameEvent { } +export class RefreshGraphicsEvent implements GameEvent {} export class ShowBuildMenuEvent implements GameEvent { - constructor(public readonly x: number, public readonly y: number) { } + constructor(public readonly x: number, public readonly y: number) {} } - - export class InputHandler { + private lastPointerX: number = 0; + private lastPointerY: number = 0; - private lastPointerX: number = 0; - private lastPointerY: number = 0; + private lastPointerDownX: number = 0; + private lastPointerDownY: number = 0; - private lastPointerDownX: number = 0; - private lastPointerDownY: number = 0; + private pointers: Map = new Map(); - private pointers: Map = new Map(); + private lastPinchDistance: number = 0; - private lastPinchDistance: number = 0; + private pointerDown: boolean = false; - private pointerDown: boolean = false + private alternateView = false; - private alternateView = false + constructor(private canvas: HTMLCanvasElement, private eventBus: EventBus) {} - constructor(private canvas: HTMLCanvasElement, private eventBus: EventBus) { } + initialize() { + this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); + this.canvas.addEventListener("pointerup", (e) => this.onPointerUp(e)); + this.canvas.addEventListener("wheel", (e) => this.onScroll(e), { + passive: false, + }); + this.canvas.addEventListener("pointermove", this.onPointerMove.bind(this)); + this.canvas.addEventListener("contextmenu", (e: MouseEvent) => { + this.onContextMenu(e); + }); + this.canvas.addEventListener("mousemove", (e) => { + if (e.movementX == 0 && e.movementY == 0) { + return; + } + this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY)); + }); + this.pointers.clear(); - initialize() { - this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); - this.canvas.addEventListener("pointerup", (e) => this.onPointerUp(e)); - this.canvas.addEventListener("wheel", (e) => this.onScroll(e), { passive: false }); - this.canvas.addEventListener('pointermove', this.onPointerMove.bind(this)); - this.canvas.addEventListener('contextmenu', (e: MouseEvent) => { - this.onContextMenu(e); - }); - this.canvas.addEventListener('pointermove', (e) => { - this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY)) - }); - this.pointers.clear() + window.addEventListener("keydown", (e) => { + if (e.code === "Space") { + e.preventDefault(); // Prevent page scrolling + if (!this.alternateView) { + this.alternateView = true; + this.eventBus.emit(new AlternateViewEvent(true)); + } + } + }); + window.addEventListener("keyup", (e) => { + if (e.code === "Space") { + e.preventDefault(); + this.alternateView = false; + this.eventBus.emit(new AlternateViewEvent(false)); + } + if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey) { + e.preventDefault(); + this.eventBus.emit(new RefreshGraphicsEvent()); + } + }); + } - window.addEventListener('keydown', (e) => { - if (e.code === 'Space') { - e.preventDefault(); // Prevent page scrolling - if (!this.alternateView) { - this.alternateView = true - this.eventBus.emit(new AlternateViewEvent(true)) - } - } - }); - - window.addEventListener('keyup', (e) => { - if (e.code === 'Space') { - e.preventDefault(); - this.alternateView = false - this.eventBus.emit(new AlternateViewEvent(false)) - } - if (e.key.toLowerCase() === 'r' && e.altKey && !e.ctrlKey) { - e.preventDefault(); - this.eventBus.emit(new RefreshGraphicsEvent()) - } - }); + private onPointerDown(event: PointerEvent) { + if (event.button > 0) { + return; } - private onPointerDown(event: PointerEvent) { + this.pointerDown = true; + this.pointers.set(event.pointerId, event); - if (event.button > 0) { - return - } + if (this.pointers.size === 1) { + this.lastPointerX = event.clientX; + this.lastPointerY = event.clientY; + this.lastPointerDownX = event.clientX; + this.lastPointerDownY = event.clientY; - this.pointerDown = true - this.pointers.set(event.pointerId, event); + this.eventBus.emit(new MouseDownEvent(event.clientX, event.clientY)); + } else if (this.pointers.size === 2) { + this.lastPinchDistance = this.getPinchDistance(); + } + } - if (this.pointers.size === 1) { - this.lastPointerX = event.clientX; - this.lastPointerY = event.clientY; + onPointerUp(event: PointerEvent) { + if (event.button > 0) { + return; + } + this.pointerDown = false; + this.pointers.clear(); - this.lastPointerDownX = event.clientX - this.lastPointerDownY = event.clientY - - this.eventBus.emit(new MouseDownEvent(event.clientX, event.clientY)); - } else if (this.pointers.size === 2) { - this.lastPinchDistance = this.getPinchDistance(); - } + if (event.ctrlKey) { + this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY)); + return; } - onPointerUp(event: PointerEvent) { - if (event.button > 0) { - return - } - this.pointerDown = false - this.pointers.clear() + const dist = + Math.abs(event.x - this.lastPointerDownX) + + Math.abs(event.y - this.lastPointerDownY); + if (dist < 10) { + if (event.pointerType == "touch") { + event.preventDefault(); + console.log("firing context menu event"); + this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)); + } else { + this.eventBus.emit(new MouseUpEvent(event.x, event.y)); + } + } + } - if (event.ctrlKey) { - this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY)) - return - } + private onScroll(event: WheelEvent) { + this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY)); + } - const dist = Math.abs(event.x - this.lastPointerDownX) + Math.abs(event.y - this.lastPointerDownY); - if (dist < 10) { - if (event.pointerType == "touch") { - event.preventDefault() - console.log('firing context menu event') - this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)) - } else { - this.eventBus.emit(new MouseUpEvent(event.x, event.y)) - } - } + private onPointerMove(event: PointerEvent) { + if (event.button > 0) { + return; } - private onScroll(event: WheelEvent) { - this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY)) + this.pointers.set(event.pointerId, event); + + if (!this.pointerDown) { + return; } - private onPointerMove(event: PointerEvent) { - if (event.button > 0) { - return - } + if (this.pointers.size === 1) { + const deltaX = event.clientX - this.lastPointerX; + const deltaY = event.clientY - this.lastPointerY; + this.eventBus.emit(new DragEvent(deltaX, deltaY)); - this.pointers.set(event.pointerId, event); + this.lastPointerX = event.clientX; + this.lastPointerY = event.clientY; + } else if (this.pointers.size === 2) { + const currentPinchDistance = this.getPinchDistance(); + const pinchDelta = currentPinchDistance - this.lastPinchDistance; - if (!this.pointerDown) { - return - } - - if (this.pointers.size === 1) { - const deltaX = event.clientX - this.lastPointerX; - const deltaY = event.clientY - this.lastPointerY; - - this.eventBus.emit(new DragEvent(deltaX, deltaY)); - - this.lastPointerX = event.clientX; - this.lastPointerY = event.clientY; - } else if (this.pointers.size === 2) { - const currentPinchDistance = this.getPinchDistance(); - const pinchDelta = currentPinchDistance - this.lastPinchDistance; - - if (Math.abs(pinchDelta) > 1) { // Threshold to avoid tiny zoom adjustments - const zoomCenter = this.getPinchCenter(); - this.eventBus.emit(new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2)); - this.lastPinchDistance = currentPinchDistance; - } - } + if (Math.abs(pinchDelta) > 1) { + // Threshold to avoid tiny zoom adjustments + const zoomCenter = this.getPinchCenter(); + this.eventBus.emit( + new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2) + ); + this.lastPinchDistance = currentPinchDistance; + } } + } - private onContextMenu(event: MouseEvent) { - event.preventDefault() - this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)) - } + private onContextMenu(event: MouseEvent) { + event.preventDefault(); + this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)); + } - private getPinchDistance(): number { - const pointerEvents = Array.from(this.pointers.values()); - const dx = pointerEvents[0].clientX - pointerEvents[1].clientX; - const dy = pointerEvents[0].clientY - pointerEvents[1].clientY; - return Math.sqrt(dx * dx + dy * dy); - } + private getPinchDistance(): number { + const pointerEvents = Array.from(this.pointers.values()); + const dx = pointerEvents[0].clientX - pointerEvents[1].clientX; + const dy = pointerEvents[0].clientY - pointerEvents[1].clientY; + return Math.sqrt(dx * dx + dy * dy); + } - private getPinchCenter(): { x: number, y: number } { - const pointerEvents = Array.from(this.pointers.values()); - return { - x: (pointerEvents[0].clientX + pointerEvents[1].clientX) / 2, - y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2 - }; - } - -} \ No newline at end of file + private getPinchCenter(): { x: number; y: number } { + const pointerEvents = Array.from(this.pointers.values()); + return { + x: (pointerEvents[0].clientX + pointerEvents[1].clientX) / 2, + y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2, + }; + } +} diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 55b0f5a84..e06984837 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -118,7 +118,8 @@ export function createRenderer( clientID, emojiTable as EmojiTable, buildMenu, - uiState + uiState, + playerInfo ), new SpawnTimer(game, transformHandler), leaderboard, diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 250de942e..ece80be11 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -1,205 +1,245 @@ -import { LitElement, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { Layer } from './Layer'; -import { Game, GameType, Player, PlayerProfile, PlayerType, Relation, Unit, UnitType } from '../../../core/game/Game'; -import { ClientID } from '../../../core/Schemas'; -import { EventBus } from '../../../core/EventBus'; -import { TransformHandler } from '../TransformHandler'; -import { MouseMoveEvent } from '../../InputHandler'; -import { GameView, PlayerView, UnitView } from '../../../core/game/GameView'; -import { TileRef } from '../../../core/game/GameMap'; -import { renderNumber, renderTroops } from '../../Utils'; +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { Layer } from "./Layer"; +import { + Game, + GameType, + Player, + PlayerProfile, + PlayerType, + Relation, + Unit, + UnitType, +} from "../../../core/game/Game"; +import { ClientID } from "../../../core/Schemas"; +import { EventBus } from "../../../core/EventBus"; +import { TransformHandler } from "../TransformHandler"; +import { MouseMoveEvent } from "../../InputHandler"; +import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; +import { TileRef } from "../../../core/game/GameMap"; +import { renderNumber, renderTroops } from "../../Utils"; -function euclideanDistWorld(coord: { x: number, y: number }, tileRef: TileRef, game: GameView): number { - const x = game.x(tileRef); - const y = game.y(tileRef); - const dx = coord.x - x; - const dy = coord.y - y; - return Math.sqrt(dx * dx + dy * dy); +function euclideanDistWorld( + coord: { x: number; y: number }, + tileRef: TileRef, + game: GameView +): number { + const x = game.x(tileRef); + const y = game.y(tileRef); + const dx = coord.x - x; + const dy = coord.y - y; + return Math.sqrt(dx * dx + dy * dy); } -function distSortUnitWorld(coord: { x: number, y: number }, game: GameView) { - return (a: Unit | UnitView, b: Unit | UnitView) => { - const distA = euclideanDistWorld(coord, a.tile(), game); - const distB = euclideanDistWorld(coord, b.tile(), game); - return distA - distB; - }; +function distSortUnitWorld(coord: { x: number; y: number }, game: GameView) { + return (a: Unit | UnitView, b: Unit | UnitView) => { + const distA = euclideanDistWorld(coord, a.tile(), game); + const distB = euclideanDistWorld(coord, b.tile(), game); + return distA - distB; + }; } -@customElement('player-info-overlay') +@customElement("player-info-overlay") export class PlayerInfoOverlay extends LitElement implements Layer { - @property({ type: Object }) - public game!: GameView; + @property({ type: Object }) + public game!: GameView; - @property({ type: String }) - public clientID!: ClientID; + @property({ type: String }) + public clientID!: ClientID; - @property({ type: Object }) - public eventBus!: EventBus; + @property({ type: Object }) + public eventBus!: EventBus; - @property({ type: Object }) - public transform!: TransformHandler; + @property({ type: Object }) + public transform!: TransformHandler; - @state() - private player: PlayerView | null = null; + @state() + private player: PlayerView | null = null; - @state() - private playerProfile: PlayerProfile | null = null; + @state() + private playerProfile: PlayerProfile | null = null; - @state() - private unit: UnitView | null = null; + @state() + private unit: UnitView | null = null; - @state() - private _isInfoVisible: boolean = false; + @state() + private _isInfoVisible: boolean = false; - private _isActive = false; + private _isActive = false; - private lastMouseUpdate = 0; + private lastMouseUpdate = 0; - init() { - this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => this.onMouseEvent(e)); - this._isActive = true; + init() { + this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => + this.onMouseEvent(e) + ); + this._isActive = true; + } + + private onMouseEvent(event: MouseMoveEvent) { + const now = Date.now(); + if (now - this.lastMouseUpdate < 100) { + return; + } + this.lastMouseUpdate = now; + this.maybeShow(event.x, event.y); + } + + public hide() { + this.setVisible(false); + this.unit = null; + this.player = null; + } + + public maybeShow(x: number, y: number) { + this.hide(); + const worldCoord = this.transform.screenToWorldCoordinates(x, y); + if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) { + return; } - private onMouseEvent(event: MouseMoveEvent) { - const now = Date.now() - if (now - this.lastMouseUpdate < 100) { - return - } - this.lastMouseUpdate = now + const tile = this.game.ref(worldCoord.x, worldCoord.y); + if (!tile) return; - this.setVisible(false); - this.unit = null; - this.player = null; + const owner = this.game.owner(tile); - const worldCoord = this.transform.screenToWorldCoordinates(event.x, event.y); - if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) { - return; - } + if (owner && owner.isPlayer()) { + this.player = owner as PlayerView; + this.player.profile().then((p) => { + this.playerProfile = p; + }); + this.setVisible(true); + } else if (!this.game.isLand(tile)) { + const units = this.game + .units(UnitType.Destroyer, UnitType.Battleship, UnitType.TradeShip) + .filter((u) => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50) + .sort(distSortUnitWorld(worldCoord, this.game)); - const tile = this.game.ref(worldCoord.x, worldCoord.y); - if (!tile) return; + if (units.length > 0) { + this.unit = units[0]; + this.setVisible(true); + } + } + } - const owner = this.game.owner(tile); + tick() { + this.requestUpdate(); + } - if (owner && owner.isPlayer()) { - this.player = owner as PlayerView; - this.player.profile().then(p => { - this.playerProfile = p; - }); - this.setVisible(true); - } else if (!this.game.isLand(tile)) { - const units = this.game.units(UnitType.Destroyer, UnitType.Battleship, UnitType.TradeShip) - .filter(u => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50) - .sort(distSortUnitWorld(worldCoord, this.game)); + renderLayer(context: CanvasRenderingContext2D) { + // Implementation for Layer interface + } - if (units.length > 0) { - this.unit = units[0]; - this.setVisible(true); - } - } + shouldTransform(): boolean { + return false; + } + + setVisible(visible: boolean) { + this._isInfoVisible = visible; + this.requestUpdate(); + } + + private myPlayer(): PlayerView | null { + if (!this.game) { + return null; + } + return this.game.playerByClientID(this.clientID); + } + + private getRelationClass(relation: Relation): string { + switch (relation) { + case Relation.Hostile: + return "text-red-500"; + case Relation.Distrustful: + return "text-red-300"; + case Relation.Neutral: + return "text-white"; + case Relation.Friendly: + return "text-green-500"; + default: + return "text-white"; + } + } + + private renderPlayerInfo(player: PlayerView) { + const myPlayer = this.myPlayer(); + const isAlly = myPlayer?.isAlliedWith(player); + let relationHtml = null; + + if (player.type() == PlayerType.FakeHuman && myPlayer != null) { + const relation = + this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral; + const relationClass = this.getRelationClass(relation); + const relationName = Relation[relation]; + + relationHtml = html` +
+ Attitude: ${relationName} +
+ `; } - tick() { - this.requestUpdate(); + return html` +
+
+ ${player.name()} +
+
+ Troops: ${renderTroops(player.troops())} +
+
+ Gold: ${renderNumber(player.gold())} +
+ ${relationHtml} +
+ `; + } + + private renderUnitInfo(unit: UnitView) { + const isAlly = + (unit.owner() == this.myPlayer() || + this.myPlayer()?.isAlliedWith(unit.owner())) ?? + false; + + return html` +
+
+ ${unit.owner().name()} +
+
+
${unit.type()}
+ ${unit.hasHealth() + ? html` +
Health: ${unit.health()}
+ ` + : ""} +
+
+ `; + } + + render() { + if (!this._isActive) { + return html``; } - renderLayer(context: CanvasRenderingContext2D) { - // Implementation for Layer interface - } + const containerClasses = this._isInfoVisible + ? "opacity-100 visible" + : "opacity-0 invisible pointer-events-none"; - shouldTransform(): boolean { - return false; - } + return html` +
+
+ ${this.player != null ? this.renderPlayerInfo(this.player) : ""} + ${this.unit != null ? this.renderUnitInfo(this.unit) : ""} +
+
+ `; + } - setVisible(visible: boolean) { - this._isInfoVisible = visible; - this.requestUpdate(); - } - - private myPlayer(): PlayerView | null { - if (!this.game) { - return null; - } - return this.game.playerByClientID(this.clientID); - } - - private getRelationClass(relation: Relation): string { - switch (relation) { - case Relation.Hostile: - return 'text-red-500'; - case Relation.Distrustful: - return 'text-red-300'; - case Relation.Neutral: - return 'text-white'; - case Relation.Friendly: - return 'text-green-500'; - default: - return 'text-white'; - } - } - - private renderPlayerInfo(player: PlayerView) { - const myPlayer = this.myPlayer(); - const isAlly = myPlayer?.isAlliedWith(player); - let relationHtml = null; - - if (player.type() == PlayerType.FakeHuman && myPlayer != null) { - const relation = this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral; - const relationClass = this.getRelationClass(relation); - const relationName = Relation[relation]; - - relationHtml = html` -
- Attitude: ${relationName} -
- `; - } - - return html` -
-
${player.name()}
-
Troops: ${renderTroops(player.troops())}
-
Gold: ${renderNumber(player.gold())}
- ${relationHtml} -
- `; - } - - private renderUnitInfo(unit: UnitView) { - const isAlly = (unit.owner() == this.myPlayer() || this.myPlayer()?.isAlliedWith(unit.owner())) ?? false; - - return html` -
-
${unit.owner().name()}
-
-
${unit.type()}
- ${unit.hasHealth() ? html` -
Health: ${unit.health()}
- ` : ''} -
-
- `; - } - - render() { - if (!this._isActive) { - return html``; - } - - const containerClasses = this._isInfoVisible ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'; - - return html` -
-
- ${this.player != null ? this.renderPlayerInfo(this.player) : ''} - ${this.unit != null ? this.renderUnitInfo(this.unit) : ''} -
-
- `; - } - - createRenderRoot() { - return this; // Disable shadow DOM to allow Tailwind styles - } -} \ No newline at end of file + createRenderRoot() { + return this; // Disable shadow DOM to allow Tailwind styles + } +} diff --git a/src/client/graphics/layers/radial/RadialMenu.ts b/src/client/graphics/layers/radial/RadialMenu.ts index a99e4af72..ec6e76b78 100644 --- a/src/client/graphics/layers/radial/RadialMenu.ts +++ b/src/client/graphics/layers/radial/RadialMenu.ts @@ -1,413 +1,503 @@ import { EventBus } from "../../../../core/EventBus"; -import { AllPlayers, Cell, Game, Player, PlayerActions, } from "../../../../core/game/Game"; +import { + AllPlayers, + Cell, + Game, + Player, + PlayerActions, +} from "../../../../core/game/Game"; import { ClientID } from "../../../../core/Schemas"; -import { ContextMenuEvent, MouseUpEvent, ShowBuildMenuEvent } from "../../../InputHandler"; -import { SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent } from "../../../Transport"; +import { + ContextMenuEvent, + MouseUpEvent, + ShowBuildMenuEvent, +} from "../../../InputHandler"; +import { + SendAllianceRequestIntentEvent, + SendAttackIntentEvent, + SendBoatAttackIntentEvent, + SendBreakAllianceIntentEvent, + SendDonateIntentEvent, + SendEmojiIntentEvent, + SendSpawnIntentEvent, + SendTargetPlayerIntentEvent, +} from "../../../Transport"; import { TransformHandler } from "../../TransformHandler"; import { Layer } from "../Layer"; -import * as d3 from 'd3'; -import traitorIcon from '../../../../../resources/images/TraitorIconWhite.png'; -import allianceIcon from '../../../../../resources/images/AllianceIconWhite.png'; -import boatIcon from '../../../../../resources/images/BoatIconWhite.png'; -import swordIcon from '../../../../../resources/images/SwordIconWhite.png'; -import targetIcon from '../../../../../resources/images/TargetIconWhite.png'; -import emojiIcon from '../../../../../resources/images/EmojiIconWhite.png'; -import disabledIcon from '../../../../../resources/images/DisabledIcon.png'; -import donateIcon from '../../../../../resources/images/DonateIconWhite.png'; -import buildIcon from '../../../../../resources/images/BuildIconWhite.svg'; +import * as d3 from "d3"; +import traitorIcon from "../../../../../resources/images/TraitorIconWhite.png"; +import allianceIcon from "../../../../../resources/images/AllianceIconWhite.png"; +import boatIcon from "../../../../../resources/images/BoatIconWhite.png"; +import swordIcon from "../../../../../resources/images/SwordIconWhite.png"; +import targetIcon from "../../../../../resources/images/TargetIconWhite.png"; +import emojiIcon from "../../../../../resources/images/EmojiIconWhite.png"; +import disabledIcon from "../../../../../resources/images/DisabledIcon.png"; +import donateIcon from "../../../../../resources/images/DonateIconWhite.png"; +import buildIcon from "../../../../../resources/images/BuildIconWhite.svg"; import { EmojiTable } from "./EmojiTable"; import { UIState } from "../../UIState"; import { BuildMenu } from "./BuildMenu"; import { consolex } from "../../../../core/Consolex"; import { GameView, PlayerView } from "../../../../core/game/GameView"; import { TileRef } from "../../../../core/game/GameMap"; - +import { PlayerInfoOverlay } from "../PlayerInfoOverlay"; enum Slot { - Alliance, - Boat, - Target, - Emoji, - Build, + Alliance, + Boat, + Target, + Emoji, + Build, } export class RadialMenu implements Layer { - private clickedCell: Cell | null = null + private clickedCell: Cell | null = null; - private menuElement: d3.Selection; - private isVisible: boolean = false; - private readonly menuItems = new Map([ - [Slot.Alliance, { name: "alliance", disabled: true, action: () => { }, color: null, icon: null }], - [Slot.Boat, { name: "boat", disabled: true, action: () => { }, color: null, icon: null }], - [Slot.Target, { name: "target", disabled: true, action: () => { } }], - [Slot.Emoji, { name: "emoji", disabled: true, action: () => { } }], - [Slot.Build, { name: "build", disabled: true, action: () => { } }], - ]); + private menuElement: d3.Selection; + private isVisible: boolean = false; + private readonly menuItems = new Map([ + [ + Slot.Alliance, + { + name: "alliance", + disabled: true, + action: () => {}, + color: null, + icon: null, + }, + ], + [ + Slot.Boat, + { + name: "boat", + disabled: true, + action: () => {}, + color: null, + icon: null, + }, + ], + [Slot.Target, { name: "target", disabled: true, action: () => {} }], + [Slot.Emoji, { name: "emoji", disabled: true, action: () => {} }], + [Slot.Build, { name: "build", disabled: true, action: () => {} }], + ]); - private readonly menuSize = 190; - private readonly centerButtonSize = 30; - private readonly iconSize = 32; - private readonly centerIconSize = 48; - private readonly disabledColor = d3.rgb(128, 128, 128).toString(); + private readonly menuSize = 190; + private readonly centerButtonSize = 30; + private readonly iconSize = 32; + private readonly centerIconSize = 48; + private readonly disabledColor = d3.rgb(128, 128, 128).toString(); - private isCenterButtonEnabled = false + private isCenterButtonEnabled = false; - constructor( - private eventBus: EventBus, - private g: GameView, - private transformHandler: TransformHandler, - private clientID: ClientID, - private emojiTable: EmojiTable, - private buildMenu: BuildMenu, - private uiState: UIState - ) { } + constructor( + private eventBus: EventBus, + private g: GameView, + private transformHandler: TransformHandler, + private clientID: ClientID, + private emojiTable: EmojiTable, + private buildMenu: BuildMenu, + private uiState: UIState, + private playerInfoOverlay: PlayerInfoOverlay + ) {} - init() { - this.eventBus.on(ContextMenuEvent, e => this.onContextMenu(e)) - this.eventBus.on(MouseUpEvent, e => this.onPointerUp(e)) - this.eventBus.on(ShowBuildMenuEvent, e => { - const clickedCell = this.transformHandler.screenToWorldCoordinates(e.x, e.y) - if (clickedCell == null) { - return - } - if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) { - return - } - const p = this.g.playerByClientID(this.clientID) - if (p == null) { - return - } - this.buildMenu.showMenu(p, clickedCell) - }) - this.createMenuElement(); + init() { + this.eventBus.on(ContextMenuEvent, (e) => this.onContextMenu(e)); + this.eventBus.on(MouseUpEvent, (e) => this.onPointerUp(e)); + this.eventBus.on(ShowBuildMenuEvent, (e) => { + const clickedCell = this.transformHandler.screenToWorldCoordinates( + e.x, + e.y + ); + if (clickedCell == null) { + return; + } + if (!this.g.isValidCoord(clickedCell.x, clickedCell.y)) { + return; + } + const p = this.g.playerByClientID(this.clientID); + if (p == null) { + return; + } + this.buildMenu.showMenu(p, clickedCell); + }); + this.createMenuElement(); + } + + private createMenuElement() { + this.menuElement = d3 + .select(document.body) + .append("div") + .style("position", "fixed") + .style("display", "none") + .style("z-index", "9999") + .style("touch-action", "none"); + + const svg = this.menuElement + .append("svg") + .attr("width", this.menuSize) + .attr("height", this.menuSize) + .append("g") + .attr( + "transform", + `translate(${this.menuSize / 2},${this.menuSize / 2})` + ); + + const pie = d3 + .pie() + .value(() => 1) + .padAngle(0.03); + + const arc = d3 + .arc() + .innerRadius(this.centerButtonSize + 5) + .outerRadius(this.menuSize / 2 - 10); + + const arcs = svg + .selectAll("path") + .data(pie(Array.from(this.menuItems.values()))) + .enter() + .append("g"); + + arcs + .append("path") + .attr("d", arc) + .attr("fill", (d) => + d.data.disabled ? this.disabledColor : d.data.color + ) + .attr("stroke", "#ffffff") + .attr("stroke-width", "2") + .style("cursor", (d) => (d.data.disabled ? "not-allowed" : "pointer")) + .style("opacity", (d) => (d.data.disabled ? 0.5 : 1)) + .attr("data-name", (d) => d.data.name) + .on("mouseover", function (event, d) { + if (!d.data.disabled) { + d3.select(this) + .transition() + .duration(200) + .attr("transform", "scale(1.05)") + .attr("filter", "url(#glow)"); + } + }) + .on("mouseout", function (event, d) { + if (!d.data.disabled) { + d3.select(this) + .transition() + .duration(200) + .attr("transform", "scale(1)") + .attr("filter", null); + } + }) + .on("click", (event, d) => { + if (!d.data.disabled) { + d.data.action(); + this.hideRadialMenu(); + } + }) + .on("touchstart", (event, d) => { + event.preventDefault(); + if (!d.data.disabled) { + d.data.action(); + this.hideRadialMenu(); + } + }); + arcs + .append("image") + .attr("xlink:href", (d) => d.data.icon) + .attr("width", this.iconSize) + .attr("height", this.iconSize) + .attr("x", (d) => arc.centroid(d)[0] - this.iconSize / 2) + .attr("y", (d) => arc.centroid(d)[1] - this.iconSize / 2) + .style("pointer-events", "none") + .attr("data-name", (d) => d.data.name); + + // Add glow filter + const defs = svg.append("defs"); + const filter = defs.append("filter").attr("id", "glow"); + filter + .append("feGaussianBlur") + .attr("stdDeviation", "3") + .attr("result", "coloredBlur"); + const feMerge = filter.append("feMerge"); + feMerge.append("feMergeNode").attr("in", "coloredBlur"); + feMerge.append("feMergeNode").attr("in", "SourceGraphic"); + + const centerButton = svg.append("g").attr("class", "center-button"); + + centerButton + .append("circle") + .attr("class", "center-button-hitbox") + .attr("r", this.centerButtonSize) + .attr("fill", "transparent") + .style("cursor", "pointer") + .on("click", () => this.handleCenterButtonClick()) + .on("touchstart", (event: Event) => { + event.preventDefault(); + this.handleCenterButtonClick(); + }) + .on("mouseover", () => this.onCenterButtonHover(true)) + .on("mouseout", () => this.onCenterButtonHover(false)); + + centerButton + .append("circle") + .attr("class", "center-button-visible") + .attr("r", this.centerButtonSize) + .attr("fill", "#2c3e50") + .style("pointer-events", "none"); + + // Replace text with sword icon + centerButton + .append("image") + .attr("class", "center-button-icon") + .attr("xlink:href", swordIcon) + .attr("width", this.centerIconSize) + .attr("height", this.centerIconSize) + .attr("x", -this.centerIconSize / 2) + .attr("y", -this.centerIconSize / 2) + .style("pointer-events", "none"); + } + + tick() { + // Update logic if needed + } + + renderLayer(context: CanvasRenderingContext2D) { + // No need to render anything on the canvas + } + + shouldTransform(): boolean { + return false; + } + + private onContextMenu(event: ContextMenuEvent) { + if (this.buildMenu.isVisible) { + this.buildMenu.hideMenu(); + return; + } + if (this.isVisible) { + this.hideRadialMenu(); + return; + } else { + this.showRadialMenu(event.x, event.y); + } + this.enableCenterButton(false); + for (const item of this.menuItems.values()) { + item.disabled = true; + this.updateMenuItemState(item); } - private createMenuElement() { - this.menuElement = d3.select(document.body) - .append('div') - .style('position', 'fixed') - .style('display', 'none') - .style('z-index', '9999') - .style('touch-action', 'none'); + this.clickedCell = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y + ); + if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) { + return; + } + const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y); - const svg = this.menuElement.append('svg') - .attr('width', this.menuSize) - .attr('height', this.menuSize) - .append('g') - .attr('transform', `translate(${this.menuSize / 2},${this.menuSize / 2})`); - - const pie = d3.pie() - .value(() => 1) - .padAngle(0.03); - - const arc = d3.arc() - .innerRadius(this.centerButtonSize + 5) - .outerRadius(this.menuSize / 2 - 10); - - const arcs = svg.selectAll('path') - .data(pie(Array.from(this.menuItems.values()))) - .enter() - .append('g'); - - arcs.append('path') - .attr('d', arc) - .attr('fill', d => d.data.disabled ? this.disabledColor : d.data.color) - .attr('stroke', '#ffffff') - .attr('stroke-width', '2') - .style('cursor', d => d.data.disabled ? 'not-allowed' : 'pointer') - .style('opacity', d => d.data.disabled ? 0.5 : 1) - .attr('data-name', d => d.data.name) - .on('mouseover', function (event, d) { - if (!d.data.disabled) { - d3.select(this) - .transition() - .duration(200) - .attr('transform', 'scale(1.05)') - .attr('filter', 'url(#glow)'); - } - }) - .on('mouseout', function (event, d) { - if (!d.data.disabled) { - d3.select(this) - .transition() - .duration(200) - .attr('transform', 'scale(1)') - .attr('filter', null); - } - }) - .on('click', (event, d) => { - if (!d.data.disabled) { - d.data.action(); - this.hideRadialMenu(); - } - }) - .on('touchstart', (event, d) => { - event.preventDefault(); - if (!d.data.disabled) { - d.data.action(); - this.hideRadialMenu(); - } - }); - arcs.append('image') - .attr('xlink:href', d => d.data.icon) - .attr('width', this.iconSize) - .attr('height', this.iconSize) - .attr('x', d => arc.centroid(d)[0] - this.iconSize / 2) - .attr('y', d => arc.centroid(d)[1] - this.iconSize / 2) - .style('pointer-events', 'none') - .attr('data-name', d => d.data.name); - - // Add glow filter - const defs = svg.append('defs'); - const filter = defs.append('filter') - .attr('id', 'glow'); - filter.append('feGaussianBlur') - .attr('stdDeviation', '3') - .attr('result', 'coloredBlur'); - const feMerge = filter.append('feMerge'); - feMerge.append('feMergeNode') - .attr('in', 'coloredBlur'); - feMerge.append('feMergeNode') - .attr('in', 'SourceGraphic'); - - const centerButton = svg.append('g') - .attr('class', 'center-button'); - - centerButton.append('circle') - .attr('class', 'center-button-hitbox') - .attr('r', this.centerButtonSize) - .attr('fill', 'transparent') - .style('cursor', 'pointer') - .on('click', () => this.handleCenterButtonClick()) - .on('touchstart', (event: Event) => { - event.preventDefault(); - this.handleCenterButtonClick(); - }) - .on('mouseover', () => this.onCenterButtonHover(true)) - .on('mouseout', () => this.onCenterButtonHover(false)); - - centerButton.append('circle') - .attr('class', 'center-button-visible') - .attr('r', this.centerButtonSize) - .attr('fill', '#2c3e50') - .style('pointer-events', 'none'); - - // Replace text with sword icon - centerButton.append('image') - .attr('class', 'center-button-icon') - .attr('xlink:href', swordIcon) - .attr('width', this.centerIconSize) - .attr('height', this.centerIconSize) - .attr('x', -this.centerIconSize / 2) - .attr('y', -this.centerIconSize / 2) - .style('pointer-events', 'none'); + if (this.g.inSpawnPhase()) { + if (this.g.isLand(tile) && !this.g.hasOwner(tile)) { + this.enableCenterButton(true); + } + return; } - tick() { - // Update logic if needed + const myPlayer = this.g + .playerViews() + .find((p) => p.clientID() == this.clientID); + if (!myPlayer) { + consolex.warn("my player not found"); + return; + } + myPlayer.actions(tile).then((actions) => { + this.handlePlayerActions(myPlayer, actions, tile); + }); + } + + private handlePlayerActions( + myPlayer: PlayerView, + actions: PlayerActions, + tile: TileRef + ) { + this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => { + this.buildMenu.showMenu(myPlayer, this.clickedCell); + }); + const canSendEmojiToPlayer = + this.g.hasOwner(tile) && + this.g.ownerID(tile) != myPlayer.smallID() && + actions.interaction?.canSendEmoji; + const canSendEmojiToAllPlayers = + this.g.ownerID(tile) == myPlayer.smallID() && + actions.canSendEmojiAllPlayers; + if (canSendEmojiToPlayer || canSendEmojiToAllPlayers) { + this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { + const target = + this.g.owner(tile) == myPlayer + ? AllPlayers + : (this.g.owner(tile) as PlayerView); + this.emojiTable.onEmojiClicked = (emoji: string) => { + this.emojiTable.hideTable(); + this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)); + }; + this.emojiTable.showTable(); + }); } - renderLayer(context: CanvasRenderingContext2D) { - // No need to render anything on the canvas + if (actions.canBoat) { + this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { + this.eventBus.emit( + new SendBoatAttackIntentEvent( + this.g.owner(tile).id(), + this.clickedCell, + this.uiState.attackRatio * myPlayer.troops() + ) + ); + }); + } + if (actions.canAttack) { + this.enableCenterButton(true); } - shouldTransform(): boolean { - return false; + if (!this.g.hasOwner(tile)) { + return; + } + const other = this.g.owner(tile) as PlayerView; + + if (actions?.interaction.canDonate) { + this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { + this.eventBus.emit(new SendDonateIntentEvent(myPlayer, other, null)); + }); } - private onContextMenu(event: ContextMenuEvent) { - if (this.buildMenu.isVisible) { - this.buildMenu.hideMenu() - return - } - if (this.isVisible) { - this.hideRadialMenu() - return - } else { - this.showRadialMenu(event.x, event.y); - } - this.enableCenterButton(false) - for (const item of this.menuItems.values()) { - item.disabled = true - this.updateMenuItemState(item) - } - - this.clickedCell = this.transformHandler.screenToWorldCoordinates(event.x, event.y) - if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) { - return - } - const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y) - - if (this.g.inSpawnPhase()) { - if (this.g.isLand(tile) && !this.g.hasOwner(tile)) { - this.enableCenterButton(true) - } - return - } - - const myPlayer = this.g.playerViews().find(p => p.clientID() == this.clientID) - if (!myPlayer) { - consolex.warn('my player not found') - return - } - myPlayer.actions(tile).then(actions => { - this.handlePlayerActions(myPlayer, actions, tile) - }) + if (actions?.interaction.canTarget) { + this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { + this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id())); + }); } - private handlePlayerActions(myPlayer: PlayerView, actions: PlayerActions, tile: TileRef) { - this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => { - this.buildMenu.showMenu(myPlayer, this.clickedCell) - }) - const canSendEmojiToPlayer = this.g.hasOwner(tile) && this.g.ownerID(tile) != myPlayer.smallID() && actions.interaction?.canSendEmoji - const canSendEmojiToAllPlayers = this.g.ownerID(tile) == myPlayer.smallID() && actions.canSendEmojiAllPlayers - if (canSendEmojiToPlayer || canSendEmojiToAllPlayers) { - this.activateMenuElement(Slot.Emoji, "#00a6a4", emojiIcon, () => { - const target = this.g.owner(tile) == myPlayer ? AllPlayers : (this.g.owner(tile) as PlayerView) - this.emojiTable.onEmojiClicked = (emoji: string) => { - this.emojiTable.hideTable() - this.eventBus.emit(new SendEmojiIntentEvent(target, emoji)) - } - this.emojiTable.showTable() - }) - } - - if (actions.canBoat) { - this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { - this.eventBus.emit( - new SendBoatAttackIntentEvent( - this.g.owner(tile).id(), - this.clickedCell, - this.uiState.attackRatio * myPlayer.troops() - ) - ) - }) - } - if (actions.canAttack) { - this.enableCenterButton(true) - } - - if (!this.g.hasOwner(tile)) { - return - } - const other = this.g.owner(tile) as PlayerView - - - if (actions?.interaction.canDonate) { - this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => { - this.eventBus.emit( - new SendDonateIntentEvent(myPlayer, other, null) - ) - }) - } - - if (actions?.interaction.canTarget) { - this.activateMenuElement(Slot.Target, "#c74848", targetIcon, () => { - this.eventBus.emit( - new SendTargetPlayerIntentEvent(other.id()) - ) - }) - } - - if (actions?.interaction.canSendAllianceRequest) { - this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { - this.eventBus.emit( - new SendAllianceRequestIntentEvent(myPlayer, other) - ) - }) - } - - if (actions?.interaction.canBreakAlliance) { - this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { - this.eventBus.emit( - new SendBreakAllianceIntentEvent(myPlayer, other) - ) - }) - } + if (actions?.interaction.canSendAllianceRequest) { + this.activateMenuElement(Slot.Alliance, "#53ac75", allianceIcon, () => { + this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other)); + }); } - private onPointerUp(event: MouseUpEvent) { - this.hideRadialMenu() - this.emojiTable.hideTable() - this.buildMenu.hideMenu() + if (actions?.interaction.canBreakAlliance) { + this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => { + this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other)); + }); } + } - private showRadialMenu(x: number, y: number) { - // Delay so center button isn't clicked immediately on press. - setTimeout(() => { - this.menuElement - .style('left', `${x - this.menuSize / 2}px`) - .style('top', `${y - this.menuSize / 2}px`) - .style('display', 'block'); - this.isVisible = true; - }, 50) + private onPointerUp(event: MouseUpEvent) { + this.hideRadialMenu(); + this.emojiTable.hideTable(); + this.buildMenu.hideMenu(); + this.playerInfoOverlay.hide(); + } + + private showRadialMenu(x: number, y: number) { + // Delay so center button isn't clicked immediately on press. + setTimeout(() => { + this.menuElement + .style("left", `${x - this.menuSize / 2}px`) + .style("top", `${y - this.menuSize / 2}px`) + .style("display", "block"); + this.playerInfoOverlay.maybeShow(x, y); + this.isVisible = true; + }, 50); + } + + private hideRadialMenu() { + this.menuElement.style("display", "none"); + this.isVisible = false; + this.playerInfoOverlay.hide(); + } + + private handleCenterButtonClick() { + if (!this.isCenterButtonEnabled) { + return; } - - private hideRadialMenu() { - this.menuElement.style('display', 'none'); - this.isVisible = false; + consolex.log("Center button clicked"); + const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y); + if (this.g.inSpawnPhase()) { + this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell)); + } else { + const myPlayer = this.g.myPlayer(); + if (myPlayer != null && this.g.owner(clicked) != myPlayer) { + this.eventBus.emit( + new SendAttackIntentEvent( + this.g.owner(clicked).id(), + this.uiState.attackRatio * myPlayer.troops() + ) + ); + } } + this.hideRadialMenu(); + } - private handleCenterButtonClick() { - if (!this.isCenterButtonEnabled) { - return - } - consolex.log('Center button clicked'); - const clicked = this.g.ref(this.clickedCell.x, this.clickedCell.y) - if (this.g.inSpawnPhase()) { - this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell)) - } else { - const myPlayer = this.g.myPlayer() - if (myPlayer != null && this.g.owner(clicked) != myPlayer) { - this.eventBus.emit(new SendAttackIntentEvent(this.g.owner(clicked).id(), this.uiState.attackRatio * myPlayer.troops())) - } - } - this.hideRadialMenu(); - } + private activateMenuElement( + slot: Slot, + color: string, + icon: string, + action: () => void + ) { + const menuItem = this.menuItems.get(slot); + menuItem.action = action; + menuItem.disabled = false; + menuItem.color = color; + menuItem.icon = icon; + this.updateMenuItemState(menuItem); + } - private activateMenuElement(slot: Slot, color: string, icon: string, action: () => void) { - const menuItem = this.menuItems.get(slot) - menuItem.action = action - menuItem.disabled = false - menuItem.color = color - menuItem.icon = icon - this.updateMenuItemState(menuItem) - } + private updateMenuItemState(item: any) { + const menuItem = this.menuElement.select(`path[data-name="${item.name}"]`); + menuItem + .attr("fill", item.disabled ? this.disabledColor : item.color) + .style("cursor", item.disabled ? "not-allowed" : "pointer") + .style("opacity", item.disabled ? 0.5 : 1); - private updateMenuItemState(item: any) { - const menuItem = this.menuElement.select(`path[data-name="${item.name}"]`); - menuItem - .attr('fill', item.disabled ? this.disabledColor : item.color) - .style('cursor', item.disabled ? 'not-allowed' : 'pointer') - .style('opacity', item.disabled ? 0.5 : 1); + this.menuElement + .select(`image[data-name="${item.name}"]`) + .attr("xlink:href", item.disabled ? disabledIcon : item.icon) + .attr("fill", item.disabled ? "#999999" : "white"); + } - this.menuElement.select(`image[data-name="${item.name}"]`) - .attr('xlink:href', item.disabled ? disabledIcon : item.icon) - .attr('fill', item.disabled ? '#999999' : 'white'); - } + private onCenterButtonHover(isHovering: boolean) { + if (!this.isCenterButtonEnabled) return; - private onCenterButtonHover(isHovering: boolean) { - if (!this.isCenterButtonEnabled) return; + const scale = isHovering ? 1.2 : 1; + const fontSize = isHovering ? "18px" : "16px"; - const scale = isHovering ? 1.2 : 1; - const fontSize = isHovering ? '18px' : '16px'; + this.menuElement + .select(".center-button-hitbox") + .transition() + .duration(200) + .attr("r", this.centerButtonSize * scale); + this.menuElement + .select(".center-button-visible") + .transition() + .duration(200) + .attr("r", this.centerButtonSize * scale); + this.menuElement + .select(".center-button-text") + .transition() + .duration(200) + .style("font-size", fontSize); + } - this.menuElement.select('.center-button-hitbox').transition().duration(200).attr('r', this.centerButtonSize * scale); - this.menuElement.select('.center-button-visible').transition().duration(200).attr('r', this.centerButtonSize * scale); - this.menuElement.select('.center-button-text').transition().duration(200).style('font-size', fontSize); - } + private enableCenterButton(enabled: boolean) { + this.isCenterButtonEnabled = enabled; + const centerButton = this.menuElement.select(".center-button"); - private enableCenterButton(enabled: boolean) { - this.isCenterButtonEnabled = enabled - const centerButton = this.menuElement.select('.center-button'); + centerButton + .select(".center-button-hitbox") + .style("cursor", enabled ? "pointer" : "not-allowed"); - centerButton.select('.center-button-hitbox') - .style('cursor', enabled ? 'pointer' : 'not-allowed'); + centerButton + .select(".center-button-visible") + .attr("fill", enabled ? "#2c3e50" : "#999999"); - centerButton.select('.center-button-visible') - .attr('fill', enabled ? '#2c3e50' : '#999999'); - - centerButton.select('.center-button-text') - .attr('fill', enabled ? 'white' : '#cccccc'); - } -} \ No newline at end of file + centerButton + .select(".center-button-text") + .attr("fill", enabled ? "white" : "#cccccc"); + } +}