Skip to content

Commit 3f1524a

Browse files
committed
Adopt utility and DOM helpers across the codebase
Replace manual patterns with shared helpers: - sumBy/minBy/count replace reduce/filter chains in ai, combat, helpers - clamp replaces Math.max(min, Math.min(max, v)) in combat, camera, main - parseHexKey replaces inline key.split(',').map(Number) in combat, victory, ai - randomChoice replaces manual array indexing in game-engine - pickBy replaces Object.fromEntries(Object.entries().filter()) in session - byId replaces document.getElementById()! across all client code - show/hide/visible replace manual style.display toggling in ui, main, tutorial - el replaces createElement+className+textContent for simple elements
1 parent ae3a35f commit 3f1524a

File tree

15 files changed

+183
-212
lines changed

15 files changed

+183
-212
lines changed

src/client/game/combat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { canAttack, getCombatStrength, hasLineOfSight, hasLineOfSightToTarget } from '../../shared/combat';
22
import { type HexCoord, hexEqual } from '../../shared/hex';
33
import type { CombatAttack, GameState, SolarSystemMap } from '../../shared/types';
4+
import { clamp } from '../../shared/util';
45
import type { PlanningState } from '../renderer/renderer';
56

67
type CombatPlanningSnapshot = Pick<
@@ -101,7 +102,7 @@ export const hasSplitFireOptions = (state: GameState, playerId: number, queuedAt
101102

102103
const clampAttackStrength = (maxStrength: number, requestedStrength: number | null): number | null => {
103104
if (maxStrength <= 0) return null;
104-
return Math.max(1, Math.min(maxStrength, requestedStrength ?? maxStrength));
105+
return clamp(requestedStrength ?? maxStrength, 1, maxStrength);
105106
};
106107

107108
export const getCombatAttackerIdAtHex = (state: GameState, playerId: number, clickHex: HexCoord): string | null => {

src/client/game/helpers.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SHIP_STATS } from '../../shared/constants';
22
import type { AstrogationOrder, GameState, Ship } from '../../shared/types';
3+
import { count } from '../../shared/util';
34
import type { PlanningState } from '../renderer/renderer';
45

56
export interface GameOverStats {
@@ -59,8 +60,8 @@ const getObjective = (state: GameState, playerId: number): string => {
5960
const getFleetStatus = (state: GameState, playerId: number): string => {
6061
const myShips = state.ships.filter((ship) => ship.owner === playerId);
6162
const enemyShips = state.ships.filter((ship) => ship.owner !== playerId);
62-
const myAlive = myShips.filter((ship) => !ship.destroyed).length;
63-
const enemyAlive = enemyShips.filter((ship) => !ship.destroyed).length;
63+
const myAlive = count(myShips, (ship) => !ship.destroyed);
64+
const enemyAlive = count(enemyShips, (ship) => !ship.destroyed);
6465
const statusParts: string[] = [];
6566
if (myShips.length > 1 || enemyShips.length > 1) {
6667
statusParts.push(`⚔ ${myAlive}v${enemyAlive}`);
@@ -72,9 +73,9 @@ const getFleetStatus = (state: GameState, playerId: number): string => {
7273
}
7374

7475
const ordnanceParts: string[] = [];
75-
const mines = activeOrdnance.filter((ordnance) => ordnance.type === 'mine').length;
76-
const torpedoes = activeOrdnance.filter((ordnance) => ordnance.type === 'torpedo').length;
77-
const nukes = activeOrdnance.filter((ordnance) => ordnance.type === 'nuke').length;
76+
const mines = count(activeOrdnance, (ordnance) => ordnance.type === 'mine');
77+
const torpedoes = count(activeOrdnance, (ordnance) => ordnance.type === 'torpedo');
78+
const nukes = count(activeOrdnance, (ordnance) => ordnance.type === 'nuke');
7879
if (mines > 0) ordnanceParts.push(`${mines}M`);
7980
if (torpedoes > 0) ordnanceParts.push(`${torpedoes}T`);
8081
if (nukes > 0) ordnanceParts.push(`${nukes}N`);
@@ -130,9 +131,9 @@ export const getGameOverStats = (state: GameState, playerId: number): GameOverSt
130131
const enemyShips = state.ships.filter((ship) => ship.owner !== playerId);
131132
return {
132133
turns: state.turnNumber,
133-
myShipsAlive: myShips.filter((ship) => !ship.destroyed).length,
134+
myShipsAlive: count(myShips, (ship) => !ship.destroyed),
134135
myShipsTotal: myShips.length,
135-
enemyShipsAlive: enemyShips.filter((ship) => !ship.destroyed).length,
136+
enemyShipsAlive: count(enemyShips, (ship) => !ship.destroyed),
136137
enemyShipsTotal: enemyShips.length,
137138
};
138139
};

src/client/game/session.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { pickBy } from '../../shared/util';
2+
13
export interface TokenStoreEntry {
24
playerToken?: string;
35
inviteToken?: string;
@@ -29,7 +31,7 @@ export const loadTokenStore = (storage: Pick<StorageLike, 'getItem'>, key = TOKE
2931
};
3032

3133
export const pruneExpiredTokens = (store: TokenStore, now: number, ttlMs = TOKEN_TTL_MS): TokenStore => {
32-
return Object.fromEntries(Object.entries(store).filter(([, entry]) => now - entry.ts <= ttlMs));
34+
return pickBy(store, (entry) => now - entry.ts <= ttlMs) as TokenStore;
3335
};
3436

3537
export const saveTokenStore = (

src/client/main.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
S2C,
1919
ShipMovement,
2020
} from '../shared/types';
21+
import { clamp } from '../shared/util';
2122
import {
2223
initAudio,
2324
isMuted,
@@ -32,6 +33,7 @@ import {
3233
playWarning,
3334
setMuted,
3435
} from './audio';
36+
import { byId, hide, show } from './dom';
3537
import { deriveAIActionPlan } from './game/ai-flow';
3638
import { deriveScenarioBriefingEntries } from './game/briefing';
3739
import { deriveBurnChangePlan } from './game/burn';
@@ -111,12 +113,12 @@ class GameClient {
111113
private timerWarningPlayed = false;
112114

113115
constructor() {
114-
this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
116+
this.canvas = byId<HTMLCanvasElement>('gameCanvas');
115117
this.renderer = new Renderer(this.canvas);
116118
this.input = new InputHandler(this.canvas, this.renderer.camera, this.renderer.planningState);
117119
this.ui = new UIManager();
118120
this.tutorial = new Tutorial();
119-
this.tooltipEl = document.getElementById('shipTooltip')!;
121+
this.tooltipEl = byId('shipTooltip');
120122

121123
this.renderer.setMap(this.map);
122124
this.input.setMap(this.map);
@@ -170,11 +172,11 @@ class GameClient {
170172
});
171173

172174
// Help overlay
173-
document.getElementById('helpCloseBtn')!.addEventListener('click', () => this.toggleHelp());
174-
document.getElementById('helpBtn')!.addEventListener('click', () => this.toggleHelp());
175+
byId('helpCloseBtn').addEventListener('click', () => this.toggleHelp());
176+
byId('helpBtn').addEventListener('click', () => this.toggleHelp());
175177

176178
// Sound toggle
177-
const soundBtn = document.getElementById('soundBtn')!;
179+
const soundBtn = byId('soundBtn');
178180
this.updateSoundButton();
179181
soundBtn.addEventListener('click', () => {
180182
setMuted(!isMuted());
@@ -184,7 +186,7 @@ class GameClient {
184186
// Ship hover tooltip
185187
this.canvas.addEventListener('mousemove', (e) => this.updateTooltip(e.clientX, e.clientY));
186188
this.canvas.addEventListener('mouseleave', () => {
187-
this.tooltipEl.style.display = 'none';
189+
hide(this.tooltipEl);
188190
});
189191

190192
// Start render loop and audio
@@ -211,7 +213,7 @@ class GameClient {
211213
private setState(newState: ClientState) {
212214
this.state = newState;
213215
// Hide tooltip on state changes
214-
this.tooltipEl.style.display = 'none';
216+
hide(this.tooltipEl);
215217

216218
const entryPlan = deriveClientStateEntryPlan(newState, this.gameState, this.playerId);
217219
const screenPlan = deriveClientScreenPlan(
@@ -837,7 +839,7 @@ class GameClient {
837839
if (maxStrength <= 0) return;
838840

839841
const current = this.renderer.planningState.combatAttackStrength ?? maxStrength;
840-
this.renderer.planningState.combatAttackStrength = Math.max(1, Math.min(maxStrength, current + delta));
842+
this.renderer.planningState.combatAttackStrength = clamp(current + delta, 1, maxStrength);
841843
}
842844

843845
private resetCombatStrengthToMax() {
@@ -1255,12 +1257,12 @@ class GameClient {
12551257
const ship = getTooltipShip(gameState, this.state, this.playerId, hoverHex);
12561258

12571259
if (!ship || !gameState) {
1258-
this.tooltipEl.style.display = 'none';
1260+
hide(this.tooltipEl);
12591261
return;
12601262
}
12611263

12621264
this.tooltipEl.innerHTML = buildShipTooltipHtml(gameState, ship, this.playerId, this.map);
1263-
this.tooltipEl.style.display = 'block';
1265+
show(this.tooltipEl, 'block');
12641266
// Position tooltip offset from cursor
12651267
this.tooltipEl.style.left = `${screenX + 12}px`;
12661268
this.tooltipEl.style.top = `${screenY - 10}px`;

src/client/renderer/camera.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CAMERA_LERP_SPEED } from '../../shared/constants';
22
import type { PixelCoord } from '../../shared/hex';
3+
import { clamp } from '../../shared/util';
34

45
export class Camera {
56
x = 0;
@@ -54,7 +55,7 @@ export class Camera {
5455
}
5556

5657
zoomAt(sx: number, sy: number, factor: number) {
57-
const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.targetZoom * factor));
58+
const newZoom = clamp(this.targetZoom * factor, this.minZoom, this.maxZoom);
5859
const worldX = (sx - this.canvasW / 2) / this.targetZoom + this.targetX;
5960
const worldY = (sy - this.canvasH / 2) / this.targetZoom + this.targetY;
6061
this.targetZoom = newZoom;

src/client/renderer/combat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '../../shared/combat';
1313
import type { HexCoord } from '../../shared/hex';
1414
import type { CombatAttack, CombatResult, GameState, Ordnance, Ship, SolarSystemMap } from '../../shared/types';
15+
import { clamp } from '../../shared/util';
1516

1617
export interface CombatOverlayPlanningState {
1718
combatTargetId: string | null;
@@ -167,7 +168,7 @@ const formatPreviewLabel = (
167168
const shipTarget = target as Ship;
168169
const maxAttackStrength = getCombatStrength(attackers);
169170
const attackStrength =
170-
maxAttackStrength > 0 ? Math.max(1, Math.min(maxAttackStrength, requestedStrength ?? maxAttackStrength)) : 0;
171+
maxAttackStrength > 0 ? clamp(requestedStrength ?? maxAttackStrength, 1, maxAttackStrength) : 0;
171172
const defendStrength = getCombatStrength([shipTarget]);
172173
const odds = computeOdds(attackStrength, defendStrength);
173174
rangeMod = computeGroupRangeMod(attackers, shipTarget);

src/client/tutorial.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Tutorial state is persisted in localStorage so it only shows once.
66
*/
77

8+
import { byId, hide, show } from './dom';
9+
810
const STORAGE_KEY = 'deltav_tutorial_done';
911

1012
interface TutorialStep {
@@ -63,18 +65,18 @@ export class Tutorial {
6365
private activeStepId: string | null = null;
6466

6567
constructor() {
66-
this.tipEl = document.getElementById('tutorialTip')!;
67-
this.textEl = document.getElementById('tutorialTipText')!;
68-
this.progressEl = document.getElementById('tutorialProgress')!;
68+
this.tipEl = byId('tutorialTip');
69+
this.textEl = byId('tutorialTipText');
70+
this.progressEl = byId('tutorialProgress');
6971

7072
// Check if tutorial already completed
7173
if (localStorage.getItem(STORAGE_KEY) === '1') {
7274
this.completed = true;
7375
}
7476

7577
// Wire buttons
76-
document.getElementById('tutorialNextBtn')!.addEventListener('click', () => this.advance());
77-
document.getElementById('tutorialSkipBtn')!.addEventListener('click', () => this.skip());
78+
byId('tutorialNextBtn').addEventListener('click', () => this.advance());
79+
byId('tutorialSkipBtn').addEventListener('click', () => this.skip());
7880
}
7981

8082
/** Check if tutorial is active (not completed) */
@@ -103,14 +105,14 @@ export class Tutorial {
103105

104106
/** Hide the tutorial tip */
105107
hideTip() {
106-
this.tipEl.style.display = 'none';
108+
hide(this.tipEl);
107109
this.activeStepId = null;
108110
}
109111

110112
private showStep(step: TutorialStep) {
111113
this.activeStepId = step.id;
112114
this.textEl.textContent = step.text;
113-
this.tipEl.style.display = 'block';
115+
show(this.tipEl, 'block');
114116
// Re-trigger animation
115117
this.tipEl.style.animation = 'none';
116118
void this.tipEl.offsetHeight; // force reflow

src/client/ui/fleet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SHIP_STATS } from '../../shared/constants';
22
import type { FleetPurchase } from '../../shared/types';
3+
import { sumBy } from '../../shared/util';
34

45
export interface FleetShopItemView {
56
shipType: string;
@@ -28,7 +29,7 @@ export const getFleetShopTypes = () => {
2829
};
2930

3031
export const getFleetCartCost = (cart: FleetPurchase[]): number => {
31-
return cart.reduce((total, purchase) => total + (SHIP_STATS[purchase.shipType]?.cost ?? 0), 0);
32+
return sumBy(cart, (purchase) => SHIP_STATS[purchase.shipType]?.cost ?? 0);
3233
};
3334

3435
export const canAddFleetShip = (cart: FleetPurchase[], totalCredits: number, shipType: string): boolean => {

0 commit comments

Comments
 (0)