Skip to content

Commit 7c23c69

Browse files
committed
feat: repurpose Info button as Quick Chat radial submenu
- Repurposes the 'i' (Info) button in the right-click radial menu to open a Quick Chat submenu with configurable preset actions - Presets support quick-chat messages, emoji panel, and start/stop trade - Defaults: 'Give me troops', emoji panel, 'Attack [P1]!' - For actions requiring a target player (requiresPlayer: true), enters a target-selection mode: cursor changes to pointer, a badge follows the mouse, and clicking an enemy tile sends the message with that player as the target - Fixes attack-on-click bug during target selection by guarding ClientGameRunner and UnitLayer event handlers - Adds a permanent 'Player Info' button (original Info behaviour) and a 'Customize Presets' settings button to the submenu - QuickChatConfigModal uses the same chat-columns UI as ChatModal - Presets auto-save to localStorage; reset requires two-step confirmation - All strings go through translateText() with new quick_chat.* i18n keys - Adds .kiro/ to .gitignore - Fixes pre-existing localStorage mock issue in tests/setup.ts
1 parent 23150f0 commit 7c23c69

15 files changed

+793
-7
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ src/assets/
1818
*.log
1919
*debug*.txt
2020
eslint_out.txt
21+
22+
# Kiro IDE workspace files
23+
.kiro/

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@
329329
<game-info-modal></game-info-modal>
330330
<alert-frame></alert-frame>
331331
<chat-modal></chat-modal>
332+
<quick-chat-config-modal></quick-chat-config-modal>
333+
<target-selection-layer></target-selection-layer>
332334
<multi-tab-modal></multi-tab-modal>
333335
<game-left-sidebar></game-left-sidebar>
334336
<performance-overlay></performance-overlay>

resources/lang/en.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,20 @@
957957
"delete_unit_title": "Delete Unit",
958958
"delete_unit_description": "Click to delete the nearest unit"
959959
},
960+
"quick_chat": {
961+
"configure_presets": "Customize Presets",
962+
"preset_label": "Preset",
963+
"add_preset": "+ Add",
964+
"done": "Done",
965+
"reset_defaults": "Reset defaults",
966+
"confirm_reset": "⚠ Confirm reset",
967+
"emoji_panel": "😀 Emoji Panel",
968+
"trade_toggle": "🤝 Start / Stop Trade",
969+
"editing_hint": "Editing preset {n} — pick a category and phrase, or choose an action",
970+
"select_hint": "Click a preset to edit it",
971+
"actions": "Actions",
972+
"select_target": "Select a player"
973+
},
960974
"discord_user_header": {
961975
"avatar_alt": "Avatar"
962976
},

src/client/ClientGameRunner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
import { createCanvas } from "./Utils";
5151
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
5252
import { GoToPlayerEvent } from "./graphics/layers/Leaderboard";
53+
import { TargetSelectionMode } from "./graphics/layers/TargetSelectionMode";
5354
import SoundManager from "./sound/SoundManager";
5455

5556
export interface LobbyConfig {
@@ -544,6 +545,10 @@ export class ClientGameRunner {
544545
if (!this.isActive || this.renderer.uiState.ghostStructure !== null) {
545546
return;
546547
}
548+
// Don't process attacks while the player is picking a target for a quick-chat action
549+
if (TargetSelectionMode.getInstance().active) {
550+
return;
551+
}
547552
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
548553
event.x,
549554
event.y,

src/client/InputHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export class ContextMenuEvent implements GameEvent {
5454
constructor(
5555
public readonly x: number,
5656
public readonly y: number,
57+
/** true when triggered by a genuine right-click / long-press */
58+
public readonly isRightClick: boolean = false,
5759
) {}
5860
}
5961

@@ -596,7 +598,7 @@ export class InputHandler {
596598
this.setGhostStructure(null);
597599
return;
598600
}
599-
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
601+
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY, true));
600602
}
601603

602604
private setGhostStructure(ghostStructure: PlayerBuildableUnitType | null) {

src/client/graphics/GameRenderer.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { UILayer } from "./layers/UILayer";
4646
import { UnitDisplay } from "./layers/UnitDisplay";
4747
import { UnitLayer } from "./layers/UnitLayer";
4848
import { WinModal } from "./layers/WinModal";
49+
import { QuickChatConfigModal } from "./layers/QuickChatConfigModal";
50+
import { TargetSelectionLayer } from "./layers/TargetSelectionLayer";
4951

5052
export function createRenderer(
5153
canvas: HTMLCanvasElement,
@@ -227,6 +229,25 @@ export function createRenderer(
227229
}
228230
headsUpMessage.game = game;
229231

232+
const targetSelectionLayer = document.querySelector(
233+
"target-selection-layer",
234+
) as TargetSelectionLayer;
235+
if (!(targetSelectionLayer instanceof TargetSelectionLayer)) {
236+
console.error("target-selection-layer not found");
237+
} else {
238+
targetSelectionLayer.eventBus = eventBus;
239+
targetSelectionLayer.game = game;
240+
targetSelectionLayer.transformHandler = transformHandler;
241+
}
242+
243+
// QuickChatConfigModal needs no runtime wiring — it reads from QuickChatPresetService directly.
244+
const quickChatConfigModal = document.querySelector(
245+
"quick-chat-config-modal",
246+
) as QuickChatConfigModal;
247+
if (!(quickChatConfigModal instanceof QuickChatConfigModal)) {
248+
console.error("quick-chat-config-modal not found");
249+
}
250+
230251
const structureLayer = new StructureLayer(game, eventBus, transformHandler);
231252
const samRadiusLayer = new SAMRadiusLayer(game, eventBus, uiState);
232253

@@ -317,6 +338,7 @@ export function createRenderer(
317338
inGamePromo,
318339
alertFrame,
319340
performanceOverlay,
341+
targetSelectionLayer,
320342
];
321343

322344
return new GameRenderer(

src/client/graphics/layers/MainRadialMenu.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { EventBus } from "../../../core/EventBus";
55
import { PlayerActions } from "../../../core/game/Game";
66
import { TileRef } from "../../../core/game/GameMap";
77
import { GameView, PlayerView } from "../../../core/game/GameView";
8+
import {
9+
CloseViewEvent,
10+
ContextMenuEvent,
11+
MouseUpEvent,
12+
TouchEvent,
13+
} from "../../InputHandler";
14+
import { SendQuickChatEvent } from "../../Transport";
815
import { TransformHandler } from "../TransformHandler";
916
import { UIState } from "../UIState";
1017
import { BuildMenu } from "./BuildMenu";
@@ -20,11 +27,11 @@ import {
2027
MenuElementParams,
2128
rootMenuElement,
2229
} from "./RadialMenuElements";
30+
import { TargetSelectionMode } from "./TargetSelectionMode";
31+
2332
const donateTroopIcon = assetUrl("images/DonateTroopIconWhite.svg");
2433
const swordIcon = assetUrl("images/SwordIconWhite.svg");
2534

26-
import { ContextMenuEvent } from "../../InputHandler";
27-
2835
@customElement("main-radial-menu")
2936
export class MainRadialMenu extends LitElement implements Layer {
3037
private radialMenu: RadialMenu;
@@ -79,7 +86,59 @@ export class MainRadialMenu extends LitElement implements Layer {
7986

8087
init() {
8188
this.radialMenu.init();
89+
90+
// Handle left-click and touch: if target-selection mode is active, resolve
91+
// the clicked tile as the chat target instead of performing a normal action.
92+
const handleSelectClick = (x: number, y: number) => {
93+
const mode = TargetSelectionMode.getInstance();
94+
if (!mode.active) return false;
95+
96+
const worldCoords = this.transformHandler.screenToWorldCoordinates(x, y);
97+
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) return true;
98+
99+
const tile = this.game.ref(worldCoords.x, worldCoords.y);
100+
const owner = this.game.owner(tile);
101+
102+
if (owner.isPlayer()) {
103+
this.eventBus.emit(
104+
new SendQuickChatEvent(
105+
mode.pendingRecipient!,
106+
mode.pendingKey!,
107+
(owner as PlayerView).id(),
108+
),
109+
);
110+
mode.exit();
111+
}
112+
// If tile has no owner, stay in mode and wait for another click.
113+
return true;
114+
};
115+
116+
this.eventBus.on(MouseUpEvent, (event) => {
117+
if (handleSelectClick(event.x, event.y)) return;
118+
});
119+
120+
this.eventBus.on(TouchEvent, (event) => {
121+
if (handleSelectClick(event.x, event.y)) return;
122+
});
123+
124+
// Escape cancels target-selection mode.
125+
this.eventBus.on(CloseViewEvent, () => {
126+
TargetSelectionMode.getInstance().exit();
127+
});
128+
82129
this.eventBus.on(ContextMenuEvent, (event) => {
130+
// While in target-selection mode:
131+
// - left-click (isRightClick=false) → attempt to resolve the target
132+
// - right-click (isRightClick=true) → cancel the mode
133+
if (TargetSelectionMode.getInstance().active) {
134+
if (event.isRightClick) {
135+
TargetSelectionMode.getInstance().exit();
136+
} else {
137+
handleSelectClick(event.x, event.y);
138+
}
139+
return;
140+
}
141+
83142
const worldCoords = this.transformHandler.screenToWorldCoordinates(
84143
event.x,
85144
event.y,

0 commit comments

Comments
 (0)