@edv4h/usketch-plugin-character 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +72 -11
  2. package/dist/character-input.d.ts +15 -4
  3. package/dist/character-input.d.ts.map +1 -1
  4. package/dist/character-input.js +174 -26
  5. package/dist/character-input.js.map +1 -1
  6. package/dist/character-key-choices.d.ts +14 -0
  7. package/dist/character-key-choices.d.ts.map +1 -0
  8. package/dist/character-key-choices.js +23 -0
  9. package/dist/character-key-choices.js.map +1 -0
  10. package/dist/character-layer.d.ts +1 -1
  11. package/dist/character-layer.d.ts.map +1 -1
  12. package/dist/character-layer.js +45 -14
  13. package/dist/character-layer.js.map +1 -1
  14. package/dist/character-physics.d.ts +156 -2
  15. package/dist/character-physics.d.ts.map +1 -1
  16. package/dist/character-physics.js +270 -22
  17. package/dist/character-physics.js.map +1 -1
  18. package/dist/character-runtime.d.ts +8 -1
  19. package/dist/character-runtime.d.ts.map +1 -1
  20. package/dist/character-runtime.js +74 -8
  21. package/dist/character-runtime.js.map +1 -1
  22. package/dist/character-service.d.ts +23 -3
  23. package/dist/character-service.d.ts.map +1 -1
  24. package/dist/character-service.js +7 -0
  25. package/dist/character-service.js.map +1 -1
  26. package/dist/character-store.d.ts +34 -1
  27. package/dist/character-store.d.ts.map +1 -1
  28. package/dist/character-store.js +86 -4
  29. package/dist/character-store.js.map +1 -1
  30. package/dist/character-sync.d.ts +11 -1
  31. package/dist/character-sync.d.ts.map +1 -1
  32. package/dist/character-sync.js +23 -1
  33. package/dist/character-sync.js.map +1 -1
  34. package/dist/character-types.d.ts +31 -2
  35. package/dist/character-types.d.ts.map +1 -1
  36. package/dist/character-types.js +2 -1
  37. package/dist/character-types.js.map +1 -1
  38. package/dist/character-virtual-input.d.ts +20 -0
  39. package/dist/character-virtual-input.d.ts.map +1 -0
  40. package/dist/character-virtual-input.js +34 -0
  41. package/dist/character-virtual-input.js.map +1 -0
  42. package/dist/index.d.ts +4 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/plugin.d.ts +13 -1
  47. package/dist/plugin.d.ts.map +1 -1
  48. package/dist/plugin.js +2 -0
  49. package/dist/plugin.js.map +1 -1
  50. package/dist/register-character-hud.d.ts.map +1 -1
  51. package/dist/register-character-hud.js +88 -1
  52. package/dist/register-character-hud.js.map +1 -1
  53. package/package.json +2 -2
package/README.md CHANGED
@@ -23,7 +23,9 @@ createCharacterPlugin({
23
23
  keys: { up: ["KeyW", "ArrowUp"] }, // KeyboardEvent.code values
24
24
  cameraMode: "rotate", // "fixed" | "rotate" | "free"
25
25
  controlScheme: "vehicle", // "directional" | "vehicle"
26
- drift: true, // vehicle: Space + steer drifts (or tuning overrides)
26
+ drift: true, // vehicle: Shift + steer drifts (or tuning overrides)
27
+ jump: true, // Space jumps (or tuning overrides)
28
+ renderShadow: ({ altitude }) => <div className="shadow" style={{ scale: 1 / (1 + altitude / 60) }} />,
27
29
  screenAnchor: { x: 0.5, y: 0.75 },
28
30
  });
29
31
  ```
@@ -34,7 +36,9 @@ createCharacterPlugin({
34
36
  as CSS `rotate()` and the viewport `rotation`.
35
37
  - **Render props**: `heading` (world), `screenHeading` (= `heading + viewport.rotation`,
36
38
  what to rotate a top-down sprite by), `moving`, `speed`, `steering`, `slip`,
37
- `drifting`, `zoom`, `appearance`, `name`, `isSelf`, `id`, `elevation`. Each character is placed centered on its screen position
39
+ `drifting`, `zoom`, `appearance`, `name`, `isSelf`, `id`, `elevation`, `z`, `altitude`,
40
+ `airborne`, `verticalSpeed`, `hovering`, `hoverGauge` (0 … 1, `null` with hovering
41
+ off). Each character is placed centered on its screen position
38
42
  in one screen-space layer, so self and remote characters look the same under zoom
39
43
  and camera rotation.
40
44
  - **Control schemes**
@@ -44,7 +48,7 @@ createCharacterPlugin({
44
48
  - `vehicle` — up/down throttle and reverse, left/right steer. Steering scales
45
49
  with speed (a stopped car can't turn) and inverts in reverse. The held steering
46
50
  (-1 … 1) is in the pose and render props, e.g. to turn the front wheels.
47
- - **Drift** (`vehicle`, opt-in via `drift`): hold the drift key (Space) and steer at
51
+ - **Drift** (`vehicle`, opt-in via `drift`): hold the drift key (Shift) and steer at
48
52
  speed to start an arcade drift toward the steering. It turns tighter than normal
49
53
  steering and slides the body by `slip` degrees (draw the car at `heading + slip`;
50
54
  it keeps traveling along `heading`). Steering into the drift tightens it,
@@ -52,7 +56,55 @@ createCharacterPlugin({
52
56
  `stopSpeed` or reversing ends it, and the slip eases back. Tuning (`DriftParams`):
53
57
  `startSpeed` / `stopSpeed` (fractions of `moveSpeed`), `turnRate` / `steerRate`
54
58
  (multiples of `turnRate`), `slipAngle` / `steerSlipAngle` (degrees), `recoveryRate`
55
- (1/s). The drift key is only captured while drift is on.
59
+ (1/s). With `hop: true`, pressing the drift key on the ground hops `hopHeight`
60
+ (default 10) first, and the drift starts on landing if the key is still held and
61
+ steering (a car can't steer in the air). The drift key is only captured while drift
62
+ is on.
63
+ - **Jump** (opt-in via `jump`): press the jump key (Space) to jump. The arc is set by
64
+ its apex `height` (world units) and `timeToApex`; it falls `fallGravity` × faster than
65
+ it rises, releasing the key while rising cuts it short (`releaseCut`), a press within
66
+ `bufferTime` before landing jumps on touchdown, and a jump within `coyoteTime` of
67
+ walking off a ledge still counts. The pose carries the feet height `z` (absolute, like
68
+ the ground height), `vz` and `airborne`. Where the jump key is also the active drift
69
+ key (both can be rebound) the drift wins. `api.jump()` (and the HUD
70
+ action) jumps from code, as a full jump.
71
+ - **Multi-jump** (`jump.airJumps`, default 0): a fresh press in the air uses one of
72
+ `airJumps` extra jumps while any remain (1 = a double jump); each reaches
73
+ `airJumpRatio` (default 0.8) × `height` (take-off speed × √ratio). They come back on
74
+ landing.
75
+ - **Hover** (`jump.hover`, default off; `true` or a partial `HoverParams`): once the air
76
+ jumps are used up (or there are none), holding the jump key while falling fires a
77
+ thrust and drains a gauge that lasts `duration` (1.5 s); an empty gauge falls normally.
78
+ `thrust` is a multiple of gravity (default 0 = a glide): below 1 the character sinks,
79
+ at 1 it keeps its vertical speed, above 1 it climbs for as long as the key is held.
80
+ Sinking is capped at `fallSpeed` (default 15 world units / s, 0 = never sink), climbing
81
+ at `riseSpeed` (60). The gauge refills
82
+ on the ground over `rechargeTime` (0.8 s), not at once on landing. The pose carries
83
+ `hovering` and `hoverGauge` (stays 1 with hovering off); both are broadcast.
84
+ - **Keys**: the jump (Space) and the drift (Shift) have their own keys. The HUD offers
85
+ a few keys for each (`KEY_CHOICES`: Space, Shift, Z, X, C, J, K, L); `setKeys` takes
86
+ any `KeyboardEvent.code`s and `getKeys` returns the bindings.
87
+ - **Shadow**: pass `renderShadow` to draw a shadow; it is placed on the ground under
88
+ each character while the body stands at its feet height `z`, so under a 3D view
89
+ (Mode 7) a jump lifts the body off its shadow. `altitude` (`z - elevation`) lets it
90
+ shrink as the character rises.
91
+ - **Keyboard focus**: movement keys are captured on `window` while the character is
92
+ on, except where the page needs them: nothing is taken while a text field has focus
93
+ or focus is inside a dialog / menu (`dialog`, `role="dialog"`, `alertdialog`, `menu`;
94
+ a dialog that contains the board itself doesn't count), arrow keys are left to
95
+ widgets that use them (range / radio inputs, `role="slider"`, `radio`, `tab`,
96
+ `option`, `combobox`, `menubar` items, `grid`, `treeitem` …) while WASD keeps
97
+ driving, and Space is left to buttons, links and other controls it activates. Focus
98
+ is read through `composedPath()`, so web-component dialogs count too. Moving focus
99
+ to such a target releases the held keys it uses.
100
+ - **Touch / gamepad (virtual input)**: a host control feeds analog input with
101
+ `api.setVirtualInput({ throttle, steer, drift })` (`throttle` / `steer` in -1 … 1;
102
+ `directional` reads them as screen up / right). Send the current state on every
103
+ change and `null` on release. It adds to the held keys (capped at ±1), so keyboard
104
+ and stick can drive together; a partial `throttle` aims for that fraction of the top
105
+ speed and a partial `steer` turns by that fraction. Touching it resumes a paused
106
+ follow camera like a movement key. It is ignored while the character is off, and
107
+ turning the character off releases it.
56
108
  - **Camera modes**
57
109
  - `fixed` — follow, world stays upright.
58
110
  - `rotate` — follow and turn the world by `-heading` (character always faces up).
@@ -69,8 +121,11 @@ createCharacterPlugin({
69
121
  `@edv4h/usketch-shared`, e.g. the map plugin's raised terrain), moving onto
70
122
  ground more than `motion.maxStep` (default 24 world units, HUD "登れる段差") higher
71
123
  than where the character stands is blocked; it slides along the cliff instead.
72
- Stepping down is always allowed. The render props carry `elevation`, and under a
73
- 3D view (Mode 7) the character is lifted onto the ground it stands on.
124
+ The step is measured from the feet, so mid-jump taller cliffs are reachable; a ledge
125
+ within the step above the feet is landed on. Stepping down within the step snaps
126
+ down; a bigger drop falls (gravity applies even with jumping off). The render props
127
+ carry `elevation` (the ground) and `z` (the feet), and under a 3D view (Mode 7) the
128
+ character is lifted to its feet height.
74
129
  - **Input**: held keys are tracked from window `keydown`/`keyup` (capture phase,
75
130
  `event.code`), swallowed while the character is on so single-letter tool
76
131
  shortcuts don't fire. Typing in text fields, and key chords with Ctrl/Cmd/Alt,
@@ -78,9 +133,9 @@ createCharacterPlugin({
78
133
 
79
134
  ## HUD
80
135
 
81
- A "キャラクター" settings group (on/off, camera, scheme, drift, speed, turn rate,
82
- climbable step, screen position, camera lag) plus actions to toggle the character and to place it at the
83
- screen position.
136
+ A "キャラクター" settings group (on/off, camera, scheme, drift, drift key, drift hop, jump, jump key, jump height, air
137
+ jumps, hover, hover time, hover thrust, speed, turn rate, climbable step, screen position, camera lag) plus actions to toggle the
138
+ character, to place it at the screen position, and to jump.
84
139
 
85
140
  ## Service
86
141
 
@@ -91,8 +146,14 @@ const api = getCharacterApi(app.services);
91
146
  api?.enable();
92
147
  api?.setCameraMode("rotate");
93
148
  api?.setRenderer(myRenderer);
94
- api?.setDrift({ slipAngle: 35 }); // true / false / tuning overrides
95
- api?.getPose(); // { x, y, heading, speed, moving, steering, slip, drift }
149
+ api?.setDrift({ slipAngle: 35, hop: true }); // true / false / tuning overrides
150
+ api?.setJump({ height: 60 }); // true / false / tuning overrides
151
+ api?.setJump({ airJumps: 1, hover: { duration: 2 } }); // double jump + hover (hover: true / false / partial)
152
+ api?.jump();
153
+ api?.setKeys({ ...api.getKeys(), drift: ["KeyX"] }); // KeyboardEvent.code values
154
+ api?.setShadowRenderer(myShadow);
155
+ api?.getPose(); // { x, y, heading, speed, moving, steering, slip, drift, z, vz, airborne, hovering, hoverGauge }
156
+ api?.setVirtualInput({ throttle: 0.8, steer: -0.3 }); // touch stick; null on release
96
157
  ```
97
158
 
98
159
  ## Limitations
@@ -1,8 +1,14 @@
1
1
  import type { MoveInput } from "./character-physics.js";
2
2
  import type { CharacterKeys } from "./character-types.js";
3
3
  export interface KeyInput {
4
- /** The currently held directions (and the drift key). */
4
+ /**
5
+ * The currently held directions (and the drift / jump keys). `jumpPressed` /
6
+ * `driftPressed` report a press since the previous `read()`, so a tap between two
7
+ * frames isn't lost.
8
+ */
5
9
  read(): MoveInput;
10
+ /** The current key bindings. */
11
+ getKeys(): CharacterKeys;
6
12
  /** Whether any bound key is held. */
7
13
  anyHeld(): boolean;
8
14
  /** Replace the key bindings (held state is cleared). */
@@ -11,10 +17,15 @@ export interface KeyInput {
11
17
  }
12
18
  /**
13
19
  * Listen for the bound keys on `window`. `isActive()` gates everything: while it is
14
- * false no key is captured (and none is swallowed). Held keys are cleared on window
15
- * blur / tab hide so a key released elsewhere can't get stuck "down".
20
+ * false no key is captured (and none is swallowed). Keys are left alone while focus is
21
+ * in a text field or inside a dialog / menu, arrow keys while a widget that uses them
22
+ * (slider, radio, tab …) has focus, and the drift / jump keys on controls Space activates.
23
+ * Held keys are cleared on window blur and tab hide, and released when focus moves to
24
+ * a target that uses them, so a key released elsewhere can't get stuck "down".
16
25
  */
17
26
  export declare function createKeyInput(initialKeys: CharacterKeys, isActive: () => boolean,
18
27
  /** Whether the drift key does anything now (else it is left to the host, e.g. Space-pan). */
19
- isDriftActive?: () => boolean): KeyInput;
28
+ isDriftActive?: () => boolean,
29
+ /** Whether the jump key does anything now (else it is left to the host). */
30
+ isJumpActive?: () => boolean): KeyInput;
20
31
  //# sourceMappingURL=character-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"character-input.d.ts","sourceRoot":"","sources":["../src/character-input.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACxB,yDAAyD;IACzD,IAAI,IAAI,SAAS,CAAC;IAClB,qCAAqC;IACrC,OAAO,IAAI,OAAO,CAAC;IACnB,wDAAwD;IACxD,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;CAChB;AAkDD;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,WAAW,EAAE,aAAa,EAC1B,QAAQ,EAAE,MAAM,OAAO;AACvB,6FAA6F;AAC7F,aAAa,GAAE,MAAM,OAAqB,GACxC,QAAQ,CAkEV"}
1
+ {"version":3,"file":"character-input.d.ts","sourceRoot":"","sources":["../src/character-input.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACxB;;;;OAIG;IACH,IAAI,IAAI,SAAS,CAAC;IAClB,gCAAgC;IAChC,OAAO,IAAI,aAAa,CAAC;IACzB,qCAAqC;IACrC,OAAO,IAAI,OAAO,CAAC;IACnB,wDAAwD;IACxD,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;CAChB;AAkJD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC7B,WAAW,EAAE,aAAa,EAC1B,QAAQ,EAAE,MAAM,OAAO;AACvB,6FAA6F;AAC7F,aAAa,GAAE,MAAM,OAAqB;AAC1C,4EAA4E;AAC5E,YAAY,GAAE,MAAM,OAAqB,GACvC,QAAQ,CAkIV"}
@@ -26,21 +26,99 @@ const SPACE_ROLES = new Set([
26
26
  "tab",
27
27
  "textbox",
28
28
  ]);
29
- /** Elements Space activates (or types into): the drift key must not steal it from them. */
30
- function isSpaceTarget(target) {
29
+ const asElement = (target) => {
31
30
  const el = target;
32
- if (!el || typeof el.tagName !== "string")
31
+ return el && typeof el.tagName === "string" ? el : null;
32
+ };
33
+ /**
34
+ * The focused element and its ancestors, innermost first. `composedPath()` reaches into
35
+ * (open) shadow roots, so a web-component dialog or slider is seen as itself rather than
36
+ * as its retargeted host; without it, the light-DOM parent chain.
37
+ */
38
+ function focusPath(e) {
39
+ const raw = typeof e.composedPath === "function" ? e.composedPath() : [];
40
+ const path = raw.map(asElement).filter((el) => el !== null);
41
+ if (path.length > 0)
42
+ return path;
43
+ const chain = [];
44
+ for (let el = asElement(e.target); el; el = asElement(el.parentElement))
45
+ chain.push(el);
46
+ return chain;
47
+ }
48
+ const roleOf = (el) => el && typeof el.getAttribute === "function" ? el.getAttribute("role") : null;
49
+ /** Elements Space activates (or types into): the drift and jump keys must not steal it from them. */
50
+ function isSpaceTarget(el) {
51
+ if (!el)
33
52
  return false;
34
53
  if (["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A", "SUMMARY"].includes(el.tagName))
35
54
  return true;
36
55
  if (el.isContentEditable === true)
37
56
  return true;
38
- const role = typeof el.getAttribute === "function" ? el.getAttribute("role") : null;
57
+ const role = roleOf(el);
39
58
  return role !== null && SPACE_ROLES.has(role);
40
59
  }
41
- function isTextInput(target) {
42
- const el = target;
43
- if (!el || typeof el.tagName !== "string")
60
+ /**
61
+ * Overlays that own the keyboard while focus is inside them (a dialog or menu opened
62
+ * mid-drive). Persistent containers such as a menubar or a sidebar listbox are not
63
+ * here: their items are arrow-key widgets, which keep WASD driving.
64
+ */
65
+ const KEY_OWNER_SELECTOR = 'dialog, [role="dialog"], [role="alertdialog"], [role="menu"]';
66
+ /** Marks the board's layers, to tell a host dialog that wraps the whole board apart. */
67
+ const BOARD_SELECTOR = "[data-layer-id]";
68
+ /** `<input type>`s that move their value or selection with the arrow keys. */
69
+ const ARROW_INPUTS = new Set(["range", "radio"]);
70
+ /** ARIA widgets (and composite containers that keep focus) that use the arrow keys. */
71
+ const ARROW_ROLES = new Set([
72
+ "combobox",
73
+ "grid",
74
+ "gridcell",
75
+ "listbox",
76
+ "menubar",
77
+ "menuitem",
78
+ "menuitemcheckbox",
79
+ "menuitemradio",
80
+ "option",
81
+ "radio",
82
+ "radiogroup",
83
+ "scrollbar",
84
+ "separator",
85
+ "slider",
86
+ "spinbutton",
87
+ "tab",
88
+ "tablist",
89
+ "toolbar",
90
+ "tree",
91
+ "treegrid",
92
+ "treeitem",
93
+ ]);
94
+ /**
95
+ * Focus is inside a dialog or menu: every bound key is left to it. A dialog that holds
96
+ * the board itself (the host shows the board in a modal) doesn't count, or the
97
+ * character could never be driven there.
98
+ */
99
+ function isInKeyOwner(path) {
100
+ const owner = path.find((el) => typeof el.matches === "function" && el.matches(KEY_OWNER_SELECTOR));
101
+ if (!owner)
102
+ return false;
103
+ return typeof owner.querySelector !== "function" || owner.querySelector(BOARD_SELECTOR) === null;
104
+ }
105
+ /** The focused element uses the arrow keys itself, so arrow bindings are left to it. */
106
+ function isArrowTarget(el) {
107
+ if (!el)
108
+ return false;
109
+ if (el.tagName === "INPUT" && ARROW_INPUTS.has((el.type ?? "").toLowerCase()))
110
+ return true;
111
+ const role = roleOf(el);
112
+ return role !== null && ARROW_ROLES.has(role);
113
+ }
114
+ /** Whether a key acts as an arrow (by code, or by key: numpad 4/6 with NumLock off). */
115
+ const isArrowKey = (code, key) => code.startsWith("Arrow") || (key ?? "").startsWith("Arrow");
116
+ /** Focus where no bound key may be taken: typing, or a dialog / menu. */
117
+ function blocksAllKeys(path) {
118
+ return isTextInput(path[0]) || isInKeyOwner(path);
119
+ }
120
+ function isTextInput(el) {
121
+ if (!el)
44
122
  return false;
45
123
  const tag = el.tagName;
46
124
  if (tag === "INPUT")
@@ -49,14 +127,26 @@ function isTextInput(target) {
49
127
  }
50
128
  /**
51
129
  * Listen for the bound keys on `window`. `isActive()` gates everything: while it is
52
- * false no key is captured (and none is swallowed). Held keys are cleared on window
53
- * blur / tab hide so a key released elsewhere can't get stuck "down".
130
+ * false no key is captured (and none is swallowed). Keys are left alone while focus is
131
+ * in a text field or inside a dialog / menu, arrow keys while a widget that uses them
132
+ * (slider, radio, tab …) has focus, and the drift / jump keys on controls Space activates.
133
+ * Held keys are cleared on window blur and tab hide, and released when focus moves to
134
+ * a target that uses them, so a key released elsewhere can't get stuck "down".
54
135
  */
55
136
  export function createKeyInput(initialKeys, isActive,
56
137
  /** Whether the drift key does anything now (else it is left to the host, e.g. Space-pan). */
57
- isDriftActive = () => false) {
138
+ isDriftActive = () => false,
139
+ /** Whether the jump key does anything now (else it is left to the host). */
140
+ isJumpActive = () => false) {
58
141
  let keys = initialKeys;
59
- const held = new Set();
142
+ /** Held codes → the `key` they produced (to tell numpad arrows from digits). */
143
+ const held = new Map();
144
+ /** Held codes that act as the jump key (bound when pressed, so a rebind can't strand one). */
145
+ const jumpHeld = new Set();
146
+ let jumpPressed = false;
147
+ /** Held codes that act as the drift key (as for the jump). */
148
+ const driftHeld = new Set();
149
+ let driftPressed = false;
60
150
  const directionOf = (code) => {
61
151
  for (const dir of ["up", "down", "left", "right"]) {
62
152
  if (keys[dir].includes(code))
@@ -64,54 +154,111 @@ isDriftActive = () => false) {
64
154
  }
65
155
  if (keys.drift?.includes(code) && isDriftActive())
66
156
  return "drift";
157
+ if (keys.jump?.includes(code) && isJumpActive())
158
+ return "jump";
67
159
  return null;
68
160
  };
161
+ /** Keys Space activates on buttons etc.: the drift and jump keys yield to those. */
162
+ const isSpaceLike = (code) => (keys.drift?.includes(code) ?? false) || (keys.jump?.includes(code) ?? false);
69
163
  const onKeyDown = (e) => {
70
- if (!isActive() || isTextInput(e.target))
164
+ if (!isActive())
71
165
  return;
72
166
  if (e.ctrlKey || e.metaKey || e.altKey)
73
167
  return; // leave chords (Cmd+S …) alone
74
168
  const dir = directionOf(e.code);
75
169
  if (!dir)
76
170
  return;
77
- if (dir === "drift" && isSpaceTarget(e.target))
171
+ const path = focusPath(e);
172
+ if (blocksAllKeys(path))
78
173
  return;
79
- held.add(e.code);
174
+ if ((dir === "drift" || dir === "jump") && isSpaceTarget(path[0]))
175
+ return;
176
+ if (isArrowKey(e.code, e.key) && isArrowTarget(path[0]))
177
+ return;
178
+ held.set(e.code, e.key ?? "");
179
+ if (dir === "jump") {
180
+ // Auto-repeat keydowns are not new presses.
181
+ if (!e.repeat && !jumpHeld.has(e.code))
182
+ jumpPressed = true;
183
+ jumpHeld.add(e.code);
184
+ }
185
+ else if (dir === "drift") {
186
+ if (!e.repeat && !driftHeld.has(e.code))
187
+ driftPressed = true;
188
+ driftHeld.add(e.code);
189
+ }
80
190
  e.preventDefault();
81
191
  e.stopImmediatePropagation();
82
192
  };
193
+ const release = (code) => {
194
+ jumpHeld.delete(code);
195
+ driftHeld.delete(code);
196
+ return held.delete(code);
197
+ };
83
198
  const onKeyUp = (e) => {
84
- if (!held.delete(e.code))
199
+ if (!release(e.code))
85
200
  return;
86
201
  e.preventDefault();
87
202
  e.stopImmediatePropagation();
88
203
  };
89
- const clear = () => held.clear();
204
+ const clear = () => {
205
+ held.clear();
206
+ jumpHeld.clear();
207
+ jumpPressed = false;
208
+ driftHeld.clear();
209
+ driftPressed = false;
210
+ };
90
211
  const onVisibility = () => {
91
212
  if (typeof document !== "undefined" && document.hidden)
92
213
  clear();
93
214
  };
215
+ // Focus moving to a target that uses a held key hands that key to it: release it, or
216
+ // the character keeps going and the key's keyup is swallowed (a button that Space
217
+ // activates on keyup would never fire). Keys the new target ignores stay held.
218
+ const onFocusIn = (e) => {
219
+ const path = focusPath(e);
220
+ if (blocksAllKeys(path)) {
221
+ clear();
222
+ return;
223
+ }
224
+ const owned = [...held].filter(([code, key]) => (isArrowKey(code, key) && isArrowTarget(path[0])) ||
225
+ (isSpaceLike(code) && isSpaceTarget(path[0])));
226
+ for (const [code] of owned)
227
+ release(code);
228
+ };
94
229
  const hasWindow = typeof window !== "undefined";
95
230
  if (hasWindow) {
96
231
  window.addEventListener("keydown", onKeyDown, true);
97
232
  window.addEventListener("keyup", onKeyUp, true);
98
233
  window.addEventListener("blur", clear);
99
234
  document.addEventListener("visibilitychange", onVisibility);
235
+ document.addEventListener("focusin", onFocusIn);
100
236
  }
101
237
  const isHeld = (dir) => keys[dir]?.some((c) => held.has(c)) ?? false;
102
238
  return {
103
- read: () => ({
104
- up: isHeld("up"),
105
- down: isHeld("down"),
106
- left: isHeld("left"),
107
- right: isHeld("right"),
108
- drift: isHeld("drift"),
109
- }),
239
+ read: () => {
240
+ const pressed = jumpPressed;
241
+ jumpPressed = false;
242
+ const drifted = driftPressed && isDriftActive();
243
+ driftPressed = false;
244
+ return {
245
+ up: isHeld("up"),
246
+ down: isHeld("down"),
247
+ left: isHeld("left"),
248
+ right: isHeld("right"),
249
+ // A key bound to both acts as the drift while drift is on (directionOf).
250
+ drift: isDriftActive() && isHeld("drift"),
251
+ jump: jumpHeld.size > 0,
252
+ jumpPressed: pressed,
253
+ driftPressed: drifted,
254
+ };
255
+ },
256
+ getKeys: () => keys,
110
257
  anyHeld: () => held.size > 0,
111
258
  setKeys(next) {
112
- // Rebinding movement keeps the drift key unless `drift` is given (`[]` unbinds).
113
- keys = { ...next, drift: next.drift ?? keys.drift };
114
- held.clear();
259
+ // Rebinding movement keeps the drift / jump keys unless given (`[]` unbinds).
260
+ keys = { ...next, drift: next.drift ?? keys.drift, jump: next.jump ?? keys.jump };
261
+ clear();
115
262
  },
116
263
  dispose() {
117
264
  if (!hasWindow)
@@ -120,6 +267,7 @@ isDriftActive = () => false) {
120
267
  window.removeEventListener("keyup", onKeyUp, true);
121
268
  window.removeEventListener("blur", clear);
122
269
  document.removeEventListener("visibilitychange", onVisibility);
270
+ document.removeEventListener("focusin", onFocusIn);
123
271
  },
124
272
  };
125
273
  }
@@ -1 +1 @@
1
- {"version":3,"file":"character-input.js","sourceRoot":"","sources":["../src/character-input.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"character-input.js","sourceRoot":"","sources":["../src/character-input.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { CharacterKeys } from "./character-types.js";
2
+ export interface KeyChoice {
3
+ /** HUD option value. */
4
+ value: string;
5
+ label: string;
6
+ /** `KeyboardEvent.code` values it binds. */
7
+ codes: readonly string[];
8
+ }
9
+ export declare const KEY_CHOICES: readonly KeyChoice[];
10
+ /** The choice whose codes are exactly `codes` (in any order), or `null`. */
11
+ export declare function keyChoiceOf(codes: readonly string[] | undefined): KeyChoice | null;
12
+ /** `keys` with the jump or drift key set to the choice `value` (unchanged for an unknown one). */
13
+ export declare function withKeyChoice(keys: CharacterKeys, action: "jump" | "drift", value: string): CharacterKeys;
14
+ //# sourceMappingURL=character-key-choices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-key-choices.d.ts","sourceRoot":"","sources":["../src/character-key-choices.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,SAAS;IACzB,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACzB;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAS3C,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAMlF;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAC5B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,MAAM,GAAG,OAAO,EACxB,KAAK,EAAE,MAAM,GACX,aAAa,CAGf"}
@@ -0,0 +1,23 @@
1
+ export const KEY_CHOICES = [
2
+ { value: "space", label: "Space", codes: ["Space"] },
3
+ { value: "shift", label: "Shift", codes: ["ShiftLeft", "ShiftRight"] },
4
+ { value: "z", label: "Z", codes: ["KeyZ"] },
5
+ { value: "x", label: "X", codes: ["KeyX"] },
6
+ { value: "c", label: "C", codes: ["KeyC"] },
7
+ { value: "j", label: "J", codes: ["KeyJ"] },
8
+ { value: "k", label: "K", codes: ["KeyK"] },
9
+ { value: "l", label: "L", codes: ["KeyL"] },
10
+ ];
11
+ /** The choice whose codes are exactly `codes` (in any order), or `null`. */
12
+ export function keyChoiceOf(codes) {
13
+ if (!codes)
14
+ return null;
15
+ const set = new Set(codes);
16
+ return (KEY_CHOICES.find((c) => c.codes.length === set.size && c.codes.every((k) => set.has(k))) ?? null);
17
+ }
18
+ /** `keys` with the jump or drift key set to the choice `value` (unchanged for an unknown one). */
19
+ export function withKeyChoice(keys, action, value) {
20
+ const choice = KEY_CHOICES.find((c) => c.value === value);
21
+ return choice ? { ...keys, [action]: [...choice.codes] } : keys;
22
+ }
23
+ //# sourceMappingURL=character-key-choices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-key-choices.js","sourceRoot":"","sources":["../src/character-key-choices.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,WAAW,GAAyB;IAChD,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE;IACpD,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE;IACtE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;CAC3C,CAAC;AAEF,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAoC;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,CACN,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAChG,CAAC;AACH,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,aAAa,CAC5B,IAAmB,EACnB,MAAwB,EACxB,KAAa;IAEb,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IAC1D,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { type Viewport } from "@edv4h/usketch-shared";
2
2
  import type { HeightAt } from "./character-physics.js";
3
- import type { CharacterStore } from "./character-store.js";
3
+ import { type CharacterStore } from "./character-store.js";
4
4
  export declare function CharacterLayer({ store, viewport, heightAt, }: {
5
5
  store: CharacterStore;
6
6
  viewport: Viewport;
@@ -1 +1 @@
1
- {"version":3,"file":"character-layer.d.ts","sourceRoot":"","sources":["../src/character-layer.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,QAAQ,EAA4C,MAAM,uBAAuB,CAAC;AAEhG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAsC3D,wBAAgB,cAAc,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,QAAQ,GACR,EAAE;IACF,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB,sCA+EA"}
1
+ {"version":3,"file":"character-layer.d.ts","sourceRoot":"","sources":["../src/character-layer.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,QAAQ,EAA4C,MAAM,uBAAuB,CAAC;AAEhG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,sBAAsB,CAAC;AA6CzE,wBAAgB,cAAc,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,QAAQ,GACR,EAAE;IACF,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB,sCA4GA"}