Stop a key getting stuck when Shift changes it while held (#227)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Failing after 8m37s

Input remembered keys under KeyboardEvent.key. Pressing a, adding
Shift and letting go sent "A" on release, which matched nothing, so
"a" stayed down until the next release without Shift.

- New rule keyName in src/core/keys.ts lowercases a single character.
  So a and A are the same key, also under Caps Lock.
- Input remembers per physical key (e.code) under which name it went
  down and releases exactly that name. So 1 released as ! is freed
  too, which lowercasing alone does not cover.
- On key down, up and held, Key is down and Axis input compare with
  the same rule. A node that already stores A reacts to a.
- Recording a key in the inspector stores Shift+A as a.
- Limit: holding Shift before pressing 1 still sends !, and a node on
  1 does not react. Nothing gets stuck by it.

Tests:
- 3 new unit tests in tests/core/behavior-events.test.ts: the flow
  from the issue, 1 released as !, and a node on A hearing a.
- Without lowercasing exactly the issue test and the A test fail.
- Without the code memory exactly the 1/! test fails.
- ./check.sh: 159 files, 2340 tests, CHECK EXIT 0 (before 2337).
- No e2e change, the check sum stays at 1377.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 18:06:16 +02:00
1 parent c1a6d63edb
commit ffb5bbe039
5 files changed
+79 -11

No files matched your search

+3 -2
View File
@@ -22,6 +22,7 @@ import {
type Value,
} from '../../core/graph';
import { stepKeyRepeat } from '../../core/graph/keyRepeat';
import { keyName } from '../../core/keys';
import { graphLookup } from '../../core/model/graphLookup';
import { type PropertyBag, textOf, type Vec2 } from '../../core/model/Properties';
import { findAssetOf, findScene, type Project } from '../../core/model/Project';
@@ -436,7 +437,7 @@ export class BehaviorSystem implements System {
update(world: World, dt: number): void {
this.syncInstances(world);
const keyOf = (node: { params: PropertyBag }) => textOf(node.params.key ?? '');
const keyOf = (node: { params: PropertyBag }) => keyName(textOf(node.params.key ?? ''));
const frozen = world.frozen;
for (const instance of this.instances) {
if (!world.entities.includes(instance.entity)) continue; // destroyed this frame, reaped below
@@ -490,7 +491,7 @@ export class BehaviorSystem implements System {
* in `nodeState`, so two nodes on the same key repeat on their own schedules.
*/
private fireKeyDown(i: GraphInterpreter, ctx: NodeContext, node: GraphNode, dt: number): void {
const key = textOf(node.params.key ?? '');
const key = keyName(textOf(node.params.key ?? ''));
const stored = i.nodeState.get(node.id);
const { state, fires } = stepKeyRepeat(
stored ? { held: stored.held ?? 0, repeats: stored.repeats ?? 0 } : null,