Skip to content

Commit 3594bc4

Browse files
author
DavidQ
committed
Handle overlay input edge cases.
PR Details: - Stabilizes input under rapid and conflicting conditions
1 parent 44e06a9 commit 3594bc4

10 files changed

Lines changed: 184 additions & 26 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Implement overlay focus and input priority rules:
6-
- Gameplay input remains primary
7-
- Overlay controls scoped to explicit actions
8-
- No focus stealing
9-
- No Tab usage
5+
Handle overlay input edge cases:
6+
- Prevent input flooding issues
7+
- Ensure no stuck key states
8+
- Maintain gameplay priority
9+
- Preserve behavior
1010

1111
Package ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Add overlay focus and input priority rules.
1+
Handle overlay input edge cases.
22

33
PR Details:
4-
- Defines gameplay-first input priority
5-
- Prevents overlay focus conflicts
4+
- Stabilizes input under rapid and conflicting conditions

docs/dev/PROJECT_INSTRUCTIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,5 @@ Commit Comment:
137137
- Do the right thing and complete the task fully and correctly
138138
- Don't ask if I want the next bundled PR, assume I want it.
139139
- Update Roadmap stutus every PR.
140+
- Every PRs must improve roadmap and be testable.
140141

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# BUILD_PR_LEVEL_19_5_OVERLAY_INPUT_EDGE_CASES Report
2+
3+
## Purpose
4+
Harden runtime overlay input handling against flooding, buffering jitter, and stuck-key edge cases while preserving gameplay input priority.
5+
6+
## Scope Applied
7+
- Added runtime control edge-case guards in `overlayGameplayRuntime`:
8+
- short action cooldown to damp rapid-fire jitter
9+
- hold-duration suppression (`suppress-until-release`) for stuck explicit input states
10+
- latch behavior hardened so held keys do not re-trigger after cooldown expiry
11+
- Preserved explicit-action control model and no-Tab mapping:
12+
- runtime toggle: `Ctrl+G`
13+
- runtime cycle: `Ctrl+Shift+G`
14+
- Added focused stress/edge-case test coverage.
15+
16+
## Files Changed
17+
- `samples/phase-17/shared/overlayGameplayRuntime.js`
18+
- `samples/phase-17/shared/overlayCycleInput.js`
19+
- `tests/runtime/Phase17OverlayInputEdgeCases.test.mjs`
20+
21+
## Validation
22+
- `tests/runtime/Phase17OverlayInputEdgeCases.test.mjs` PASS
23+
- `tests/runtime/Phase17OverlayGameplayRuntimeIntegration.test.mjs` PASS
24+
- `tests/runtime/Phase17RealGameplaySample.test.mjs` PASS
25+
- `tests/runtime/Phase17Sample1712GameplayMetricsTelemetry.test.mjs` PASS
26+
- `tests/runtime/Phase17Sample1713FinalReferenceGame.test.mjs` PASS
27+
- `tests/runtime/Phase17TabDebugOverlayCycle1707Plus.test.mjs` PASS
28+
- `tests/runtime/Phase17DebugOverlayBottomRightPosition.test.mjs` PASS
29+
- `tests/runtime/Phase18RuntimeLayerScaffold.test.mjs` PASS
30+
- `tests/runtime/Phase18IntegrationFlowPass.test.mjs` PASS
31+
- `tests/runtime/Phase18CoreServicesSkeleton.test.mjs` PASS
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[ ] Gameplay input unaffected
2-
[ ] Overlay controls work
3-
[ ] No focus conflicts
4-
[ ] No Tab usage
1+
[ ] No stuck inputs
2+
[ ] Rapid cycling stable
3+
[ ] Gameplay unaffected
4+
[ ] No missed inputs

docs/pr/BUILD_PR.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
# BUILD_PR_LEVEL_19_4_OVERLAY_FOCUS_AND_INPUT_PRIORITY
1+
# BUILD_PR_LEVEL_19_5_OVERLAY_INPUT_EDGE_CASES
22

33
## Purpose
4-
Define and enforce focus + input priority rules so gameplay and overlays coexist without conflicts.
4+
Handle edge cases in overlay input interactions to ensure robust behavior under unusual or rapid input scenarios.
55

66
## Scope
7-
- Establish input priority: gameplay > overlay (except explicit overlay controls)
8-
- Define focus model (no hard focus steal by overlays)
9-
- Ensure overlay controls are scoped and non-invasive
7+
- Rapid key presses
8+
- Simultaneous input (gameplay + overlay)
9+
- Input buffering issues
10+
- Lost or stuck key states
1011

1112
## Test Steps
12-
1. Run gameplay sample
13-
2. Trigger overlay controls
14-
3. Verify gameplay input remains primary
15-
4. Confirm overlay controls only act when invoked
13+
1. Spam overlay cycle key
14+
2. Hold gameplay + overlay inputs simultaneously
15+
3. Trigger rapid show/hide toggles
16+
4. Validate no stuck states
1617

1718
## Expected
18-
- No input conflicts
19-
- Predictable priority behavior
19+
- No input lockups
20+
- No missed cycles
21+
- Stable behavior under stress

samples/phase-17/1708/RealGameplayMiniGameScene.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ export default class RealGameplayMiniGameScene extends Scene {
487487
this.lastCollisionCount = this.debugCollisionRows.length;
488488
this.updateFeedback(dt);
489489
this.refreshMissionFeedState();
490-
stepOverlayGameplayRuntimeControls(this.overlayGameplayRuntime, input);
490+
stepOverlayGameplayRuntimeControls(this.overlayGameplayRuntime, input, { dtSeconds: dt });
491491
stepOverlayGameplayRuntime(this.overlayGameplayRuntime, {
492492
scene: this,
493493
engine,

samples/phase-17/1710/RealGameplayMiniGameScene.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ export default class RealGameplayMiniGameScene extends Scene {
487487
this.lastCollisionCount = this.debugCollisionRows.length;
488488
this.updateFeedback(dt);
489489
this.refreshMissionFeedState();
490-
stepOverlayGameplayRuntimeControls(this.overlayGameplayRuntime, input);
490+
stepOverlayGameplayRuntimeControls(this.overlayGameplayRuntime, input, { dtSeconds: dt });
491491
stepOverlayGameplayRuntime(this.overlayGameplayRuntime, {
492492
scene: this,
493493
engine,

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export function createOverlayGameplayRuntime({ runtimeExtensions = [] } = {}) {
7878
interactionCycleLatch: false,
7979
interactionToggleLatch: false,
8080
interactionCycleKey: LEVEL17_OVERLAY_CYCLE_KEY,
81+
interactionActionCooldownSeconds: 0.03,
82+
interactionCooldownRemainingSeconds: 0,
83+
interactionMaxHoldSeconds: 1.25,
84+
interactionExplicitHoldSeconds: 0,
85+
interactionSuppressUntilRelease: false,
8186
};
8287
}
8388

@@ -112,14 +117,24 @@ export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
112117
count: extensions.length,
113118
activeOverlayId: active?.overlayId || '',
114119
cycleKey: String(runtime?.interactionCycleKey || LEVEL17_OVERLAY_CYCLE_KEY),
120+
suppressUntilRelease: runtime?.interactionSuppressUntilRelease === true,
121+
cooldownRemainingSeconds: Math.max(0, Number(runtime?.interactionCooldownRemainingSeconds) || 0),
115122
};
116123
}
117124

118-
export function stepOverlayGameplayRuntimeControls(runtime, input) {
125+
export function stepOverlayGameplayRuntimeControls(runtime, input, options = {}) {
119126
if (!runtime) {
120127
return false;
121128
}
122129

130+
const dtSeconds = Math.max(0, Math.min(0.25, Number(options?.dtSeconds) || 0));
131+
if (runtime.interactionCooldownRemainingSeconds > 0 && dtSeconds > 0) {
132+
runtime.interactionCooldownRemainingSeconds = Math.max(
133+
0,
134+
runtime.interactionCooldownRemainingSeconds - dtSeconds
135+
);
136+
}
137+
123138
const cycleKey = String(runtime.interactionCycleKey || LEVEL17_OVERLAY_CYCLE_KEY);
124139
const cyclePressed = input?.isDown(cycleKey) === true;
125140
const toggleModifierActive = isOverlayRuntimeToggleModifierActive(input);
@@ -131,13 +146,35 @@ export function stepOverlayGameplayRuntimeControls(runtime, input) {
131146
if (!explicitActionPressed) {
132147
runtime.interactionToggleLatch = false;
133148
runtime.interactionCycleLatch = false;
149+
runtime.interactionExplicitHoldSeconds = 0;
150+
runtime.interactionSuppressUntilRelease = false;
151+
return false;
152+
}
153+
154+
const holdDt = dtSeconds > 0 ? dtSeconds : 0.016;
155+
runtime.interactionExplicitHoldSeconds += holdDt;
156+
if (runtime.interactionExplicitHoldSeconds >= runtime.interactionMaxHoldSeconds) {
157+
runtime.interactionSuppressUntilRelease = true;
158+
}
159+
if (runtime.interactionSuppressUntilRelease === true) {
160+
return false;
161+
}
162+
163+
if (runtime.interactionCooldownRemainingSeconds > 0) {
164+
if (togglePressed) {
165+
runtime.interactionToggleLatch = true;
166+
}
167+
if (runtimeCyclePressed) {
168+
runtime.interactionCycleLatch = true;
169+
}
134170
return false;
135171
}
136172

137173
if (togglePressed && runtime.interactionToggleLatch === false) {
138174
runtime.interactionToggleLatch = true;
139175
runtime.interactionVisible = runtime.interactionVisible === false;
140176
runtime.interactionCycleLatch = true;
177+
runtime.interactionCooldownRemainingSeconds = runtime.interactionActionCooldownSeconds;
141178
return true;
142179
}
143180

@@ -160,6 +197,7 @@ export function stepOverlayGameplayRuntimeControls(runtime, input) {
160197
normalizeInteractionIndex(runtime);
161198
const count = runtime.runtimeExtensions.length;
162199
runtime.interactionIndex = (runtime.interactionIndex + 1 + count) % count;
200+
runtime.interactionCooldownRemainingSeconds = runtime.interactionActionCooldownSeconds;
163201
return true;
164202
}
165203

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
Phase17OverlayInputEdgeCases.test.mjs
6+
*/
7+
import assert from 'node:assert/strict';
8+
import {
9+
getOverlayRuntimeCycleInputCodes,
10+
getOverlayRuntimeToggleInputCodes,
11+
} from '../../samples/phase-17/shared/overlayCycleInput.js';
12+
import {
13+
createOverlayGameplayRuntime,
14+
getOverlayGameplayRuntimeInteractionSnapshot,
15+
stepOverlayGameplayRuntimeControls,
16+
} from '../../samples/phase-17/shared/overlayGameplayRuntime.js';
17+
18+
function makeInput(keys = []) {
19+
const down = new Set(keys);
20+
return {
21+
isDown(code) {
22+
return down.has(code);
23+
},
24+
};
25+
}
26+
27+
function stepControls(runtime, keys = [], dtSeconds = 0.02) {
28+
return stepOverlayGameplayRuntimeControls(runtime, makeInput(keys), { dtSeconds });
29+
}
30+
31+
function assertInputFloodAndStuckStateHandling() {
32+
const runtime = createOverlayGameplayRuntime({
33+
runtimeExtensions: [
34+
{ onStep() {} },
35+
{ onStep() {} },
36+
{ onStep() {} },
37+
],
38+
});
39+
40+
const runtimeCycleKeys = getOverlayRuntimeCycleInputCodes();
41+
const runtimeToggleKeys = getOverlayRuntimeToggleInputCodes();
42+
43+
const firstCycle = stepControls(runtime, runtimeCycleKeys, 0.02);
44+
assert.equal(firstCycle, true, 'First explicit runtime cycle action should trigger.');
45+
assert.equal(runtime.interactionIndex, 1, 'First runtime cycle action should advance index once.');
46+
47+
stepControls(runtime, [], 0.005);
48+
const immediateCycleAfterAction = stepControls(runtime, runtimeCycleKeys, 0.005);
49+
assert.equal(immediateCycleAfterAction, false, 'Cooldown should prevent immediate re-trigger after action jitter.');
50+
51+
for (let i = 0; i < 40; i += 1) {
52+
const cycled = stepControls(runtime, runtimeCycleKeys, 0.01);
53+
assert.equal(cycled, false, 'Held runtime cycle input should not flood repeated actions.');
54+
}
55+
assert.equal(runtime.interactionIndex, 1, 'Held runtime cycle input should keep index stable.');
56+
57+
stepControls(runtime, [], 0.02);
58+
const cycleAfterRelease = stepControls(runtime, runtimeCycleKeys, 0.02);
59+
assert.equal(cycleAfterRelease, true, 'Cycle should resume after release once cooldown has elapsed.');
60+
assert.equal(runtime.interactionIndex, 2, 'Cycle after release should advance index once.');
61+
62+
stepControls(runtime, [], 0.05);
63+
const firstToggle = stepControls(runtime, runtimeToggleKeys, 0.02);
64+
assert.equal(firstToggle, true, 'First explicit runtime toggle should trigger.');
65+
assert.equal(runtime.interactionVisible, false, 'First runtime toggle should hide overlay runtime.');
66+
67+
let repeatedToggleCount = 0;
68+
for (let i = 0; i < 120; i += 1) {
69+
if (stepControls(runtime, runtimeToggleKeys, 0.02)) {
70+
repeatedToggleCount += 1;
71+
}
72+
}
73+
assert.equal(repeatedToggleCount, 0, 'Held toggle input should not repeatedly fire and flood actions.');
74+
assert.equal(runtime.interactionVisible, false, 'Held toggle input should keep visibility stable.');
75+
76+
const snapshotBeforeRelease = getOverlayGameplayRuntimeInteractionSnapshot(runtime);
77+
assert.equal(snapshotBeforeRelease.suppressUntilRelease, true, 'Long held explicit input should enter suppress-until-release mode.');
78+
79+
stepControls(runtime, [], 0.02);
80+
const toggleAfterRelease = stepControls(runtime, runtimeToggleKeys, 0.02);
81+
assert.equal(toggleAfterRelease, true, 'Released stuck input should recover and allow next explicit action.');
82+
assert.equal(runtime.interactionVisible, true, 'Recovered toggle action should restore visibility.');
83+
}
84+
85+
export function run() {
86+
assertInputFloodAndStuckStateHandling();
87+
}

0 commit comments

Comments
 (0)