mobile UI: show playerInfo when radial menu activated

This commit is contained in:
Evan
2025-01-30 11:46:48 -08:00
parent 5a1295a0da
commit 4266bd8a59
4 changed files with 814 additions and 697 deletions
+143 -157
View File
@@ -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<number, PointerEvent> = new Map();
private pointers: Map<number, PointerEvent> = 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
};
}
}
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,
};
}
}
+2 -1
View File
@@ -118,7 +118,8 @@ export function createRenderer(
clientID,
emojiTable as EmojiTable,
buildMenu,
uiState
uiState,
playerInfo
),
new SpawnTimer(game, transformHandler),
leaderboard,
+215 -175
View File
@@ -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`
<div class="text-sm opacity-80">
Attitude: <span class="${relationClass}">${relationName}</span>
</div>
`;
}
tick() {
this.requestUpdate();
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? "text-green-500" : "text-white"}">
${player.name()}
</div>
<div class="text-sm opacity-80">
Troops: ${renderTroops(player.troops())}
</div>
<div class="text-sm opacity-80">
Gold: ${renderNumber(player.gold())}
</div>
${relationHtml}
</div>
`;
}
private renderUnitInfo(unit: UnitView) {
const isAlly =
(unit.owner() == this.myPlayer() ||
this.myPlayer()?.isAlliedWith(unit.owner())) ??
false;
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? "text-green-500" : "text-white"}">
${unit.owner().name()}
</div>
<div class="mt-1">
<div class="text-sm opacity-80">${unit.type()}</div>
${unit.hasHealth()
? html`
<div class="text-sm opacity-80">Health: ${unit.health()}</div>
`
: ""}
</div>
</div>
`;
}
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`
<div class="fixed top-24 right-3 z-50 flex flex-col md:top-20 md:right-2">
<div
class="bg-opacity-70 bg-gray-900 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 min-w-[120px] md:min-w-[100px] text-white text-lg md:text-base ${containerClasses}"
>
${this.player != null ? this.renderPlayerInfo(this.player) : ""}
${this.unit != null ? this.renderUnitInfo(this.unit) : ""}
</div>
</div>
`;
}
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`
<div class="text-sm opacity-80">
Attitude: <span class="${relationClass}">${relationName}</span>
</div>
`;
}
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? 'text-green-500' : 'text-white'}">${player.name()}</div>
<div class="text-sm opacity-80">Troops: ${renderTroops(player.troops())}</div>
<div class="text-sm opacity-80">Gold: ${renderNumber(player.gold())}</div>
${relationHtml}
</div>
`;
}
private renderUnitInfo(unit: UnitView) {
const isAlly = (unit.owner() == this.myPlayer() || this.myPlayer()?.isAlliedWith(unit.owner())) ?? false;
return html`
<div class="p-2">
<div class="font-bold mb-1 ${isAlly ? 'text-green-500' : 'text-white'}">${unit.owner().name()}</div>
<div class="mt-1">
<div class="text-sm opacity-80">${unit.type()}</div>
${unit.hasHealth() ? html`
<div class="text-sm opacity-80">Health: ${unit.health()}</div>
` : ''}
</div>
</div>
`;
}
render() {
if (!this._isActive) {
return html``;
}
const containerClasses = this._isInfoVisible ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none';
return html`
<div class="fixed top-24 right-3 z-50 flex flex-col md:top-20 md:right-2">
<div class="bg-opacity-70 bg-gray-900 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 min-w-[120px] md:min-w-[100px] text-white text-lg md:text-base ${containerClasses}">
${this.player != null ? this.renderPlayerInfo(this.player) : ''}
${this.unit != null ? this.renderUnitInfo(this.unit) : ''}
</div>
</div>
`;
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
}
+454 -364
View File
@@ -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<HTMLDivElement, unknown, null, undefined>;
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<HTMLDivElement, unknown, null, undefined>;
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<any>()
.value(() => 1)
.padAngle(0.03);
const arc = d3
.arc<any>()
.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<any>()
.value(() => 1)
.padAngle(0.03);
const arc = d3.arc<any>()
.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');
}
}
centerButton
.select(".center-button-text")
.attr("fill", enabled ? "white" : "#cccccc");
}
}