diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 45d1188a3b..4be2878900 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -136,6 +136,20 @@ export class TickMetricsEvent implements GameEvent { ) {} } +export class AllianceRequestEvent implements GameEvent { + constructor( + public readonly x: number, + public readonly y: number, + ) {} +} + +export class BreakAllianceEvent implements GameEvent { + constructor( + public readonly x: number, + public readonly y: number, + ) {} +} + export class InputHandler { private lastPointerX: number = 0; private lastPointerY: number = 0; @@ -225,6 +239,8 @@ export class InputHandler { buildAtomBomb: "Digit8", buildHydrogenBomb: "Digit9", buildMIRV: "Digit0", + alliance: "KeyF", + breakAlliance: "KeyG", ...saved, }; @@ -343,6 +359,8 @@ export class InputHandler { "ControlRight", "ShiftLeft", "ShiftRight", + this.keybinds.alliance, + this.keybinds.breakAlliance, ].includes(e.code) ) { this.activeKeys.add(e.code); @@ -507,6 +525,18 @@ export class InputHandler { return; } + if (this.activeKeys.has(this.keybinds.alliance)) { + this.eventBus.emit( + new AllianceRequestEvent(event.clientX, event.clientY), + ); + return; + } + + if (this.activeKeys.has(this.keybinds.breakAlliance)) { + this.eventBus.emit(new BreakAllianceEvent(event.clientX, event.clientY)); + return; + } + const dist = Math.abs(event.x - this.lastPointerDownX) + Math.abs(event.y - this.lastPointerDownY); diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 0de490f3f8..3c2252d890 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -279,7 +279,7 @@ export function createRenderer( new RailroadLayer(game, eventBus, transformHandler, uiState), structureLayer, samRadiusLayer, - new UnitLayer(game, eventBus, transformHandler), + new UnitLayer(game, eventBus, transformHandler, uiState), new FxLayer(game, eventBus, transformHandler), new UILayer(game, eventBus, transformHandler), new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState), diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts index 989b5aa797..1961454943 100644 --- a/src/client/graphics/layers/MainRadialMenu.ts +++ b/src/client/graphics/layers/MainRadialMenu.ts @@ -22,7 +22,11 @@ import { import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url"; import swordIcon from "/images/SwordIconWhite.svg?url"; -import { ContextMenuEvent } from "../../InputHandler"; +import { + AllianceRequestEvent, + BreakAllianceEvent, + ContextMenuEvent, +} from "../../InputHandler"; @customElement("main-radial-menu") export class MainRadialMenu extends LitElement implements Layer { @@ -103,6 +107,54 @@ export class MainRadialMenu extends LitElement implements Layer { ); }); }); + + this.eventBus.on(AllianceRequestEvent, (event) => { + const worldCoords = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) { + return; + } + const myPlayer = this.game.myPlayer(); + if (!myPlayer) { + return; + } + + const tile = this.game.ref(worldCoords.x, worldCoords.y); + const owner = this.game.owner(tile); + if (owner.isPlayer() && owner !== myPlayer) { + // Send alliance request + this.playerActionHandler.handleAllianceRequest( + myPlayer, + owner as PlayerView, + ); + } + }); + + this.eventBus.on(BreakAllianceEvent, (event) => { + const worldCoords = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) { + return; + } + const myPlayer = this.game.myPlayer(); + if (!myPlayer) { + return; + } + + const tile = this.game.ref(worldCoords.x, worldCoords.y); + const owner = this.game.owner(tile); + if (owner.isPlayer() && owner !== myPlayer) { + // Break alliance + this.playerActionHandler.handleBreakAlliance( + myPlayer, + owner as PlayerView, + ); + } + }); } private async updatePlayerActions( diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index ca1de78e72..1983b93062 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,27 +1,31 @@ import { colord, Colord } from "colord"; -import { EventBus } from "../../../core/EventBus"; import { Theme } from "../../../core/configuration/Config"; +import { EventBus } from "../../../core/EventBus"; import { UnitType } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; -import { GameView, UnitView } from "../../../core/game/GameView"; +import { GameUpdateType } from "../../../core/game/GameUpdates"; +import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { BezenhamLine } from "../../../core/utilities/Line"; import { AlternateViewEvent, ContextMenuEvent, + MouseMoveEvent, MouseUpEvent, TouchEvent, UnitSelectionEvent, } from "../../InputHandler"; import { MoveWarshipIntentEvent } from "../../Transport"; -import { TransformHandler } from "../TransformHandler"; -import { Layer } from "./Layer"; - -import { GameUpdateType } from "../../../core/game/GameUpdates"; import { getColoredSprite, isSpriteReady, loadAllSprites, } from "../SpriteLoader"; +import { TransformHandler } from "../TransformHandler"; +import { UIState } from "../UIState"; +import { Layer } from "./Layer"; + +import allianceIconPath from "/images/AllianceIconWhite.svg?url"; +import traitorIconPath from "/images/TraitorIconWhite.svg?url"; enum Relationship { Self, @@ -51,13 +55,63 @@ export class UnitLayer implements Layer { // Configuration for unit selection private readonly WARSHIP_SELECTION_RADIUS = 10; // Radius in game cells for warship selection hit zone + private fKeyHeld = false; + private gKeyHeld = false; + private mouseX = 0; + private mouseY = 0; + + private allianceIcon: HTMLCanvasElement | null = null; + private traitorIcon: HTMLCanvasElement | null = null; + constructor( private game: GameView, private eventBus: EventBus, transformHandler: TransformHandler, + private uiState: UIState, ) { this.theme = game.config().theme(); this.transformHandler = transformHandler; + + this.loadIcons(); + } + + private loadIcons() { + this.loadAndTintIcon(allianceIconPath, "#53ac75", (icon) => { + this.allianceIcon = icon; + }); + this.loadAndTintIcon(traitorIconPath, "#c74848", (icon) => { + this.traitorIcon = icon; + }); + } + + private loadAndTintIcon( + path: string, + color: string, + callback: (icon: HTMLCanvasElement) => void, + ) { + const img = new Image(); + img.src = path; + img.onload = () => { + callback(this.tintIcon(img, color)); + }; + } + + private tintIcon(image: HTMLImageElement, color: string): HTMLCanvasElement { + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext("2d"); + if (!ctx) return canvas; + + // Draw the color + ctx.fillStyle = color; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Mask with image + ctx.globalCompositeOperation = "destination-in"; + ctx.drawImage(image, 0, 0); + + return canvas; } shouldTransform(): boolean { @@ -77,11 +131,28 @@ export class UnitLayer implements Layer { this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); this.eventBus.on(TouchEvent, (e) => this.onTouch(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); + this.eventBus.on(MouseMoveEvent, (e) => { + this.mouseX = e.x; + this.mouseY = e.y; + }); + + window.addEventListener("keydown", (e) => { + if (e.code === "KeyF") this.fKeyHeld = true; + if (e.code === "KeyG") this.gKeyHeld = true; + }); + + window.addEventListener("keyup", (e) => { + if (e.code === "KeyF") this.fKeyHeld = false; + if (e.code === "KeyG") this.gKeyHeld = false; + }); + this.redraw(); loadAllSprites(); } + // ... rest of the file ... + /** * Find player-owned warships near the given cell within a configurable radius * @param clickRef The tile to check @@ -214,6 +285,63 @@ export class UnitLayer implements Layer { this.game.width(), this.game.height(), ); + + this.drawCursorIcon(context); + } + + private drawCursorIcon(context: CanvasRenderingContext2D) { + if (!this.fKeyHeld && !this.gKeyHeld) return; + + // Determine target under cursor + const cell = this.transformHandler.screenToWorldCoordinates( + this.mouseX, + this.mouseY, + ); + if (!this.game.isValidCoord(cell.x, cell.y)) return; + + const clickRef = this.game.ref(cell.x, cell.y); + const owner = this.game.owner(clickRef); + const myPlayer = this.game.myPlayer(); + + if (!myPlayer || !owner.isPlayer() || owner === myPlayer) { + return; + } + + const targetPlayer = owner as PlayerView; + const isAllied = targetPlayer.isAlliedWith(myPlayer); + + let iconToDraw: HTMLCanvasElement | null = null; + + if (this.fKeyHeld && !isAllied) { + // Check if we can actually request? (e.g. not enemy?) + // For now, show icon if F is held and not allied. + iconToDraw = this.allianceIcon; + } else if (this.gKeyHeld && isAllied) { + iconToDraw = this.traitorIcon; + } + + if (iconToDraw) { + const x = cell.x - this.game.width() / 2; + const y = cell.y - this.game.height() / 2; + + // Draw icon centered at x,y + const drawSize = 32; // size in world units (cells) + + // console.log("DrawIcon", { f: this.fKeyHeld, g: this.gKeyHeld, x, y, drawSize, icon: !!iconToDraw }); + + context.save(); + context.globalAlpha = 0.8; + + context.drawImage( + iconToDraw, + x - drawSize / 2, + y - drawSize / 2, + drawSize, + drawSize, + ); + + context.restore(); + } } onAlternativeViewEvent(event: AlternateViewEvent) {