diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 694e0e493..91a719dbe 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -451,10 +451,6 @@ export class InputHandler { } private onPointerDown(event: PointerEvent) { - if (this.uiState.overGhostControls) { - return; - } - if (event.button === 1) { event.preventDefault(); this.eventBus.emit(new AutoUpgradeEvent(event.clientX, event.clientY)); @@ -482,12 +478,6 @@ export class InputHandler { } onPointerUp(event: PointerEvent) { - if (this.uiState.overGhostControls) { - this.pointerDown = false; - this.pointers.clear(); - return; - } - if (event.button === 1) { event.preventDefault(); return; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 3263251fd..cd6b1d31e 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -55,7 +55,6 @@ export function createRenderer( ghostStructure: null, rocketDirectionUp: true, lockedGhostTile: null, - overGhostControls: false, } as UIState; //hide when the game renders diff --git a/src/client/graphics/UIState.ts b/src/client/graphics/UIState.ts index 4350a7b1b..292532466 100644 --- a/src/client/graphics/UIState.ts +++ b/src/client/graphics/UIState.ts @@ -6,5 +6,4 @@ export interface UIState { ghostStructure: UnitType | null; rocketDirectionUp: boolean; lockedGhostTile: TileRef | null; - overGhostControls: boolean; } diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 7d7ae9503..441c86274 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -87,10 +87,10 @@ export class StructureIconsLayer implements Layer { private renderSprites = true; private factory: SpriteFactory; private ghostControls: { - container: PIXI.Container; - confirm: PIXI.Graphics; - cancel: PIXI.Graphics; - flip: PIXI.Graphics; + container: HTMLDivElement; + confirm: HTMLButtonElement; + cancel: HTMLButtonElement; + flip: HTMLButtonElement; } | null = null; private readonly structures: Map = new Map([ [UnitType.City, { visible: true }], @@ -603,91 +603,63 @@ export class StructureIconsLayer implements Layer { private ensureGhostControls() { if (this.ghostControls) return; - const container = new PIXI.Container(); - this.ghostStage.addChild(container); - - const buttonSize = 48; - const gap = 8; - const buttonRadius = 6; + const container = document.createElement("div"); + // Fixed positioning keeps controls anchored to the viewport even if the page scrolls + container.style.position = "fixed"; + container.style.display = "flex"; + container.style.gap = "8px"; + container.style.transform = "translate(-50%, 0)"; + container.style.pointerEvents = "auto"; + container.style.zIndex = "5"; const makeButton = ( label: string, - color: number, - x: number, + background: string, onClick: () => void, - ): PIXI.Graphics => { - const button = new PIXI.Graphics(); - button.beginFill(color); - button.drawRoundedRect(0, 0, buttonSize, buttonSize, buttonRadius); - button.endFill(); - button.x = x; - button.y = 0; - button.eventMode = "static"; - button.cursor = "pointer"; - - // Add text label - const text = new PIXI.Text(label, { - fontFamily: "Arial", - fontSize: 24, - fontWeight: "bold", - fill: 0xffffff, - }); - text.anchor.set(0.5); - text.x = buttonSize / 2; - text.y = buttonSize / 2; - button.addChild(text); - - // Hover effects - button.on("pointerover", () => { - this.uiState.overGhostControls = true; - button.tint = 0xdddddd; - }); - button.on("pointerout", () => { - this.uiState.overGhostControls = false; - button.tint = 0xffffff; - }); - button.on("pointerdown", () => { - this.uiState.overGhostControls = true; - button.tint = 0xaaaaaa; - }); - button.on("pointerup", () => { - this.uiState.overGhostControls = false; - button.tint = 0xffffff; - }); - - button.on("pointertap", (e) => { - e.stopPropagation(); - this.uiState.overGhostControls = false; - onClick(); - }); - + ): HTMLButtonElement => { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = label; + button.style.minHeight = "48px"; + button.style.minWidth = "48px"; + button.style.padding = "8px 16px"; + button.style.borderRadius = "6px"; + button.style.border = "none"; + button.style.fontWeight = "700"; + button.style.fontSize = "13px"; + button.style.color = "#ffffff"; + button.style.background = background; + button.style.cursor = "pointer"; + button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.25)"; + button.style.whiteSpace = "nowrap"; + button.addEventListener("click", onClick); return button; }; - const confirm = makeButton("✓", 0x2e7d32, 0, () => { + const confirm = makeButton("✓", "#2e7d32", () => { if (this.uiState.lockedGhostTile) { this.emitBuildIntent(this.uiState.lockedGhostTile); } }); - const flip = makeButton("↕", 0x1565c0, buttonSize + gap, () => { + const flip = makeButton("↕", "#1565c0", () => { const next = !this.uiState.rocketDirectionUp; this.eventBus.emit(new SwapRocketDirectionEvent(next)); }); - const cancel = makeButton("✕", 0xb71c1c, (buttonSize + gap) * 2, () => + const cancel = makeButton("✕", "#b71c1c", () => this.removeGhostStructure(), ); - container.addChild(confirm, flip, cancel); + container.append(confirm, flip, cancel); + document.body.appendChild(container); this.ghostControls = { container, confirm, cancel, flip }; } private destroyGhostControls() { if (!this.ghostControls) return; - this.uiState.overGhostControls = false; - this.ghostControls.container.destroy({ children: true }); + this.ghostControls.container.remove(); this.ghostControls = null; } @@ -713,13 +685,9 @@ export class StructureIconsLayer implements Layer { 0.75, Math.min(1.4, this.transformHandler.scale / 2), ); - - // Position PIXI container in world coordinates - const buttonWidth = 48; - const totalWidth = buttonWidth * 3 + 8 * 2; // 3 buttons + 2 gaps - this.ghostControls!.container.x = localX - totalWidth / 2; - this.ghostControls!.container.y = localY + offsetY; - this.ghostControls!.container.scale.set(scale); + this.ghostControls!.container.style.left = `${rect.left + localX}px`; + this.ghostControls!.container.style.top = `${rect.top + localY + offsetY}px`; + this.ghostControls!.container.style.transform = `translate(-50%, 0) scale(${scale})`; } private resolveGhostRangeLevel( diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index 779634a73..27ea31878 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -39,7 +39,6 @@ describe("InputHandler AutoUpgrade", () => { ghostStructure: null, rocketDirectionUp: true, lockedGhostTile: null, - overGhostControls: false, }, mockCanvas, eventBus,