Skip to content

Commit 81c6c4e

Browse files
authored
Merge pull request #67 from sam-mfb/control-cleanup
Control cleanup
2 parents dcb09ac + ecff6e4 commit 81c6c4e

File tree

6 files changed

+290
-27
lines changed

6 files changed

+290
-27
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* @fileoverview Tests for blankControls utility
3+
*/
4+
5+
import { describe, it, expect } from 'vitest'
6+
import { blankControls } from './blankControls'
7+
8+
describe('blankControls', () => {
9+
it('creates blank controls with all values false', () => {
10+
const source = {
11+
a: true,
12+
b: true,
13+
c: true
14+
}
15+
16+
const result = blankControls(source)
17+
18+
expect(result).toEqual({
19+
a: false,
20+
b: false,
21+
c: false
22+
})
23+
})
24+
25+
it('preserves keys from source regardless of their values', () => {
26+
const source = {
27+
foo: false,
28+
bar: false,
29+
baz: false
30+
}
31+
32+
const result = blankControls(source)
33+
34+
expect(result).toEqual({
35+
foo: false,
36+
bar: false,
37+
baz: false
38+
})
39+
})
40+
41+
it('applies overrides to specific controls', () => {
42+
const source = {
43+
a: true,
44+
b: true,
45+
c: true
46+
}
47+
48+
const result = blankControls(source, { b: true })
49+
50+
expect(result).toEqual({
51+
a: false,
52+
b: true,
53+
c: false
54+
})
55+
})
56+
57+
it('applies multiple overrides', () => {
58+
const source = {
59+
x: true,
60+
y: true,
61+
z: true
62+
}
63+
64+
const result = blankControls(source, { x: true, z: true })
65+
66+
expect(result).toEqual({
67+
x: true,
68+
y: false,
69+
z: true
70+
})
71+
})
72+
73+
it('works with no overrides', () => {
74+
const source = {
75+
thrust: true,
76+
fire: true,
77+
shield: true
78+
}
79+
80+
const result = blankControls(source, {})
81+
82+
expect(result).toEqual({
83+
thrust: false,
84+
fire: false,
85+
shield: false
86+
})
87+
})
88+
89+
it('handles source with different property names', () => {
90+
const source = {
91+
propA: true,
92+
propB: false,
93+
propC: true
94+
}
95+
96+
const result = blankControls(source, { propC: true })
97+
98+
expect(result.propA).toBe(false)
99+
expect(result.propB).toBe(false)
100+
expect(result.propC).toBe(true)
101+
})
102+
})

src/core/controls/blankControls.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @fileoverview Utility for creating blank control matrices
3+
*/
4+
5+
/**
6+
* Creates a blank control matrix with all controls set to false.
7+
* Optionally allows specific controls to be overridden.
8+
*
9+
* @param source - A source control matrix to use as a template
10+
* @param overrides - Optional partial control matrix to override specific controls
11+
* @returns A complete control matrix with all controls false except overrides
12+
*/
13+
export function blankControls<T extends Record<string, boolean>>(
14+
source: T,
15+
overrides?: Partial<T>
16+
): T {
17+
const blank = {} as T
18+
const keys = Object.keys(source) as (keyof T)[]
19+
20+
// Set all controls to false
21+
for (const key of keys) {
22+
blank[key] = (overrides?.[key] ?? false) as T[keyof T]
23+
}
24+
25+
return blank
26+
}

src/core/controls/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ export {
2121
} from './types'
2222

2323
export { getControls } from './getControls'
24+
25+
export { mergeControls } from './mergeControls'
26+
27+
export { blankControls } from './blankControls'
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @fileoverview Tests for mergeControls utility
3+
*/
4+
5+
import { describe, it, expect } from 'vitest'
6+
import { mergeControls } from './mergeControls'
7+
8+
describe('mergeControls', () => {
9+
it('merges two control sets with OR logic', () => {
10+
const controls1 = {
11+
a: true,
12+
b: false,
13+
c: true
14+
}
15+
16+
const controls2 = {
17+
a: false,
18+
b: true,
19+
c: true
20+
}
21+
22+
const result = mergeControls(controls1, controls2)
23+
24+
expect(result).toEqual({
25+
a: true, // true OR false = true
26+
b: true, // false OR true = true
27+
c: true // true OR true = true
28+
})
29+
})
30+
31+
it('handles all false controls', () => {
32+
const controls1 = {
33+
x: false,
34+
y: false,
35+
z: false
36+
}
37+
38+
const controls2 = {
39+
x: false,
40+
y: false,
41+
z: false
42+
}
43+
44+
const result = mergeControls(controls1, controls2)
45+
46+
expect(result).toEqual({
47+
x: false,
48+
y: false,
49+
z: false
50+
})
51+
})
52+
53+
it('handles all true in first set', () => {
54+
const controls1 = {
55+
foo: true,
56+
bar: true
57+
}
58+
59+
const controls2 = {
60+
foo: false,
61+
bar: false
62+
}
63+
64+
const result = mergeControls(controls1, controls2)
65+
66+
expect(result).toEqual({
67+
foo: true,
68+
bar: true
69+
})
70+
})
71+
72+
it('merges more than two control sets', () => {
73+
const controls1 = {
74+
a: true,
75+
b: false,
76+
c: false
77+
}
78+
79+
const controls2 = {
80+
a: false,
81+
b: true,
82+
c: false
83+
}
84+
85+
const controls3 = {
86+
a: false,
87+
b: false,
88+
c: true
89+
}
90+
91+
const result = mergeControls(controls1, controls2, controls3)
92+
93+
expect(result.a).toBe(true)
94+
expect(result.b).toBe(true)
95+
expect(result.c).toBe(true)
96+
})
97+
98+
it('works with different property names', () => {
99+
const controls1 = {
100+
thrust: true,
101+
shield: false
102+
}
103+
104+
const controls2 = {
105+
thrust: false,
106+
shield: true
107+
}
108+
109+
const result = mergeControls(controls1, controls2)
110+
111+
expect(result.thrust).toBe(true)
112+
expect(result.shield).toBe(true)
113+
})
114+
115+
it('throws error when called with no arguments', () => {
116+
expect(() => mergeControls()).toThrow(
117+
'mergeControls requires at least one control set'
118+
)
119+
})
120+
})

src/core/controls/mergeControls.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @fileoverview Utility for merging multiple control sources
3+
*/
4+
5+
/**
6+
* Merges multiple control matrices using OR logic.
7+
* Type-safe approach that ensures all control properties are handled.
8+
*
9+
* @param controlSets - One or more control matrices to merge
10+
* @returns A merged control matrix where each control is true if any source has it true
11+
*/
12+
export function mergeControls<T extends Record<string, boolean>>(
13+
...controlSets: T[]
14+
): T {
15+
if (controlSets.length === 0) {
16+
throw new Error('mergeControls requires at least one control set')
17+
}
18+
19+
const result = { ...controlSets[0] } as T
20+
const keys = Object.keys(result) as (keyof T)[]
21+
22+
for (const key of keys) {
23+
result[key] = controlSets.some(
24+
controls => controls[key]
25+
) as unknown as T[keyof T]
26+
}
27+
28+
return result
29+
}

src/game/components/GameRenderer.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import React, { useEffect, useRef, useState } from 'react'
22
import type { FrameInfo, KeyInfo, MonochromeBitmap } from '@lib/bitmap'
33
import { useAppDispatch, useAppSelector, type RootState } from '../store'
44
import { togglePause, showMap, hideMap, pause, unpause } from '../gameSlice'
5-
import { getControls, type ControlMatrix } from '@core/controls'
5+
import {
6+
getControls,
7+
mergeControls,
8+
blankControls,
9+
type ControlMatrix
10+
} from '@core/controls'
611
import { Map } from './Map'
712
import { bitmapToCollisionItem, type CollisionService } from '@/core/collision'
813
import { Collision } from '@/core/collision/constants'
@@ -136,37 +141,14 @@ const GameRenderer: React.FC<GameRendererProps> = ({
136141
const keyboardControls = getControls(keyInfo, bindings)
137142

138143
// Merge keyboard and touch controls (OR logic for each control)
139-
const mergedControls: ControlMatrix = {
140-
thrust: keyboardControls.thrust || touchControls.thrust,
141-
left: keyboardControls.left || touchControls.left,
142-
right: keyboardControls.right || touchControls.right,
143-
fire: keyboardControls.fire || touchControls.fire,
144-
shield: keyboardControls.shield || touchControls.shield,
145-
selfDestruct:
146-
keyboardControls.selfDestruct || touchControls.selfDestruct,
147-
pause: keyboardControls.pause || touchControls.pause,
148-
quit: keyboardControls.quit || touchControls.quit,
149-
nextLevel: keyboardControls.nextLevel || touchControls.nextLevel,
150-
extraLife: keyboardControls.extraLife || touchControls.extraLife,
151-
map: keyboardControls.map || touchControls.map
152-
}
144+
const mergedControls = mergeControls(keyboardControls, touchControls)
153145

154146
// If map is showing, use blank controls (all false) to prevent game input
155147
const controls = showMapState
156-
? {
157-
thrust: false,
158-
left: false,
159-
right: false,
160-
fire: false,
161-
shield: false,
162-
selfDestruct: false,
163-
pause: false,
164-
quit: false,
165-
nextLevel: false,
166-
extraLife: false,
148+
? blankControls(mergedControls, {
167149
// need to still detect the map key
168150
map: mergedControls.map
169-
}
151+
})
170152
: mergedControls
171153

172154
if (controls.map) {

0 commit comments

Comments
 (0)