Skip to content

Commit be473bd

Browse files
committed
Add Controls axis directions input events and input family mapping - PR_26161_034-controls-axis-direction-and-input-events
1 parent 1f60960 commit be473bd

13 files changed

Lines changed: 1142 additions & 1337 deletions

docs_build/dev/reports/codex_changed_files.txt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,26 @@ M docs_build/dev/reports/coverage_changed_js_guardrail.txt
33
M docs_build/dev/reports/playwright_v8_coverage_report.txt
44
M src/dev-runtime/persistence/mock-db-store.js
55
M src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js
6-
M src/engine/input/InputService.js
76
M src/engine/input/NormalizedInputRegistry.js
87
M tests/input/NormalizedInputRegistry.test.mjs
98
M tests/playwright/tools/AdminDbViewer.spec.mjs
109
M tests/playwright/tools/InputMappingV2Tool.spec.mjs
1110
M toolbox/controls/controls.js
1211
M toolbox/controls/index.html
13-
?? docs_build/dev/reports/controls-normalized-default-input-architecture-report.md
12+
?? docs_build/dev/reports/controls-axis-direction-and-input-events-report.md
1413

1514
# git ls-files --others --exclude-standard
16-
docs_build/dev/reports/controls-normalized-default-input-architecture-report.md
15+
docs_build/dev/reports/controls-axis-direction-and-input-events-report.md
1716

1817
# git diff --stat
19-
.../dev/reports/coverage_changed_js_guardrail.txt | 10 +-
20-
.../dev/reports/playwright_v8_coverage_report.txt | 21 ++-
21-
src/dev-runtime/persistence/mock-db-store.js | 6 +-
22-
.../input-mapping-mock-repository.js | 93 +++++-------
23-
src/engine/input/InputService.js | 12 +-
24-
src/engine/input/NormalizedInputRegistry.js | 167 ++++++++++++++++++---
25-
tests/input/NormalizedInputRegistry.test.mjs | 75 +++++++--
26-
tests/playwright/tools/AdminDbViewer.spec.mjs | 10 +-
27-
tests/playwright/tools/InputMappingV2Tool.spec.mjs | 107 +++++++------
28-
toolbox/controls/controls.js | 103 ++++++-------
29-
toolbox/controls/index.html | 30 ++--
30-
11 files changed, 410 insertions(+), 224 deletions(-)
18+
.../dev/reports/coverage_changed_js_guardrail.txt | 4 +-
19+
.../dev/reports/playwright_v8_coverage_report.txt | 12 +-
20+
src/dev-runtime/persistence/mock-db-store.js | 2 +-
21+
.../input-mapping-mock-repository.js | 6 +
22+
src/engine/input/NormalizedInputRegistry.js | 83 ++++++++---
23+
tests/input/NormalizedInputRegistry.test.mjs | 56 ++++++--
24+
tests/playwright/tools/AdminDbViewer.spec.mjs | 2 +
25+
tests/playwright/tools/InputMappingV2Tool.spec.mjs | 75 +++++++---
26+
toolbox/controls/controls.js | 154 +++++++++++++++++----
27+
toolbox/controls/index.html | 4 +-
28+
10 files changed, 315 insertions(+), 83 deletions(-)

docs_build/dev/reports/codex_review.diff

Lines changed: 746 additions & 1239 deletions
Large diffs are not rendered by default.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# PR_26161_034 Controls Axis Direction And Input Events
2+
3+
## Branch Validation
4+
- PASS: Current git branch was verified as `main` before edits.
5+
6+
## Impacted Lane
7+
- Controls / Input Mapping
8+
- `src/engine/input` normalized input registry
9+
- Shared DB/mock adapter Controls tables
10+
11+
## Playwright Impacted
12+
- Yes.
13+
14+
## Requirement Checklist
15+
- PASS: Controls status remains Wireframe. Playwright validates registry metadata for `controls` remains `wireframe`.
16+
- PASS: Controls data remains DB-backed through the shared DB/mock adapter. Mappings persist through `game_input_mappings`; player input mappings persist through `player_controller_profiles`.
17+
- PASS: Normalized axis choices are split into `move.x-`, `move.x+`, `move.y-`, `move.y+`, `aim.x-`, `aim.x+`, `aim.y-`, `aim.y+`.
18+
- PASS: Axis +/- directions use separate normalized input dropdown choices in Player Input Mapping.
19+
- PASS: Deadzone belongs to the physical axis and is shared by both +/- directions; Playwright verifies one Axis0 row with one Deadzone control and both direction dropdowns.
20+
- PASS: Invert belongs to the physical axis; engine normalization test validates invert changes axis direction resolution.
21+
- PASS: User-facing `Assigned Normalized Input` copy is removed; Playwright verifies it is absent.
22+
- PASS: Dropdown/list heights are consistent; Playwright compares generated select heights in Game Input Mapping and Player Input Mapping edit rows.
23+
- PASS: Game Input Mapping includes an Input Event column with `Press`, `Down`, and `Release`.
24+
- PASS: Game Input Mapping includes an Input Family column with `Keyboard`, `Mouse`, `Gamepad`, and `Joystick`.
25+
- PASS: Input Family stores only the family value; Playwright verifies `game_input_mappings` does not store controller name/id, binding, input device, or raw physical input.
26+
- PASS: Normalized architecture is preserved: Player Input Mapping maps physical input to normalized input; Game Input Mapping maps normalized input to game action.
27+
- PASS: Default fallback profiles, reset confirmation, accordions, status logging, and DB persistence are preserved by targeted Playwright.
28+
- PASS: No sample JSON alignment, auth behavior, production account system, or unrelated rewrites were added.
29+
30+
## Testing Performed
31+
- PASS: `node --check src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js`
32+
- PASS: `node --check src/dev-runtime/persistence/mock-db-store.js`
33+
- PASS: `node --check src/engine/input/NormalizedInputRegistry.js`
34+
- PASS: `node --check toolbox/controls/controls.js`
35+
- PASS: `node --check tests/input/NormalizedInputRegistry.test.mjs`
36+
- PASS: `node --check tests/playwright/tools/InputMappingV2Tool.spec.mjs`
37+
- PASS: `node --check tests/playwright/tools/AdminDbViewer.spec.mjs`
38+
- PASS: `node tests/input/NormalizedInputRegistry.test.mjs`
39+
- PASS: `npx playwright test tests/playwright/tools/InputMappingV2Tool.spec.mjs`
40+
41+
## Manual Validation Steps
42+
1. Open `/toolbox/controls/index.html`.
43+
2. In Game Input Mapping, click Add Game Control and confirm columns include Normalized Input, Input Family, Input Event, Game Action, Object, State, and Actions.
44+
3. Select `move.x-`, `Gamepad`, and `Release`, save, reload, and confirm the row persists.
45+
4. Inspect the mock DB snapshot and confirm `game_input_mappings` stores `inputFamily` and `inputEventPhase` without controller id/name or raw physical input.
46+
5. Create a gamepad Player Input Mapping and edit `Axis0`.
47+
6. Confirm `Axis0` has separate Negative and Positive normalized input dropdowns, one Deadzone field, and one Invert checkbox.
48+
7. Confirm `Assigned Normalized Input` does not appear.
49+
8. Save, reload, and confirm the Axis0 mapping persists with one shared deadzone/invert pair and both direction values.
50+
51+
## Skipped Lanes
52+
- Full samples validation was skipped by request.
53+
- Full repository Playwright was skipped because the impacted browser lane is Controls/Input Mapping.
54+
- Project Workspace validation was skipped because this PR does not change Project Workspace behavior.
55+
56+
## Runtime Engine Behavior
57+
- No production game runtime behavior was added.
58+
- `src/engine/input/NormalizedInputRegistry.js` received the smallest shared input extension required for directional normalized axes and deadzone/invert direction resolution.
59+
60+
## Required Artifacts
61+
- `docs_build/dev/reports/codex_review.diff`
62+
- `docs_build/dev/reports/codex_changed_files.txt`
63+
- `docs_build/dev/reports/playwright_v8_coverage_report.txt`
64+
- `tmp/PR_26161_034-controls-axis-direction-and-input-events_delta.zip`
65+
66+
## Completion Audit
67+
- PASS: Original PR request was re-read before packaging.
68+
- PASS: Every requested item is implemented, validated, and marked PASS above.

docs_build/dev/reports/coverage_changed_js_guardrail.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Changed runtime JS files considered:
99
(0%) src/dev-runtime/persistence/mock-db-store.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
1010
(0%) src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
1111
(32%) src/engine/input/InputService.js - executed lines 451/451; executed functions 21/66
12-
(93%) toolbox/controls/controls.js - executed lines 1412/1412; executed functions 154/165
13-
(100%) src/engine/input/NormalizedInputRegistry.js - executed lines 242/242; executed functions 26/26
12+
(94%) toolbox/controls/controls.js - executed lines 1509/1509; executed functions 160/171
13+
(96%) src/engine/input/NormalizedInputRegistry.js - executed lines 284/284; executed functions 27/28
1414

1515
Guardrail warnings:
1616
(0%) src/dev-runtime/persistence/mock-db-store.js - WARNING: changed runtime JS file missing from coverage; advisory only

docs_build/dev/reports/playwright_v8_coverage_report.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ Changed runtime JS files covered:
2020
(0%) src/dev-runtime/persistence/mock-db-store.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
2121
(0%) src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only
2222
(32%) src/engine/input/InputService.js - executed lines 451/451; executed functions 21/66
23-
(93%) toolbox/controls/controls.js - executed lines 1412/1412; executed functions 154/165
24-
(100%) src/engine/input/NormalizedInputRegistry.js - executed lines 242/242; executed functions 26/26
23+
(94%) toolbox/controls/controls.js - executed lines 1509/1509; executed functions 160/171
24+
(96%) src/engine/input/NormalizedInputRegistry.js - executed lines 284/284; executed functions 27/28
2525

2626
Files with executed line/function counts where available:
2727
(11%) src/engine/input/InputMappingManifest.js - executed lines 78/78; executed functions 1/9
@@ -38,8 +38,8 @@ Files with executed line/function counts where available:
3838
(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
3939
(75%) toolbox/tool-registry-api-client.js - executed lines 152/152; executed functions 21/28
4040
(80%) src/engine/input/GamepadState.js - executed lines 100/100; executed functions 16/20
41-
(93%) toolbox/controls/controls.js - executed lines 1412/1412; executed functions 154/165
42-
(100%) src/engine/input/NormalizedInputRegistry.js - executed lines 242/242; executed functions 26/26
41+
(94%) toolbox/controls/controls.js - executed lines 1509/1509; executed functions 160/171
42+
(96%) src/engine/input/NormalizedInputRegistry.js - executed lines 284/284; executed functions 27/28
4343
(100%) toolbox/controls/controls-api-client.js - executed lines 12/12; executed functions 2/2
4444
(100%) toolbox/objects/objects-api-client.js - executed lines 10/10; executed functions 2/2
4545

@@ -55,5 +55,5 @@ Changed JS files considered:
5555
(0%) tests/playwright/tools/AdminDbViewer.spec.mjs - changed JS file not collected as browser runtime coverage
5656
(0%) tests/playwright/tools/InputMappingV2Tool.spec.mjs - changed JS file not collected as browser runtime coverage
5757
(32%) src/engine/input/InputService.js - changed JS file with browser V8 coverage
58-
(93%) toolbox/controls/controls.js - changed JS file with browser V8 coverage
59-
(100%) src/engine/input/NormalizedInputRegistry.js - changed JS file with browser V8 coverage
58+
(94%) toolbox/controls/controls.js - changed JS file with browser V8 coverage
59+
(96%) src/engine/input/NormalizedInputRegistry.js - changed JS file with browser V8 coverage

src/dev-runtime/persistence/mock-db-store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const MOCK_DB_TABLE_SCHEMAS = Object.freeze({
136136
game_configuration_records: Object.freeze(["key", "projectKey", "status", "summary", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
137137
game_configuration_validation_items: Object.freeze(["key", "projectKey", "label", "status", "action", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
138138
object_definition_records: Object.freeze(["key", "id", "projectId", "name", "type", "state", "modelType", "renderType", "renderAssetKey", "renderPreviewPath", "capabilities", "behavior", "interaction", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
139-
game_input_mappings: Object.freeze(["key", "id", "projectId", "objectKey", "objectName", "gameAction", "gameActionLabel", "normalizedInput", "state", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
139+
game_input_mappings: Object.freeze(["key", "id", "projectId", "objectKey", "objectName", "gameAction", "gameActionLabel", "normalizedInput", "inputFamily", "inputEventPhase", "state", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
140140
player_controller_profiles: Object.freeze(["key", "id", "playerId", "deviceType", "controllerName", "controllerId", "profileName", "inputs", "inputMappings", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
141141
input_custom_action_records: Object.freeze(["key", "id", "projectId", "label", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]),
142142
project_journey_note_types: Object.freeze(["key", "typeSlug", "name", "seeded", "userExtensible", "createdAt", "updatedAt", "createdBy", "updatedBy"]),

src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ function normalizeProfileInputMappings(value) {
7171
return (Array.isArray(value) ? value : []).map((mapping) => ({
7272
deadzone: Number.isFinite(Number(mapping?.deadzone)) ? Number(mapping.deadzone) : 0.2,
7373
invert: Boolean(mapping?.invert),
74+
negativeNormalizedInput: normalizeText(mapping?.negativeNormalizedInput),
7475
normalizedInput: normalizeText(mapping?.normalizedInput),
7576
physicalInput: normalizeText(mapping?.physicalInput || mapping?.input),
77+
positiveNormalizedInput: normalizeText(mapping?.positiveNormalizedInput),
7678
})).filter((mapping) => mapping.physicalInput);
7779
}
7880

@@ -122,6 +124,8 @@ function mappingFromRecord(record = {}) {
122124
action,
123125
actionLabel,
124126
id: normalizeText(record.id),
127+
inputEventPhase: normalizeText(record.inputEventPhase) || "Down",
128+
inputFamily: normalizeText(record.inputFamily) || "Keyboard",
125129
normalizedInput: normalizeText(record.normalizedInput),
126130
objectKey: normalizeText(record.objectKey),
127131
objectName: normalizeText(record.objectName),
@@ -289,6 +293,8 @@ export function createInputMappingToolMockRepository(options = {}) {
289293
gameAction: action,
290294
gameActionLabel: normalizeText(mapping.actionLabel),
291295
id,
296+
inputEventPhase: normalizeText(mapping.inputEventPhase) || "Down",
297+
inputFamily: normalizeText(mapping.inputFamily) || "Keyboard",
292298
key: previous?.key,
293299
normalizedInput,
294300
objectKey,

src/engine/input/NormalizedInputRegistry.js

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ NormalizedInputRegistry.js
66
*/
77

88
export const NORMALIZED_INPUT_REGISTRY = Object.freeze([
9-
Object.freeze({ id: 'move.x', label: 'move.x', kind: 'axis' }),
10-
Object.freeze({ id: 'move.y', label: 'move.y', kind: 'axis' }),
11-
Object.freeze({ id: 'aim.x', label: 'aim.x', kind: 'axis' }),
12-
Object.freeze({ id: 'aim.y', label: 'aim.y', kind: 'axis' }),
9+
Object.freeze({ id: 'move.x-', label: 'move.x-', kind: 'axis' }),
10+
Object.freeze({ id: 'move.x+', label: 'move.x+', kind: 'axis' }),
11+
Object.freeze({ id: 'move.y-', label: 'move.y-', kind: 'axis' }),
12+
Object.freeze({ id: 'move.y+', label: 'move.y+', kind: 'axis' }),
13+
Object.freeze({ id: 'aim.x-', label: 'aim.x-', kind: 'axis' }),
14+
Object.freeze({ id: 'aim.x+', label: 'aim.x+', kind: 'axis' }),
15+
Object.freeze({ id: 'aim.y-', label: 'aim.y-', kind: 'axis' }),
16+
Object.freeze({ id: 'aim.y+', label: 'aim.y+', kind: 'axis' }),
1317
Object.freeze({ id: 'action.primary', label: 'action.primary', kind: 'action' }),
1418
Object.freeze({ id: 'action.secondary', label: 'action.secondary', kind: 'action' }),
1519
Object.freeze({ id: 'action.tertiary', label: 'action.tertiary', kind: 'action' }),
@@ -30,14 +34,14 @@ export const NORMALIZED_INPUT_REGISTRY = Object.freeze([
3034
const NORMALIZED_INPUT_IDS = new Set(NORMALIZED_INPUT_REGISTRY.map((input) => input.id));
3135

3236
const DEFAULT_PHYSICAL_INPUT_MAP = Object.freeze({
33-
ArrowDown: 'move.y',
34-
ArrowLeft: 'move.x',
35-
ArrowRight: 'move.x',
36-
ArrowUp: 'move.y',
37-
Axis0: 'move.x',
38-
Axis1: 'move.y',
39-
Axis2: 'aim.x',
40-
Axis3: 'aim.y',
37+
ArrowDown: 'move.y+',
38+
ArrowLeft: 'move.x-',
39+
ArrowRight: 'move.x+',
40+
ArrowUp: 'move.y-',
41+
Axis0: 'move.x+',
42+
Axis1: 'move.y+',
43+
Axis2: 'aim.x+',
44+
Axis3: 'aim.y+',
4145
Button0: 'action.primary',
4246
Button1: 'action.secondary',
4347
Button2: 'action.tertiary',
@@ -50,20 +54,29 @@ const DEFAULT_PHYSICAL_INPUT_MAP = Object.freeze({
5054
'DPad Up': 'dpad.up',
5155
Enter: 'action.confirm',
5256
Escape: 'action.cancel',
53-
KeyA: 'move.x',
54-
KeyD: 'move.x',
57+
KeyA: 'move.x-',
58+
KeyD: 'move.x+',
5559
KeyP: 'action.pause',
56-
KeyS: 'move.y',
57-
KeyW: 'move.y',
60+
KeyS: 'move.y+',
61+
KeyW: 'move.y-',
5862
MouseButton0: 'action.primary',
5963
MouseButton2: 'action.secondary',
60-
MouseX: 'aim.x',
61-
MouseY: 'aim.y',
64+
MouseX: 'aim.x+',
65+
MouseY: 'aim.y+',
6266
Space: 'action.primary',
6367
'Trigger Left': 'trigger.left',
6468
'Trigger Right': 'trigger.right',
6569
});
6670

71+
const DEFAULT_PHYSICAL_AXIS_DIRECTION_MAP = Object.freeze({
72+
Axis0: Object.freeze({ negative: 'move.x-', positive: 'move.x+' }),
73+
Axis1: Object.freeze({ negative: 'move.y-', positive: 'move.y+' }),
74+
Axis2: Object.freeze({ negative: 'aim.x-', positive: 'aim.x+' }),
75+
Axis3: Object.freeze({ negative: 'aim.y-', positive: 'aim.y+' }),
76+
MouseX: Object.freeze({ negative: 'aim.x-', positive: 'aim.x+' }),
77+
MouseY: Object.freeze({ negative: 'aim.y-', positive: 'aim.y+' }),
78+
});
79+
6780
const SYSTEM_DEFAULT_GAMEPAD_INPUTS = Object.freeze([
6881
'Button0',
6982
'Button1',
@@ -130,6 +143,14 @@ export function defaultNormalizedInputForPhysicalInput(physicalInput) {
130143
return DEFAULT_PHYSICAL_INPUT_MAP[inputName] || '';
131144
}
132145

146+
export function defaultNormalizedInputDirectionsForPhysicalInput(physicalInput) {
147+
const directions = DEFAULT_PHYSICAL_AXIS_DIRECTION_MAP[normalizeInputName(physicalInput)];
148+
return {
149+
negative: directions?.negative || '',
150+
positive: directions?.positive || '',
151+
};
152+
}
153+
133154
export function systemDefaultInputMappings(deviceType = '') {
134155
const inputs = normalizeDeviceType(deviceType) === 'gamepad'
135156
? SYSTEM_DEFAULT_GAMEPAD_INPUTS
@@ -225,12 +246,21 @@ export function normalizedInputIsAnalog(inputId) {
225246
export function normalizeProfileInputMapping(inputName, source = {}) {
226247
const physicalInput = normalizeInputName(source.physicalInput || source.input || inputName);
227248
const fallbackNormalizedInput = defaultNormalizedInputForPhysicalInput(physicalInput);
249+
const fallbackDirections = defaultNormalizedInputDirectionsForPhysicalInput(physicalInput);
228250
const deadzone = Number(source.deadzone);
251+
const negativeNormalizedInput = normalizeNormalizedInput(source.negativeNormalizedInput, fallbackDirections.negative);
252+
const positiveNormalizedInput = normalizeNormalizedInput(source.positiveNormalizedInput, fallbackDirections.positive);
253+
const normalizedInput = normalizeNormalizedInput(
254+
source.normalizedInput,
255+
positiveNormalizedInput || fallbackNormalizedInput || negativeNormalizedInput,
256+
);
229257
return {
230258
deadzone: Number.isFinite(deadzone) ? Math.max(0, Math.min(1, deadzone)) : 0.2,
231259
invert: Boolean(source.invert),
232-
normalizedInput: normalizeNormalizedInput(source.normalizedInput, fallbackNormalizedInput),
260+
negativeNormalizedInput,
261+
normalizedInput,
233262
physicalInput,
263+
positiveNormalizedInput,
234264
};
235265
}
236266

@@ -255,6 +285,21 @@ function normalizeInputName(value) {
255285
return String(value || '').trim();
256286
}
257287

288+
export function resolvePhysicalAxisNormalizedInput(inputMapping = {}, axisValue = 0) {
289+
const deadzone = Number.isFinite(Number(inputMapping.deadzone))
290+
? Math.max(0, Math.min(1, Number(inputMapping.deadzone)))
291+
: 0.2;
292+
const value = Number(axisValue) || 0;
293+
if (Math.abs(value) < deadzone) {
294+
return '';
295+
}
296+
const resolvedValue = inputMapping.invert ? -value : value;
297+
const directionInput = resolvedValue < 0
298+
? inputMapping.negativeNormalizedInput
299+
: inputMapping.positiveNormalizedInput;
300+
return normalizeNormalizedInput(directionInput);
301+
}
302+
258303
function normalizeDeviceType(value) {
259304
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
260305
if (normalized === 'keyboard' || normalized === 'mouse' || normalized === 'keyboard-mouse') {

0 commit comments

Comments
 (0)