Skip to content
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ src/assets/
*.log
*debug*.txt
eslint_out.txt

# Kiro IDE workspace files
.kiro/
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@
<game-info-modal></game-info-modal>
<alert-frame></alert-frame>
<chat-modal></chat-modal>
<quick-chat-config-modal></quick-chat-config-modal>
<target-selection-layer></target-selection-layer>
<multi-tab-modal></multi-tab-modal>
<game-left-sidebar></game-left-sidebar>
<performance-overlay></performance-overlay>
Expand Down
16 changes: 16 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,22 @@
"delete_unit_title": "Delete Unit",
"delete_unit_description": "Click to delete the nearest unit"
},
"quick_chat": {
"configure_presets": "Customize Presets",
"preset_label": "Preset",
"add_preset": "+ Add",
"done": "Done",
"reset_defaults": "Reset defaults",
"confirm_reset": "⚠ Confirm reset",
"emoji_panel": "😀 Emoji Panel",
"trade_toggle": "🤝 Start / Stop Trade",
"editing_hint": "Editing preset {n} — pick a category and phrase, or choose an action",
"select_hint": "Click a preset to edit it",
"actions": "Actions",
"select_target": "Select a player",
"player_info": "Player Info",
"needs_target": "[needs target]"
},
"discord_user_header": {
"avatar_alt": "Avatar"
},
Expand Down
5 changes: 5 additions & 0 deletions src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { createCanvas } from "./Utils";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { GoToPlayerEvent } from "./graphics/layers/Leaderboard";
import { TargetSelectionMode } from "./graphics/layers/TargetSelectionMode";
import SoundManager from "./sound/SoundManager";

export interface LobbyConfig {
Expand Down Expand Up @@ -544,6 +545,10 @@ export class ClientGameRunner {
if (!this.isActive || this.renderer.uiState.ghostStructure !== null) {
return;
}
// Don't process attacks while the player is picking a target for a quick-chat action
if (TargetSelectionMode.getInstance().active) {
return;
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
Expand Down
6 changes: 5 additions & 1 deletion src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class ContextMenuEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
/** true when triggered by a genuine right-click / long-press */
public readonly isRightClick: boolean = false,
) {}
}

Expand Down Expand Up @@ -596,7 +598,9 @@ export class InputHandler {
this.setGhostStructure(null);
return;
}
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
this.eventBus.emit(
new ContextMenuEvent(event.clientX, event.clientY, true),
);
}

private setGhostStructure(ghostStructure: PlayerBuildableUnitType | null) {
Expand Down
24 changes: 24 additions & 0 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import { NukeTrajectoryPreviewLayer } from "./layers/NukeTrajectoryPreviewLayer"
import { PerformanceOverlay } from "./layers/PerformanceOverlay";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { QuickChatConfigModal } from "./layers/QuickChatConfigModal";
import { RailroadLayer } from "./layers/RailroadLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SAMRadiusLayer } from "./layers/SAMRadiusLayer";
import { SettingsModal } from "./layers/SettingsModal";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TargetSelectionLayer } from "./layers/TargetSelectionLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
Expand Down Expand Up @@ -227,6 +229,25 @@ export function createRenderer(
}
headsUpMessage.game = game;

const targetSelectionLayer = document.querySelector(
"target-selection-layer",
) as TargetSelectionLayer;
if (!(targetSelectionLayer instanceof TargetSelectionLayer)) {
console.error("target-selection-layer not found");
} else {
targetSelectionLayer.eventBus = eventBus;
targetSelectionLayer.game = game;
targetSelectionLayer.transformHandler = transformHandler;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// QuickChatConfigModal needs no runtime wiring — it reads from QuickChatPresetService directly.
const quickChatConfigModal = document.querySelector(
"quick-chat-config-modal",
) as QuickChatConfigModal;
if (!(quickChatConfigModal instanceof QuickChatConfigModal)) {
console.error("quick-chat-config-modal not found");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const structureLayer = new StructureLayer(game, eventBus, transformHandler);
const samRadiusLayer = new SAMRadiusLayer(game, eventBus, uiState);

Expand Down Expand Up @@ -317,6 +338,9 @@ export function createRenderer(
inGamePromo,
alertFrame,
performanceOverlay,
...(targetSelectionLayer instanceof TargetSelectionLayer
? [targetSelectionLayer]
: []),
];

return new GameRenderer(
Expand Down
63 changes: 61 additions & 2 deletions src/client/graphics/layers/MainRadialMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { EventBus } from "../../../core/EventBus";
import { PlayerActions } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import {
CloseViewEvent,
ContextMenuEvent,
MouseUpEvent,
TouchEvent,
} from "../../InputHandler";
import { SendQuickChatEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { BuildMenu } from "./BuildMenu";
Expand All @@ -20,11 +27,11 @@ import {
MenuElementParams,
rootMenuElement,
} from "./RadialMenuElements";
import { TargetSelectionMode } from "./TargetSelectionMode";

const donateTroopIcon = assetUrl("images/DonateTroopIconWhite.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");

import { ContextMenuEvent } from "../../InputHandler";

@customElement("main-radial-menu")
export class MainRadialMenu extends LitElement implements Layer {
private radialMenu: RadialMenu;
Expand Down Expand Up @@ -79,7 +86,59 @@ export class MainRadialMenu extends LitElement implements Layer {

init() {
this.radialMenu.init();

// Handle left-click and touch: if target-selection mode is active, resolve
// the clicked tile as the chat target instead of performing a normal action.
const handleSelectClick = (x: number, y: number) => {
const mode = TargetSelectionMode.getInstance();
if (!mode.active) return false;

const worldCoords = this.transformHandler.screenToWorldCoordinates(x, y);
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) return true;

const tile = this.game.ref(worldCoords.x, worldCoords.y);
const owner = this.game.owner(tile);

if (owner.isPlayer()) {
this.eventBus.emit(
new SendQuickChatEvent(
mode.pendingRecipient!,
mode.pendingKey!,
(owner as PlayerView).id(),
),
);
mode.exit();
}
// If tile has no owner, stay in mode and wait for another click.
return true;
};

this.eventBus.on(MouseUpEvent, (event) => {
if (handleSelectClick(event.x, event.y)) return;
});

this.eventBus.on(TouchEvent, (event) => {
if (handleSelectClick(event.x, event.y)) return;
});

// Escape cancels target-selection mode.
this.eventBus.on(CloseViewEvent, () => {
TargetSelectionMode.getInstance().exit();
});

this.eventBus.on(ContextMenuEvent, (event) => {
// While in target-selection mode:
// - left-click (isRightClick=false) → attempt to resolve the target
// - right-click (isRightClick=true) → cancel the mode
if (TargetSelectionMode.getInstance().active) {
if (event.isRightClick) {
TargetSelectionMode.getInstance().exit();
} else {
handleSelectClick(event.x, event.y);
}
return;
}

const worldCoords = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
Expand Down
Loading
Loading