Skip to content

Commit 7ae4b0c

Browse files
committed
Make UI button wiring declarative
1 parent 75ac322 commit 7ae4b0c

File tree

3 files changed

+154
-149
lines changed

3 files changed

+154
-149
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
ACTION_BUTTON_BINDINGS,
5+
ACTION_BUTTON_IDS,
6+
STATIC_BUTTON_BINDINGS,
7+
} from './button-bindings';
8+
9+
describe('ui-button-bindings', () => {
10+
it('keeps action button ids aligned with the action bindings', () => {
11+
expect(ACTION_BUTTON_IDS).toEqual(
12+
ACTION_BUTTON_BINDINGS.map(({ id }) => id),
13+
);
14+
});
15+
16+
it('includes rematch and exit in the static button binding set', () => {
17+
expect(STATIC_BUTTON_BINDINGS.slice(-2)).toEqual([
18+
{ id: 'rematchBtn', event: { type: 'rematch' } },
19+
{ id: 'exitBtn', event: { type: 'exit' } },
20+
]);
21+
});
22+
23+
it('does not duplicate bound button ids', () => {
24+
const ids = STATIC_BUTTON_BINDINGS.map(({ id }) => id);
25+
26+
expect(new Set(ids).size).toBe(ids.length);
27+
});
28+
});

src/client/ui/button-bindings.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { UIEvent } from './events';
2+
3+
export interface UIButtonEventBinding {
4+
id: string;
5+
event: UIEvent;
6+
}
7+
8+
export const ACTION_BUTTON_BINDINGS = [
9+
{ id: 'undoBtn', event: { type: 'undo' } },
10+
{ id: 'confirmBtn', event: { type: 'confirm' } },
11+
{
12+
id: 'launchMineBtn',
13+
event: { type: 'launchOrdnance', ordType: 'mine' },
14+
},
15+
{
16+
id: 'launchTorpedoBtn',
17+
event: { type: 'launchOrdnance', ordType: 'torpedo' },
18+
},
19+
{
20+
id: 'launchNukeBtn',
21+
event: { type: 'launchOrdnance', ordType: 'nuke' },
22+
},
23+
{ id: 'emplaceBaseBtn', event: { type: 'emplaceBase' } },
24+
{ id: 'skipOrdnanceBtn', event: { type: 'skipOrdnance' } },
25+
{ id: 'attackBtn', event: { type: 'attack' } },
26+
{ id: 'fireBtn', event: { type: 'fireAll' } },
27+
{ id: 'skipCombatBtn', event: { type: 'skipCombat' } },
28+
{ id: 'skipLogisticsBtn', event: { type: 'skipLogistics' } },
29+
{ id: 'confirmTransfersBtn', event: { type: 'confirmTransfers' } },
30+
] as const satisfies readonly UIButtonEventBinding[];
31+
32+
export const STATIC_BUTTON_BINDINGS = [
33+
...ACTION_BUTTON_BINDINGS,
34+
{ id: 'rematchBtn', event: { type: 'rematch' } },
35+
{ id: 'exitBtn', event: { type: 'exit' } },
36+
] as const satisfies readonly UIButtonEventBinding[];
37+
38+
export const ACTION_BUTTON_IDS = ACTION_BUTTON_BINDINGS.map(({ id }) => id);

src/client/ui/ui.ts

Lines changed: 88 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Ship,
99
} from '../../shared/types';
1010
import { byId, el, hide, show, visible } from '../dom';
11+
import { ACTION_BUTTON_IDS, STATIC_BUTTON_BINDINGS } from './button-bindings';
1112
import type { UIEvent } from './events';
1213
import { canAddFleetShip, getFleetCartView, getFleetShopView } from './fleet';
1314
import {
@@ -56,20 +57,7 @@ export class UIManager {
5657
private playerId: number = -1;
5758
private layoutSyncFrame: number | null = null;
5859

59-
private readonly actionButtonIds = [
60-
'undoBtn',
61-
'confirmBtn',
62-
'launchMineBtn',
63-
'launchTorpedoBtn',
64-
'launchNukeBtn',
65-
'emplaceBaseBtn',
66-
'skipOrdnanceBtn',
67-
'attackBtn',
68-
'fireBtn',
69-
'skipCombatBtn',
70-
'skipLogisticsBtn',
71-
'confirmTransfersBtn',
72-
];
60+
private readonly actionButtonIds = ACTION_BUTTON_IDS;
7361

7462
onEvent: ((event: UIEvent) => void) | null = null;
7563

@@ -100,20 +88,7 @@ export class UIManager {
10088
this.chatInput = byId('chatInput') as HTMLInputElement;
10189
this.fleetBuildingEl = byId('fleetBuilding');
10290

103-
this.chatInput.addEventListener('keydown', (e) => {
104-
// Prevent game keyboard shortcuts while
105-
// typing
106-
e.stopPropagation();
107-
108-
if (e.key === 'Enter') {
109-
const text = this.chatInput.value.trim();
110-
111-
if (text && this.onEvent) {
112-
this.onEvent({ type: 'chat', text });
113-
this.chatInput.value = '';
114-
}
115-
}
116-
});
91+
this.bindChatInput();
11792

11893
const mobileQuery = window.matchMedia('(max-width: 760px)');
11994
this.isMobile = mobileQuery.matches;
@@ -133,7 +108,42 @@ export class UIManager {
133108
this.handleViewportResize,
134109
);
135110

136-
// Wire up buttons
111+
this.bindMenuControls();
112+
this.bindDifficultyButtons();
113+
// Generate scenario buttons from data
114+
this.buildScenarioList();
115+
this.bindJoinControls();
116+
this.bindCopyButton();
117+
this.bindStaticButtons();
118+
this.bindLogControls();
119+
}
120+
121+
private emit(event: UIEvent) {
122+
this.onEvent?.(event);
123+
}
124+
125+
private bindChatInput() {
126+
this.chatInput.addEventListener('keydown', (e) => {
127+
// Prevent game keyboard shortcuts while
128+
// typing
129+
e.stopPropagation();
130+
131+
if (e.key !== 'Enter') {
132+
return;
133+
}
134+
135+
const text = this.chatInput.value.trim();
136+
137+
if (!text) {
138+
return;
139+
}
140+
141+
this.emit({ type: 'chat', text });
142+
this.chatInput.value = '';
143+
});
144+
}
145+
146+
private bindMenuControls() {
137147
byId('createBtn').addEventListener('click', () => {
138148
this.showScenarioSelect();
139149
});
@@ -143,70 +153,60 @@ export class UIManager {
143153
this.showScenarioSelect();
144154
});
145155

146-
// Difficulty buttons
147-
for (const btn of Array.from(
148-
document.querySelectorAll('.btn-difficulty'),
149-
)) {
156+
byId('backBtn').addEventListener('click', () => {
157+
this.emit({ type: 'backToMenu' });
158+
this.showMenu();
159+
});
160+
}
161+
162+
private bindDifficultyButtons() {
163+
const buttons = Array.from(
164+
document.querySelectorAll<HTMLElement>('.btn-difficulty'),
165+
);
166+
167+
for (const btn of buttons) {
150168
btn.addEventListener('click', (e: Event) => {
151169
e.stopPropagation();
152170

153-
const diff = (btn as HTMLElement).dataset.difficulty as
154-
| 'easy'
155-
| 'normal'
156-
| 'hard';
171+
const diff = btn.dataset.difficulty as 'easy' | 'normal' | 'hard';
157172
this.aiDifficulty = diff;
158173

159-
// Update active state
160-
for (const b of Array.from(
161-
document.querySelectorAll('.btn-difficulty'),
162-
)) {
163-
b.classList.remove('active');
174+
for (const button of buttons) {
175+
button.classList.remove('active');
164176
}
165177

166178
btn.classList.add('active');
167179
});
168180
}
181+
}
169182

170-
// Generate scenario buttons from data
171-
this.buildScenarioList();
183+
private submitJoin(rawValue: string) {
184+
const parsed = parseJoinInput(rawValue, CODE_LENGTH);
172185

173-
byId('backBtn').addEventListener('click', () => {
174-
this.onEvent?.({ type: 'backToMenu' });
175-
this.showMenu();
186+
if (!parsed) {
187+
return;
188+
}
189+
190+
this.emit({
191+
type: 'join',
192+
code: parsed.code,
193+
playerToken: parsed.playerToken,
176194
});
195+
}
177196

197+
private bindJoinControls() {
178198
byId('joinBtn').addEventListener('click', () => {
179-
const parsed = parseJoinInput(
180-
(byId('codeInput') as HTMLInputElement).value,
181-
CODE_LENGTH,
182-
);
183-
184-
if (parsed) {
185-
this.onEvent?.({
186-
type: 'join',
187-
code: parsed.code,
188-
playerToken: parsed.playerToken,
189-
});
190-
}
199+
this.submitJoin((byId('codeInput') as HTMLInputElement).value);
191200
});
192201

193202
byId('codeInput').addEventListener('keydown', (e) => {
194203
if (e.key === 'Enter') {
195-
const parsed = parseJoinInput(
196-
(e.target as HTMLInputElement).value,
197-
CODE_LENGTH,
198-
);
199-
200-
if (parsed) {
201-
this.onEvent?.({
202-
type: 'join',
203-
code: parsed.code,
204-
playerToken: parsed.playerToken,
205-
});
206-
}
204+
this.submitJoin((e.target as HTMLInputElement).value);
207205
}
208206
});
207+
}
209208

209+
private bindCopyButton() {
210210
byId('copyBtn').addEventListener('click', () => {
211211
const code = byId('gameCode').textContent;
212212
const url = `${window.location.origin}/?code=${code}`;
@@ -219,99 +219,38 @@ export class UIManager {
219219
}, 2000);
220220
});
221221
});
222+
}
222223

223-
byId('undoBtn').addEventListener('click', () =>
224-
this.onEvent?.({ type: 'undo' }),
225-
);
226-
227-
byId('confirmBtn').addEventListener('click', () =>
228-
this.onEvent?.({ type: 'confirm' }),
229-
);
230-
231-
byId('launchMineBtn').addEventListener('click', () =>
232-
this.onEvent?.({
233-
type: 'launchOrdnance',
234-
ordType: 'mine',
235-
}),
236-
);
237-
238-
byId('launchTorpedoBtn').addEventListener('click', () =>
239-
this.onEvent?.({
240-
type: 'launchOrdnance',
241-
ordType: 'torpedo',
242-
}),
243-
);
244-
245-
byId('launchNukeBtn').addEventListener('click', () =>
246-
this.onEvent?.({
247-
type: 'launchOrdnance',
248-
ordType: 'nuke',
249-
}),
250-
);
251-
252-
byId('emplaceBaseBtn').addEventListener('click', () =>
253-
this.onEvent?.({ type: 'emplaceBase' }),
254-
);
255-
256-
byId('skipOrdnanceBtn').addEventListener('click', () =>
257-
this.onEvent?.({ type: 'skipOrdnance' }),
258-
);
259-
260-
byId('attackBtn').addEventListener('click', () =>
261-
this.onEvent?.({ type: 'attack' }),
262-
);
263-
264-
byId('fireBtn').addEventListener('click', () =>
265-
this.onEvent?.({ type: 'fireAll' }),
266-
);
267-
268-
byId('skipCombatBtn').addEventListener('click', () =>
269-
this.onEvent?.({ type: 'skipCombat' }),
270-
);
271-
272-
byId('skipLogisticsBtn').addEventListener('click', () =>
273-
this.onEvent?.({
274-
type: 'skipLogistics',
275-
}),
276-
);
224+
private bindStaticButtons() {
225+
for (const binding of STATIC_BUTTON_BINDINGS) {
226+
byId(binding.id).addEventListener('click', () => {
227+
this.emit(binding.event);
228+
});
229+
}
230+
}
277231

278-
byId('confirmTransfersBtn').addEventListener('click', () =>
279-
this.onEvent?.({
280-
type: 'confirmTransfers',
281-
}),
282-
);
232+
private setDesktopLogVisible(visibleOnDesktop: boolean) {
233+
this.logVisible = visibleOnDesktop;
283234

284-
byId('rematchBtn').addEventListener('click', () =>
285-
this.onEvent?.({ type: 'rematch' }),
286-
);
235+
const visibility = buildScreenVisibility('hud', this.logVisible);
287236

288-
byId('exitBtn').addEventListener('click', () =>
289-
this.onEvent?.({ type: 'exit' }),
290-
);
237+
this.gameLogEl.style.display = visibility.gameLog;
238+
this.logShowBtn.style.display = visibility.logShowBtn;
239+
}
291240

292-
// Game log toggle
241+
private bindLogControls() {
293242
byId('logToggleBtn').addEventListener('click', () => {
294243
if (this.isMobile) {
295244
this.collapseMobileLog();
296245

297246
return;
298247
}
299248

300-
this.logVisible = false;
301-
302-
const visibility = buildScreenVisibility('hud', this.logVisible);
303-
304-
this.gameLogEl.style.display = visibility.gameLog;
305-
this.logShowBtn.style.display = visibility.logShowBtn;
249+
this.setDesktopLogVisible(false);
306250
});
307251

308252
this.logShowBtn.addEventListener('click', () => {
309-
this.logVisible = true;
310-
311-
const visibility = buildScreenVisibility('hud', this.logVisible);
312-
313-
this.gameLogEl.style.display = visibility.gameLog;
314-
this.logShowBtn.style.display = visibility.logShowBtn;
253+
this.setDesktopLogVisible(true);
315254
});
316255
}
317256

0 commit comments

Comments
 (0)