Skip to content

Commit 55e066a

Browse files
committed
Update User Controls defaults and enforce profile naming rules - PR_26162_050-user-controls-defaults-and-profile-rules
1 parent 31e03ad commit 55e066a

9 files changed

Lines changed: 290 additions & 76 deletions

account/user-controls-page.js

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111

1212
const DEVICE_POLL_INTERVAL_MS = 1200;
1313
const INPUT_CAPTURE_TIMEOUT_MS = 5000;
14-
const KEYBOARD_INPUTS = Object.freeze(["KeyW", "KeyA", "KeyS", "KeyD", "Space", "Enter", "Escape", "KeyP"]);
15-
const MOUSE_INPUTS = Object.freeze(["MouseButton0", "MouseButton2", "MouseX", "MouseY"]);
14+
const KEYBOARD_INPUTS = Object.freeze(["KeyW", "KeyA", "KeyS", "KeyD", "Space", "ShiftLeft", "ControlLeft", "Enter", "Backspace", "KeyP"]);
15+
const MOUSE_INPUTS = Object.freeze(["MouseButton0", "MouseButton2", "MouseButton1", "MouseWheelUp", "MouseWheelDown", "MouseX-", "MouseX+", "MouseY-", "MouseY+"]);
1616
const KEYBOARD_MOUSE_EXCLUDED_NORMALIZED_PREFIXES = Object.freeze(["dpad.", "trigger."]);
1717
const SUPPORTED_CONTROL_TYPES = Object.freeze([
1818
"Keyboard Key",
@@ -99,8 +99,13 @@ function rangeValueLabel(value, unit = "") {
9999
return `${value}${unit}`;
100100
}
101101

102-
function physicalInputSupportsTuning(physicalInput) {
103-
return physicalInputIsAnalog(physicalInput) || Boolean(physicalInputSensitivityDescriptor(physicalInput));
102+
function physicalInputSupportsDeadzoneInvert(physicalInput) {
103+
const normalizedInput = normalizeText(physicalInput);
104+
const lowerName = normalizedInput.toLowerCase();
105+
return physicalInputIsAnalog(normalizedInput)
106+
|| lowerName.includes("trigger")
107+
|| normalizedInput === "LT"
108+
|| normalizedInput === "RT";
104109
}
105110

106111
function createSliderControl({ ariaLabel, dataName, defaultValue, index, max, min, step, unit, value }) {
@@ -315,7 +320,7 @@ export class AccountUserControlsPage {
315320
this.profiles = result.profiles.map((profile) => this.normalizeProfile(profile));
316321
return true;
317322
}
318-
this.setStatus(result?.message || "WARN: Account user controls could not reach the shared DB adapter.");
323+
this.setStatus(result?.message || "WARN: User Controls could not reach the shared DB adapter.");
319324
return false;
320325
}
321326

@@ -330,6 +335,43 @@ export class AccountUserControlsPage {
330335
});
331336
}
332337

338+
uniqueProfileForDevice(device) {
339+
const family = this.profileListFamily(device);
340+
const baseControllerName = normalizeText(device.controllerName) || family;
341+
const baseProfileName = normalizeText(device.mappingProfile) || `${baseControllerName} Profile`;
342+
const profileNames = new Set(this.profiles
343+
.filter((profile) => this.profileListFamily(profile) === family)
344+
.map((profile) => normalizeText(profile.mappingProfile).toLowerCase()));
345+
let controllerName = baseControllerName;
346+
let mappingProfile = baseProfileName;
347+
let suffix = 2;
348+
while (profileNames.has(mappingProfile.toLowerCase())) {
349+
controllerName = `${baseControllerName} ${suffix}`;
350+
mappingProfile = `${baseControllerName} ${suffix} Profile`;
351+
suffix += 1;
352+
}
353+
return this.normalizeProfile({
354+
controllerId: device.controllerId,
355+
controllerName,
356+
deviceType: device.deviceType,
357+
inputMappings: normalizeProfileInputMappings(device.inputs),
358+
inputs: device.inputs,
359+
mappingProfile,
360+
});
361+
}
362+
363+
createProfile(device) {
364+
const profile = this.uniqueProfileForDevice(device);
365+
if (!this.saveProfiles([profile, ...this.profiles])) {
366+
this.setStatus("FAIL: User Controls could not reach the shared DB adapter.");
367+
return;
368+
}
369+
this.editingProfile = null;
370+
this.viewingDefaultFamily = "";
371+
this.renderProfiles();
372+
this.setStatus(`PASS: Created ${profile.mappingProfile}. Use Edit to change it.`);
373+
}
374+
333375
renderDeviceSelect() {
334376
if (!this.elements.deviceSelect) {
335377
return;
@@ -661,8 +703,8 @@ export class AccountUserControlsPage {
661703
return row;
662704
}
663705
row.append(
664-
tableCell(physicalInputSupportsTuning(inputMapping.physicalInput) ? String(inputMapping.deadzone) : "N/A"),
665-
tableCell(physicalInputSupportsTuning(inputMapping.physicalInput) && inputMapping.invert ? "On" : physicalInputSupportsTuning(inputMapping.physicalInput) ? "Off" : "N/A"),
706+
tableCell(physicalInputSupportsDeadzoneInvert(inputMapping.physicalInput) ? String(inputMapping.deadzone) : "N/A"),
707+
tableCell(physicalInputSupportsDeadzoneInvert(inputMapping.physicalInput) && inputMapping.invert ? "On" : physicalInputSupportsDeadzoneInvert(inputMapping.physicalInput) ? "Off" : "N/A"),
666708
tableCell(physicalInputSensitivityDescriptor(inputMapping.physicalInput)
667709
? rangeValueLabel(inputMapping.sensitivity ?? physicalInputSensitivityDescriptor(inputMapping.physicalInput).defaultValue, physicalInputSensitivityDescriptor(inputMapping.physicalInput).unit)
668710
: "N/A"),
@@ -830,13 +872,6 @@ export class AccountUserControlsPage {
830872
});
831873
positiveSelect.dataset.accountUserControlsInputPositive = String(index);
832874
stack.append(labeledControl("Negative", negativeSelect), labeledControl("Positive", positiveSelect));
833-
} else if (family === "Keyboard") {
834-
const value = normalizeText(inputMapping.normalizedInput) || "Unassigned";
835-
const readonlyControl = document.createElement("span");
836-
readonlyControl.className = "status";
837-
readonlyControl.dataset.accountUserControlsInputNormalizedReadonly = String(index);
838-
readonlyControl.textContent = value;
839-
stack.append(readonlyControl);
840875
} else {
841876
const select = selectControl({
842877
ariaLabel: `${inputMapping.physicalInput} normalized control`,
@@ -857,7 +892,7 @@ export class AccountUserControlsPage {
857892
}
858893

859894
const deadzoneCell = document.createElement("td");
860-
if (physicalInputSupportsTuning(inputMapping.physicalInput)) {
895+
if (physicalInputSupportsDeadzoneInvert(inputMapping.physicalInput)) {
861896
const deadzone = document.createElement("input");
862897
deadzone.type = "number";
863898
deadzone.min = "0";
@@ -871,7 +906,7 @@ export class AccountUserControlsPage {
871906
}
872907

873908
const invertCell = document.createElement("td");
874-
if (physicalInputSupportsTuning(inputMapping.physicalInput)) {
909+
if (physicalInputSupportsDeadzoneInvert(inputMapping.physicalInput)) {
875910
const invert = document.createElement("input");
876911
invert.type = "checkbox";
877912
invert.checked = Boolean(inputMapping.invert);
@@ -984,37 +1019,13 @@ export class AccountUserControlsPage {
9841019
this.setStatus("WARN: Choose a physical controller before creating a user control profile.");
9851020
return;
9861021
}
987-
const existing = this.profiles.find((profile) => profile.deviceType === device.deviceType && profile.controllerId === device.controllerId);
988-
if (existing) {
989-
this.editingProfile = { id: existing.id, values: existing };
990-
this.viewingDefaultFamily = "";
991-
this.renderProfiles();
992-
this.setStatus(`Editing existing ${existing.mappingProfile}.`);
993-
return;
994-
}
995-
const profile = this.profileFromDevice(device);
996-
this.editingProfile = { id: profile.id, values: profile };
997-
this.viewingDefaultFamily = "";
998-
this.renderProfiles();
999-
this.setStatus(`Review ${profile.mappingProfile} before saving.`);
1022+
this.createProfile(device);
10001023
}
10011024

10021025
editFamilyMappings(family) {
10031026
const normalizedFamily = family === "Mouse" ? "Mouse" : "Keyboard";
10041027
const device = this.familyDevice(normalizedFamily);
1005-
const existing = this.profiles.find((profile) => profile.deviceType === device.deviceType && profile.controllerId === device.controllerId);
1006-
if (existing) {
1007-
this.editingProfile = { id: existing.id, values: existing };
1008-
this.viewingDefaultFamily = "";
1009-
this.renderProfiles();
1010-
this.setStatus(`Editing existing ${existing.mappingProfile}.`);
1011-
return;
1012-
}
1013-
const profile = this.profileFromDevice(device);
1014-
this.editingProfile = { id: profile.id, values: profile };
1015-
this.viewingDefaultFamily = "";
1016-
this.renderProfiles();
1017-
this.setStatus(`Review ${profile.mappingProfile} before saving.`);
1028+
this.createProfile(device);
10181029
}
10191030

10201031
profileFromEditingRow() {
@@ -1113,6 +1124,19 @@ export class AccountUserControlsPage {
11131124
};
11141125
}
11151126

1127+
duplicateProfileName(profile) {
1128+
const profileName = normalizeText(profile.mappingProfile).toLowerCase();
1129+
if (!profileName) {
1130+
return null;
1131+
}
1132+
const family = this.profileListFamily(profile);
1133+
return this.profiles.find((candidate) =>
1134+
candidate.id !== profile.id
1135+
&& this.profileListFamily(candidate) === family
1136+
&& normalizeText(candidate.mappingProfile).toLowerCase() === profileName,
1137+
) || null;
1138+
}
1139+
11161140
renderInputValidation(validation) {
11171141
const invalidIndexes = new Set(validation.invalidIndexes || []);
11181142
this.root.querySelectorAll("[data-account-user-controls-input-validation]").forEach((status) => {
@@ -1207,11 +1231,15 @@ export class AccountUserControlsPage {
12071231
this.setStatus(`FAIL: ${validation.message}`);
12081232
return;
12091233
}
1234+
if (this.duplicateProfileName(profile)) {
1235+
this.setStatus(`FAIL: ${profile.mappingProfile} already exists for ${this.profileListFamily(profile)}.`);
1236+
return;
1237+
}
12101238
const nextProfiles = this.editingProfile?.id && this.profiles.some((candidate) => candidate.id === this.editingProfile.id)
12111239
? this.profiles.map((candidate) => (candidate.id === this.editingProfile.id ? profile : candidate))
12121240
: [profile, ...this.profiles];
12131241
if (!this.saveProfiles(nextProfiles)) {
1214-
this.setStatus("FAIL: Account user controls could not reach the shared DB adapter.");
1242+
this.setStatus("FAIL: User Controls could not reach the shared DB adapter.");
12151243
return;
12161244
}
12171245
this.editingProfile = null;

account/user-controls.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<head>
55
<meta charset="utf-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1">
7-
<title>Account User Controls - GameFoundryStudio</title>
7+
<title>User Controls - GameFoundryStudio</title>
88
<meta name="description" content="Map personal physical controls to generic normalized controls.">
99
<link rel="icon" href="/favicon.svg">
1010
<link rel="stylesheet" href="../assets/theme-v2/css/theme.css">
@@ -16,7 +16,7 @@
1616
<section class="page-title">
1717
<div class="container">
1818
<div class="kicker">Account</div>
19-
<h1>Account User Controls</h1>
19+
<h1>User Controls</h1>
2020
<p class="lede">Map your physical keyboard, mouse, game controller, joystick, or touch controls to generic normalized controls.</p>
2121
</div>
2222
</section>
@@ -26,9 +26,9 @@ <h1>Account User Controls</h1>
2626
<div class="card">
2727
<div class="card-body content-stack">
2828
<div>
29-
<div class="kicker">Account User Controls</div>
29+
<div class="kicker">User Controls</div>
3030
<h2>Physical Input Mapping</h2>
31-
<p>Account User Controls resolves physical input to normalized controls. Game behavior stays in Game Controls.</p>
31+
<p>User Controls resolves physical input to normalized controls. Game behavior stays in Game Controls.</p>
3232
</div>
3333
<div class="content-stack content-stack--compact">
3434
<h3>Selected Device</h3>
@@ -130,7 +130,7 @@ <h3>Selected Device</h3>
130130
<details class="vertical-accordion" open>
131131
<summary>Status</summary>
132132
<div class="accordion-body">
133-
<div class="status" role="status" data-account-user-controls-status>Account User Controls ready.</div>
133+
<div class="status" role="status" data-account-user-controls-status>User Controls ready.</div>
134134
</div>
135135
</details>
136136
</div>

docs_build/dev/reports/codex_changed_files.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ docs_build/dev/reports/codex_changed_files.txt
44
docs_build/dev/reports/codex_review.diff
55
docs_build/dev/reports/coverage_changed_js_guardrail.txt
66
docs_build/dev/reports/playwright_v8_coverage_report.txt
7-
src/dev-runtime/persistence/mock-db-store.js
8-
src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js
7+
src/engine/input/NormalizedInputRegistry.js
98
tests/playwright/tools/InputMappingV2Tool.spec.mjs
10-
docs_build/dev/reports/user-controls-selected-device-fallback-report.md
9+
docs_build/dev/reports/user-controls-defaults-and-profile-rules-report.md
-9.99 KB
Binary file not shown.

docs_build/dev/reports/coverage_changed_js_guardrail.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run.
88
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
11-
(94%) toolbox/controls/controls.js - executed lines 667/667; executed functions 68/72
11+
(89%) src/engine/input/NormalizedInputRegistry.js - executed lines 341/341; executed functions 24/27
1212

1313
Guardrail warnings:
1414
(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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Exercised tool entry points detected:
1919
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
22-
(94%) toolbox/controls/controls.js - executed lines 667/667; executed functions 68/72
22+
(89%) src/engine/input/NormalizedInputRegistry.js - executed lines 341/341; executed functions 24/27
2323

2424
Files with executed line/function counts where available:
2525
(11%) src/engine/input/InputMappingManifest.js - executed lines 78/78; executed functions 1/9
@@ -37,8 +37,8 @@ Files with executed line/function counts where available:
3737
(64%) assets/theme-v2/js/tool-display-mode.js - executed lines 209/209; executed functions 9/14
3838
(75%) toolbox/tool-registry-api-client.js - executed lines 152/152; executed functions 21/28
3939
(80%) src/engine/input/GamepadState.js - executed lines 100/100; executed functions 16/20
40-
(89%) src/engine/input/NormalizedInputRegistry.js - executed lines 321/321; executed functions 24/27
41-
(92%) account/user-controls-page.js - executed lines 1217/1217; executed functions 132/144
40+
(89%) src/engine/input/NormalizedInputRegistry.js - executed lines 341/341; executed functions 24/27
41+
(93%) account/user-controls-page.js - executed lines 1242/1242; executed functions 137/148
4242
(94%) toolbox/controls/controls.js - executed lines 667/667; executed functions 68/72
4343
(100%) account/user-controls.js - executed lines 2/2; executed functions 1/1
4444
(100%) toolbox/controls/controls-api-client.js - executed lines 12/12; executed functions 2/2
@@ -51,5 +51,5 @@ Changed JS files considered:
5151
(0%) src/dev-runtime/persistence/mock-db-store.js - changed JS file not collected as browser runtime coverage
5252
(0%) src/dev-runtime/persistence/tool-repositories/input-mapping-mock-repository.js - changed JS file not collected as browser runtime coverage
5353
(0%) tests/playwright/tools/InputMappingV2Tool.spec.mjs - changed JS file not collected as browser runtime coverage
54-
(92%) account/user-controls-page.js - changed JS file with browser V8 coverage
55-
(94%) toolbox/controls/controls.js - changed JS file with browser V8 coverage
54+
(89%) src/engine/input/NormalizedInputRegistry.js - changed JS file with browser V8 coverage
55+
(93%) account/user-controls-page.js - changed JS file with browser V8 coverage

0 commit comments

Comments
 (0)