mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 05:36:09 +00:00
mobile UI: show playerInfo when radial menu activated
This commit is contained in:
+143
-157
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,8 @@ export function createRenderer(
|
||||
clientID,
|
||||
emojiTable as EmojiTable,
|
||||
buildMenu,
|
||||
uiState
|
||||
uiState,
|
||||
playerInfo
|
||||
),
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user