Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -958,6 +958,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