Skip to content

Commit 8842780

Browse files
committed
Complete controls account follow-up and enforce engine input ownership - PR_26162_036-controls-account-followup
1 parent 162bf8c commit 8842780

77 files changed

Lines changed: 5769 additions & 5685 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

account/achievements.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ <h1>Achievements</h1>
2222
</section>
2323
<section class="section">
2424
<div class="container account-panel">
25-
<aside class="side-menu" aria-label="Account pages">
26-
<a href="index.html">Account Home</a>
27-
<a class="active" href="achievements.html">Achievements</a>
28-
<a href="preferences.html">Preferences</a>
29-
<a href="profile.html">Profile</a>
30-
<a href="security.html">Security</a>
31-
<a href="user-controls.html">User Controls</a>
32-
</aside>
25+
<div data-partial="account-side-nav" data-partial-replace></div>
3326
<div class="content-stack">
3427
<div class="card">
3528
<div class="card-body">

account/index.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ <h1>Account Home</h1>
2222
</section>
2323
<section class="section">
2424
<div class="container account-panel">
25-
<aside class="side-menu" aria-label="Account pages">
26-
<a class="active" href="index.html">Account Home</a>
27-
<a href="achievements.html">Achievements</a>
28-
<a href="preferences.html">Preferences</a>
29-
<a href="profile.html">Profile</a>
30-
<a href="security.html">Security</a>
31-
<a href="user-controls.html">User Controls</a>
32-
</aside>
25+
<div data-partial="account-side-nav" data-partial-replace></div>
3326
<div class="card">
3427
<div class="card-body">
3528
<div class="kicker">Status = Planning</div>

account/preferences.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ <h1>Preferences</h1>
2222
</section>
2323
<section class="section">
2424
<div class="container account-panel">
25-
<aside class="side-menu" aria-label="Account pages">
26-
<a href="index.html">Account Home</a>
27-
<a href="achievements.html">Achievements</a>
28-
<a class="active" href="preferences.html">Preferences</a>
29-
<a href="profile.html">Profile</a>
30-
<a href="security.html">Security</a>
31-
<a href="user-controls.html">User Controls</a>
32-
</aside>
25+
<div data-partial="account-side-nav" data-partial-replace></div>
3326
<div class="card">
3427
<div class="card-body">
3528
<div class="kicker">Status = Planning</div>

account/profile.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ <h1>Profile</h1>
2222
</section>
2323
<section class="section">
2424
<div class="container account-panel">
25-
<aside class="side-menu" aria-label="Account pages">
26-
<a href="index.html">Account Home</a>
27-
<a href="achievements.html">Achievements</a>
28-
<a href="preferences.html">Preferences</a>
29-
<a class="active" href="profile.html">Profile</a>
30-
<a href="security.html">Security</a>
31-
<a href="user-controls.html">User Controls</a>
32-
</aside>
25+
<div data-partial="account-side-nav" data-partial-replace></div>
3326
<div class="card">
3427
<div class="card-body">
3528
<div class="kicker">Status = Planning</div>

account/security.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ <h1>Security</h1>
2222
</section>
2323
<section class="section">
2424
<div class="container account-panel">
25-
<aside class="side-menu" aria-label="Account pages">
26-
<a href="index.html">Account Home</a>
27-
<a href="achievements.html">Achievements</a>
28-
<a href="preferences.html">Preferences</a>
29-
<a href="profile.html">Profile</a>
30-
<a class="active" href="security.html">Security</a>
31-
<a href="user-controls.html">User Controls</a>
32-
</aside>
25+
<div data-partial="account-side-nav" data-partial-replace></div>
3326
<div class="card">
3427
<div class="card-body">
3528
<div class="kicker">Status = Planning</div>

account/user-controls-page.js

Lines changed: 191 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import InputService from "../src/engine/input/InputService.js";
33
import { gamepadProfileInputNames } from "../src/engine/input/GamepadInputClassifier.js";
44
import {
55
normalizeProfileInputMappings,
6+
physicalInputSensitivityDescriptor,
67
normalizedInputIsAnalog,
78
physicalInputIsAnalog,
89
} from "../src/engine/input/NormalizedInputRegistry.js";
@@ -89,6 +90,31 @@ function labeledControl(labelText, control) {
8990
return label;
9091
}
9192

93+
function rangeValueLabel(value, unit = "") {
94+
return `${value}${unit}`;
95+
}
96+
97+
function createSliderControl({ ariaLabel, dataName, defaultValue, index, max, min, step, unit, value }) {
98+
const input = document.createElement("input");
99+
input.type = "range";
100+
input.min = String(min);
101+
input.max = String(max);
102+
input.step = String(step);
103+
input.value = String(value);
104+
input.dataset[dataName] = String(index);
105+
input.dataset.defaultValue = String(defaultValue);
106+
input.dataset.unit = unit;
107+
input.setAttribute("aria-label", ariaLabel);
108+
const valueLabel = document.createElement("span");
109+
valueLabel.className = "status";
110+
valueLabel.dataset.sliderValueFor = dataName;
111+
valueLabel.textContent = rangeValueLabel(input.value, unit);
112+
const wrapper = document.createElement("div");
113+
wrapper.className = "content-cluster";
114+
wrapper.append(input, valueLabel);
115+
return wrapper;
116+
}
117+
92118
export class AccountUserControlsPage {
93119
constructor(root) {
94120
this.root = root;
@@ -102,6 +128,7 @@ export class AccountUserControlsPage {
102128
deviceStatus: root.querySelector("[data-account-user-controls-device-status]"),
103129
list: root.querySelector("[data-account-user-controls-list]"),
104130
refresh: root.querySelector("[data-account-user-controls-refresh]"),
131+
saveAll: root.querySelector("[data-account-user-controls-save-all]"),
105132
status: root.querySelector("[data-account-user-controls-status]"),
106133
types: root.querySelector("[data-account-user-controls-types]"),
107134
};
@@ -115,12 +142,15 @@ export class AccountUserControlsPage {
115142
this.renderProfiles();
116143
this.elements.refresh?.addEventListener("click", () => {
117144
this.renderDeviceSelect();
118-
this.setStatus("Device list refreshed.");
145+
this.setStatus(this.deviceRefreshMessage());
119146
});
120147
this.elements.addProfile?.addEventListener("click", () => this.addProfileForSelectedDevice());
148+
this.elements.saveAll?.addEventListener("click", () => this.saveCurrentState());
121149
this.elements.deviceSelect?.addEventListener("change", () => this.renderDeviceStatus());
122150
this.elements.list?.addEventListener("click", (event) => this.handleListClick(event));
123151
this.elements.list?.addEventListener("change", (event) => this.handleListChange(event));
152+
this.elements.list?.addEventListener("input", (event) => this.handleListInput(event));
153+
this.elements.list?.addEventListener("dblclick", (event) => this.handleListDoubleClick(event));
124154
}
125155

126156
setStatus(message) {
@@ -165,6 +195,14 @@ export class AccountUserControlsPage {
165195
return [keyboardMouse, ...gamepads];
166196
}
167197

198+
deviceRefreshMessage() {
199+
const gamepadCount = this.availableGamepads().length;
200+
if (gamepadCount > 0) {
201+
return `PASS: Device list refreshed. Keyboard/Mouse and ${gamepadCount} game controller${gamepadCount === 1 ? "" : "s"} available.`;
202+
}
203+
return "PASS: Keyboard/Mouse available. To enumerate a game controller, connect it, press a button, allow browser gamepad access, then refresh devices.";
204+
}
205+
168206
selectedDevice() {
169207
const selectedValue = this.elements.deviceSelect?.value || "";
170208
return this.deviceOptions().find((device) => device.value === selectedValue) || null;
@@ -246,7 +284,7 @@ export class AccountUserControlsPage {
246284
renderDeviceStatus() {
247285
const device = this.selectedDevice();
248286
if (!device) {
249-
this.setDeviceStatus("Choose a physical controller before creating a user control profile.");
287+
this.setDeviceStatus(this.deviceRefreshMessage());
250288
return;
251289
}
252290
this.setDeviceStatus(`${device.label} selected. Create a user control profile to map physical inputs to normalized controls.`);
@@ -278,7 +316,7 @@ export class AccountUserControlsPage {
278316
if (!rows.length) {
279317
const row = document.createElement("tr");
280318
const cell = document.createElement("td");
281-
cell.colSpan = 6;
319+
cell.colSpan = 7;
282320
cell.textContent = "No account user control profiles saved yet.";
283321
row.append(cell);
284322
rows.push(row);
@@ -311,6 +349,18 @@ export class AccountUserControlsPage {
311349
return `${analogMappings.length} analog axes, ${inverted} inverted`;
312350
}
313351

352+
profileSensitivitySummary(profile) {
353+
const sensitiveInputs = profile.inputMappings.filter((mapping) => physicalInputSensitivityDescriptor(mapping.physicalInput));
354+
if (!sensitiveInputs.length) {
355+
return "No sensitivity controls";
356+
}
357+
const adjusted = sensitiveInputs.filter((mapping) => {
358+
const descriptor = physicalInputSensitivityDescriptor(mapping.physicalInput);
359+
return descriptor && Number(mapping.sensitivity ?? descriptor.defaultValue) !== descriptor.defaultValue;
360+
}).length;
361+
return `${sensitiveInputs.length} sensitivity controls, ${adjusted} adjusted`;
362+
}
363+
314364
renderProfileRow(profile) {
315365
const row = document.createElement("tr");
316366
row.dataset.accountUserControlsProfileRow = profile.id;
@@ -320,6 +370,7 @@ export class AccountUserControlsPage {
320370
tableCell(this.profileInputSummary(profile)),
321371
tableCell(this.profileAnalogSummary(profile)),
322372
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
373+
tableCell(this.profileSensitivitySummary(profile)),
323374
);
324375
const actions = document.createElement("td");
325376
const group = document.createElement("div");
@@ -382,6 +433,28 @@ export class AccountUserControlsPage {
382433
invert.dataset.accountUserControlsInvert = String(index);
383434
stack.append(labeledControl("Deadzone", deadzone), labeledControl("Invert", invert));
384435
}
436+
const sensitivity = physicalInputSensitivityDescriptor(inputMapping.physicalInput);
437+
if (sensitivity) {
438+
const value = Number.isFinite(Number(inputMapping.sensitivity))
439+
? Number(inputMapping.sensitivity)
440+
: sensitivity.defaultValue;
441+
const slider = createSliderControl({
442+
ariaLabel: sensitivity.label,
443+
dataName: "accountUserControlsSensitivity",
444+
defaultValue: sensitivity.defaultValue,
445+
index,
446+
max: sensitivity.max,
447+
min: sensitivity.min,
448+
step: sensitivity.step,
449+
unit: sensitivity.unit,
450+
value,
451+
});
452+
stack.append(labeledControl(sensitivity.label, slider));
453+
}
454+
const validation = document.createElement("p");
455+
validation.className = "status";
456+
validation.dataset.accountUserControlsInputValidation = String(index);
457+
stack.append(validation);
385458
wrapper.append(label, stack);
386459
return wrapper;
387460
}
@@ -404,13 +477,14 @@ export class AccountUserControlsPage {
404477
tableCell(this.profileInputSummary(profile)),
405478
tableCell(this.profileAnalogSummary(profile)),
406479
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
480+
tableCell(this.profileSensitivitySummary(profile)),
407481
actions,
408482
);
409483

410484
const detailsRow = document.createElement("tr");
411485
detailsRow.dataset.accountUserControlsEditingDetails = "true";
412486
const detailsCell = document.createElement("td");
413-
detailsCell.colSpan = 6;
487+
detailsCell.colSpan = 7;
414488
const grid = document.createElement("div");
415489
grid.className = "content-grid content-grid--three";
416490
grid.append(...profile.inputMappings.map((inputMapping, index) => this.inputControl(profile, inputMapping, index)));
@@ -447,6 +521,7 @@ export class AccountUserControlsPage {
447521
const positiveSelect = details?.querySelector(`[data-account-user-controls-input-positive="${index}"]`);
448522
const deadzoneInput = details?.querySelector(`[data-account-user-controls-deadzone="${index}"]`);
449523
const invertInput = details?.querySelector(`[data-account-user-controls-invert="${index}"]`);
524+
const sensitivityInput = details?.querySelector(`[data-account-user-controls-sensitivity="${index}"]`);
450525
const negativeNormalizedInput = normalizeText(negativeSelect?.value ?? mapping.negativeNormalizedInput);
451526
const positiveNormalizedInput = normalizeText(positiveSelect?.value ?? mapping.positiveNormalizedInput);
452527
return {
@@ -456,6 +531,7 @@ export class AccountUserControlsPage {
456531
normalizedInput: positiveNormalizedInput || normalizeText(normalizedSelect?.value ?? mapping.normalizedInput) || negativeNormalizedInput,
457532
physicalInput: mapping.physicalInput,
458533
positiveNormalizedInput,
534+
sensitivity: sensitivityInput ? Number(sensitivityInput.value) : mapping.sensitivity,
459535
};
460536
});
461537
return this.normalizeProfile({
@@ -465,17 +541,103 @@ export class AccountUserControlsPage {
465541
});
466542
}
467543

544+
mappingHasNormalizedControl(mapping) {
545+
return Boolean(
546+
normalizeText(mapping.normalizedInput)
547+
|| normalizeText(mapping.negativeNormalizedInput)
548+
|| normalizeText(mapping.positiveNormalizedInput),
549+
);
550+
}
551+
552+
validateProfile(profile) {
553+
if (!normalizeText(profile.controllerId) || !normalizeText(profile.mappingProfile)) {
554+
return {
555+
invalidIndexes: [],
556+
message: "Choose a physical controller before saving.",
557+
ok: false,
558+
};
559+
}
560+
const invalidIndexes = [];
561+
let assignedCount = 0;
562+
profile.inputMappings.forEach((mapping, index) => {
563+
if (this.mappingHasNormalizedControl(mapping)) {
564+
assignedCount += 1;
565+
}
566+
const deadzone = Number(mapping.deadzone);
567+
const descriptor = physicalInputSensitivityDescriptor(mapping.physicalInput);
568+
const sensitivity = Number(mapping.sensitivity ?? descriptor?.defaultValue);
569+
const invalidDeadzone = Number.isFinite(deadzone) && (deadzone < 0 || deadzone > 1);
570+
const invalidSensitivity = descriptor && (!Number.isFinite(sensitivity) || sensitivity < descriptor.min || sensitivity > descriptor.max);
571+
if (invalidDeadzone || invalidSensitivity) {
572+
invalidIndexes.push(index);
573+
}
574+
});
575+
if (!assignedCount) {
576+
return {
577+
invalidIndexes: profile.inputMappings.map((_, index) => index),
578+
message: "Assign at least one physical input to a normalized control before saving.",
579+
ok: false,
580+
};
581+
}
582+
if (invalidIndexes.length) {
583+
return {
584+
invalidIndexes,
585+
message: "Resolve highlighted input rows before saving.",
586+
ok: false,
587+
};
588+
}
589+
return {
590+
invalidIndexes: [],
591+
message: "",
592+
ok: true,
593+
};
594+
}
595+
596+
renderInputValidation(validation) {
597+
const invalidIndexes = new Set(validation.invalidIndexes || []);
598+
this.elements.list?.querySelectorAll("[data-account-user-controls-input-validation]").forEach((status) => {
599+
const index = Number(status.dataset.accountUserControlsInputValidation);
600+
status.textContent = invalidIndexes.has(index)
601+
? "Needs normalized control, valid deadzone, and valid sensitivity."
602+
: "";
603+
});
604+
}
605+
468606
saveEditingProfile() {
469607
const profile = this.profileFromEditingRow();
608+
const validation = this.validateProfile(profile);
609+
this.renderInputValidation(validation);
610+
if (!validation.ok) {
611+
this.setStatus(`FAIL: ${validation.message}`);
612+
return;
613+
}
470614
const nextProfiles = this.editingProfile?.id && this.profiles.some((candidate) => candidate.id === this.editingProfile.id)
471615
? this.profiles.map((candidate) => (candidate.id === this.editingProfile.id ? profile : candidate))
472616
: [profile, ...this.profiles];
473617
if (!this.saveProfiles(nextProfiles)) {
618+
this.setStatus("FAIL: Account user controls could not reach the shared DB adapter.");
474619
return;
475620
}
476621
this.editingProfile = null;
477622
this.renderProfiles();
478-
this.setStatus(`Saved ${profile.mappingProfile}.`);
623+
this.setStatus(`PASS: Saved ${profile.mappingProfile}.`);
624+
}
625+
626+
saveCurrentState() {
627+
if (this.editingProfile) {
628+
this.saveEditingProfile();
629+
return;
630+
}
631+
if (!this.profiles.length) {
632+
this.setStatus("FAIL: Create a user control profile before saving.");
633+
return;
634+
}
635+
if (!this.saveProfiles(this.profiles)) {
636+
this.setStatus("FAIL: Account user controls could not reach the shared DB adapter.");
637+
return;
638+
}
639+
this.renderProfiles();
640+
this.setStatus("PASS: Saved account user controls.");
479641
}
480642

481643
handleListClick(event) {
@@ -509,4 +671,28 @@ export class AccountUserControlsPage {
509671
handleListChange() {
510672
this.setStatus("Unsaved user control profile changes.");
511673
}
674+
675+
handleListInput(event) {
676+
const target = event.target instanceof HTMLInputElement ? event.target : null;
677+
if (target?.matches("[data-account-user-controls-sensitivity]")) {
678+
const valueLabel = target.parentElement?.querySelector("[data-slider-value-for='accountUserControlsSensitivity']");
679+
if (valueLabel) {
680+
valueLabel.textContent = rangeValueLabel(target.value, target.dataset.unit || "");
681+
}
682+
}
683+
this.setStatus("Unsaved user control profile changes.");
684+
}
685+
686+
handleListDoubleClick(event) {
687+
const target = event.target instanceof HTMLInputElement ? event.target : null;
688+
if (!target?.matches("[data-account-user-controls-sensitivity]")) {
689+
return;
690+
}
691+
target.value = target.dataset.defaultValue || "100";
692+
const valueLabel = target.parentElement?.querySelector("[data-slider-value-for='accountUserControlsSensitivity']");
693+
if (valueLabel) {
694+
valueLabel.textContent = rangeValueLabel(target.value, target.dataset.unit || "");
695+
}
696+
target.dispatchEvent(new Event("input", { bubbles: true }));
697+
}
512698
}

0 commit comments

Comments
 (0)