@edv4h/usketch-plugin-character 0.2.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.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/character-camera.d.ts +33 -0
- package/dist/character-camera.d.ts.map +1 -0
- package/dist/character-camera.js +45 -0
- package/dist/character-camera.js.map +1 -0
- package/dist/character-input.d.ts +18 -0
- package/dist/character-input.d.ts.map +1 -0
- package/dist/character-input.js +89 -0
- package/dist/character-input.js.map +1 -0
- package/dist/character-layer.d.ts +7 -0
- package/dist/character-layer.d.ts.map +1 -0
- package/dist/character-layer.js +53 -0
- package/dist/character-layer.js.map +1 -0
- package/dist/character-physics.d.ts +55 -0
- package/dist/character-physics.d.ts.map +1 -0
- package/dist/character-physics.js +97 -0
- package/dist/character-physics.js.map +1 -0
- package/dist/character-runtime.d.ts +30 -0
- package/dist/character-runtime.d.ts.map +1 -0
- package/dist/character-runtime.js +229 -0
- package/dist/character-runtime.js.map +1 -0
- package/dist/character-service.d.ts +47 -0
- package/dist/character-service.d.ts.map +1 -0
- package/dist/character-service.js +36 -0
- package/dist/character-service.js.map +1 -0
- package/dist/character-store.d.ts +55 -0
- package/dist/character-store.d.ts.map +1 -0
- package/dist/character-store.js +100 -0
- package/dist/character-store.js.map +1 -0
- package/dist/character-sync.d.ts +42 -0
- package/dist/character-sync.d.ts.map +1 -0
- package/dist/character-sync.js +117 -0
- package/dist/character-sync.js.map +1 -0
- package/dist/character-types.d.ts +52 -0
- package/dist/character-types.d.ts.map +1 -0
- package/dist/character-types.js +7 -0
- package/dist/character-types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +36 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +38 -0
- package/dist/plugin.js.map +1 -0
- package/dist/register-character-hud.d.ts +4 -0
- package/dist/register-character-hud.d.ts.map +1 -0
- package/dist/register-character-hud.js +122 -0
- package/dist/register-character-hud.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EdV4H
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @edv4h/usketch-plugin-character
|
|
2
|
+
|
|
3
|
+
A controllable character for uSketch: place it at a chosen spot on screen, drive it
|
|
4
|
+
with customizable keys (WASD by default), keep track of which way it faces, and have
|
|
5
|
+
the camera follow it — either keeping the world upright or turning the world with
|
|
6
|
+
the character's heading (racing view, via the Core viewport `rotation`). Other
|
|
7
|
+
users' characters appear alongside yours through Yjs awareness.
|
|
8
|
+
|
|
9
|
+
**The plugin draws nothing.** It computes positions and headings and hands them to
|
|
10
|
+
your `renderCharacter`, so the look (sprite, car, avatar, labels, animation) is
|
|
11
|
+
entirely the host's.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createCharacterPlugin } from "@edv4h/usketch-plugin-character";
|
|
15
|
+
|
|
16
|
+
createCharacterPlugin({
|
|
17
|
+
renderCharacter: ({ screenHeading, appearance, name }) => (
|
|
18
|
+
<div style={{ transform: `rotate(${screenHeading}deg)`, color: String(appearance.color) }}>▲</div>
|
|
19
|
+
),
|
|
20
|
+
appearance: { color: "#e44" }, // broadcast, so others draw you the same way
|
|
21
|
+
wsProvider, // optional: omit for single-player
|
|
22
|
+
userName: "Alice",
|
|
23
|
+
keys: { up: ["KeyW", "ArrowUp"] }, // KeyboardEvent.code values
|
|
24
|
+
cameraMode: "rotate", // "fixed" | "rotate" | "free"
|
|
25
|
+
controlScheme: "vehicle", // "directional" | "vehicle"
|
|
26
|
+
screenAnchor: { x: 0.5, y: 0.75 },
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Concepts
|
|
31
|
+
|
|
32
|
+
- **Heading**: degrees, `0` = up (world −y), clockwise-positive — the same convention
|
|
33
|
+
as CSS `rotate()` and the viewport `rotation`.
|
|
34
|
+
- **Render props**: `heading` (world), `screenHeading` (= `heading + viewport.rotation`,
|
|
35
|
+
what to rotate a top-down sprite by), `moving`, `speed`, `zoom`, `appearance`,
|
|
36
|
+
`name`, `isSelf`, `id`. Each character is placed centered on its screen position
|
|
37
|
+
in one screen-space layer, so self and remote characters look the same under zoom
|
|
38
|
+
and camera rotation.
|
|
39
|
+
- **Control schemes**
|
|
40
|
+
- `directional` — keys move in screen directions (camera-relative); the heading
|
|
41
|
+
turns toward the movement at `turnRate`. With the `rotate` camera, left/right
|
|
42
|
+
therefore circle (the camera follows the turn).
|
|
43
|
+
- `vehicle` — up/down throttle and reverse, left/right steer. Steering scales
|
|
44
|
+
with speed (a stopped car can't turn) and inverts in reverse.
|
|
45
|
+
- **Camera modes**
|
|
46
|
+
- `fixed` — follow, world stays upright.
|
|
47
|
+
- `rotate` — follow and turn the world by `-heading` (character always faces up).
|
|
48
|
+
- `free` — don't move the camera.
|
|
49
|
+
|
|
50
|
+
Panning by hand pauses following; press a movement key to resume. Zooming keeps
|
|
51
|
+
following. Turning the character off turns the camera back upright.
|
|
52
|
+
- **Multiplayer**: pass `wsProvider` (anything with a y-protocols `awareness`). Your
|
|
53
|
+
character is published under the `character` awareness field (~20 Hz, only when it
|
|
54
|
+
changes) and others are eased between updates. It's ephemeral: a character
|
|
55
|
+
vanishes when its owner disconnects or turns it off. Nothing is stored as shapes.
|
|
56
|
+
- **Input**: held keys are tracked from window `keydown`/`keyup` (capture phase,
|
|
57
|
+
`event.code`), swallowed while the character is on so single-letter tool
|
|
58
|
+
shortcuts don't fire. Typing in text fields, and key chords with Ctrl/Cmd/Alt,
|
|
59
|
+
are ignored.
|
|
60
|
+
|
|
61
|
+
## HUD
|
|
62
|
+
|
|
63
|
+
A "キャラクター" settings group (on/off, camera, scheme, speed, turn rate, screen
|
|
64
|
+
position, camera lag) plus actions to toggle the character and to place it at the
|
|
65
|
+
screen position.
|
|
66
|
+
|
|
67
|
+
## Service
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { getCharacterApi } from "@edv4h/usketch-plugin-character";
|
|
71
|
+
|
|
72
|
+
const api = getCharacterApi(app.services);
|
|
73
|
+
api?.enable();
|
|
74
|
+
api?.setCameraMode("rotate");
|
|
75
|
+
api?.setRenderer(myRenderer);
|
|
76
|
+
api?.getPose(); // { x, y, heading, speed, moving }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Limitations
|
|
80
|
+
|
|
81
|
+
While the camera is rotated, overlays that compute screen positions themselves
|
|
82
|
+
(snap guides, resize handles, connectors, comment badges, the background grid)
|
|
83
|
+
are not rotation-aware yet. Tools that go through the Core `screenToWorld` get
|
|
84
|
+
correct coordinates.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Point, type Viewport } from "@edv4h/usketch-shared";
|
|
2
|
+
/**
|
|
3
|
+
* - `fixed`: follow; the world stays axis-aligned (rotation 0) and the character
|
|
4
|
+
* turns on screen.
|
|
5
|
+
* - `rotate`: follow AND turn the world by `-heading`, so the character always
|
|
6
|
+
* faces screen-up (racing view).
|
|
7
|
+
* - `free`: don't touch the camera.
|
|
8
|
+
*/
|
|
9
|
+
export type CameraMode = "fixed" | "rotate" | "free";
|
|
10
|
+
/** The smoothed camera target: the world point pinned to the anchor + rotation. */
|
|
11
|
+
export interface CameraFocus {
|
|
12
|
+
focus: Point;
|
|
13
|
+
rotation: number;
|
|
14
|
+
}
|
|
15
|
+
/** The camera rotation a mode wants for a heading. */
|
|
16
|
+
export declare function targetRotation(mode: CameraMode, heading: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Frame-rate-independent smoothing factor: `smoothing` 0 = rigid (always 1),
|
|
19
|
+
* approaching 1 = heavier lag. It is the fraction LEFT after 1/60 s.
|
|
20
|
+
*/
|
|
21
|
+
export declare function smoothingAlpha(smoothing: number, dt: number): number;
|
|
22
|
+
/** Ease `current` toward `target` by `alpha` (rotation the short way round). */
|
|
23
|
+
export declare function easeFocus(current: CameraFocus, target: CameraFocus, alpha: number): CameraFocus;
|
|
24
|
+
/** Screen-pixel anchor from a fractional anchor and the canvas size. */
|
|
25
|
+
export declare function anchorPx(anchor: Point, size: {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}): Point;
|
|
29
|
+
/** The viewport that pins `cam.focus` to `anchor` (px) at `zoom`, turned by `cam.rotation`. */
|
|
30
|
+
export declare function followViewport(cam: CameraFocus, anchor: Point, zoom: number): Viewport;
|
|
31
|
+
/** True when two viewports are the same to within sub-pixel / sub-degree noise. */
|
|
32
|
+
export declare function sameViewport(a: Viewport, b: Viewport): boolean;
|
|
33
|
+
//# sourceMappingURL=character-camera.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-camera.d.ts","sourceRoot":"","sources":["../src/character-camera.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,KAAK,EAEV,KAAK,QAAQ,EAGb,MAAM,uBAAuB,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAErD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAW/F;AAED,wEAAwE;AACxE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAEtF;AAED,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAEtF;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAO9D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure follow-camera math: where the viewport should be so the character sits at
|
|
2
|
+
// its screen anchor, optionally turning the world so the character faces up.
|
|
3
|
+
import { shortestAngleDelta, viewportAnchoredAt, wrapDeg, } from "@edv4h/usketch-shared";
|
|
4
|
+
/** The camera rotation a mode wants for a heading. */
|
|
5
|
+
export function targetRotation(mode, heading) {
|
|
6
|
+
return mode === "rotate" ? wrapDeg(-heading) : 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Frame-rate-independent smoothing factor: `smoothing` 0 = rigid (always 1),
|
|
10
|
+
* approaching 1 = heavier lag. It is the fraction LEFT after 1/60 s.
|
|
11
|
+
*/
|
|
12
|
+
export function smoothingAlpha(smoothing, dt) {
|
|
13
|
+
const s = Math.min(0.99, Math.max(0, smoothing));
|
|
14
|
+
if (s === 0)
|
|
15
|
+
return 1;
|
|
16
|
+
return 1 - s ** (dt * 60);
|
|
17
|
+
}
|
|
18
|
+
/** Ease `current` toward `target` by `alpha` (rotation the short way round). */
|
|
19
|
+
export function easeFocus(current, target, alpha) {
|
|
20
|
+
if (alpha >= 1)
|
|
21
|
+
return target;
|
|
22
|
+
return {
|
|
23
|
+
focus: {
|
|
24
|
+
x: current.focus.x + (target.focus.x - current.focus.x) * alpha,
|
|
25
|
+
y: current.focus.y + (target.focus.y - current.focus.y) * alpha,
|
|
26
|
+
},
|
|
27
|
+
rotation: wrapDeg(current.rotation + shortestAngleDelta(current.rotation, target.rotation) * alpha),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Screen-pixel anchor from a fractional anchor and the canvas size. */
|
|
31
|
+
export function anchorPx(anchor, size) {
|
|
32
|
+
return { x: anchor.x * size.width, y: anchor.y * size.height };
|
|
33
|
+
}
|
|
34
|
+
/** The viewport that pins `cam.focus` to `anchor` (px) at `zoom`, turned by `cam.rotation`. */
|
|
35
|
+
export function followViewport(cam, anchor, zoom) {
|
|
36
|
+
return viewportAnchoredAt(cam.focus, anchor, zoom, cam.rotation);
|
|
37
|
+
}
|
|
38
|
+
/** True when two viewports are the same to within sub-pixel / sub-degree noise. */
|
|
39
|
+
export function sameViewport(a, b) {
|
|
40
|
+
return (Math.abs(a.x - b.x) < 0.01 &&
|
|
41
|
+
Math.abs(a.y - b.y) < 0.01 &&
|
|
42
|
+
Math.abs(a.zoom - b.zoom) < 1e-6 &&
|
|
43
|
+
Math.abs(shortestAngleDelta(a.rotation ?? 0, b.rotation ?? 0)) < 1e-4);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=character-camera.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-camera.js","sourceRoot":"","sources":["../src/character-camera.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,6EAA6E;AAC7E,OAAO,EAEN,kBAAkB,EAElB,kBAAkB,EAClB,OAAO,GACP,MAAM,uBAAuB,CAAC;AAiB/B,sDAAsD;AACtD,MAAM,UAAU,cAAc,CAAC,IAAgB,EAAE,OAAe;IAC/D,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,EAAU;IAC3D,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IACjD,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,SAAS,CAAC,OAAoB,EAAE,MAAmB,EAAE,KAAa;IACjF,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IAC9B,OAAO;QACN,KAAK,EAAE;YACN,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK;YAC/D,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK;SAC/D;QACD,QAAQ,EAAE,OAAO,CAChB,OAAO,CAAC,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,KAAK,CAChF;KACD,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,QAAQ,CAAC,MAAa,EAAE,IAAuC;IAC9E,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;AAChE,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,cAAc,CAAC,GAAgB,EAAE,MAAa,EAAE,IAAY;IAC3E,OAAO,kBAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;AAClE,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,CAAW,EAAE,CAAW;IACpD,OAAO,CACN,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;QAC1B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;QAC1B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI;QAChC,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CACrE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MoveInput } from "./character-physics.js";
|
|
2
|
+
import type { CharacterKeys } from "./character-types.js";
|
|
3
|
+
export interface KeyInput {
|
|
4
|
+
/** The currently held directions. */
|
|
5
|
+
read(): MoveInput;
|
|
6
|
+
/** Whether any bound key is held. */
|
|
7
|
+
anyHeld(): boolean;
|
|
8
|
+
/** Replace the key bindings (held state is cleared). */
|
|
9
|
+
setKeys(keys: CharacterKeys): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 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".
|
|
16
|
+
*/
|
|
17
|
+
export declare function createKeyInput(initialKeys: CharacterKeys, isActive: () => boolean): KeyInput;
|
|
18
|
+
//# sourceMappingURL=character-input.d.ts.map
|
|
@@ -0,0 +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,qCAAqC;IACrC,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;AAuBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,GAAG,QAAQ,CA6D5F"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** `<input type>`s that don't take typed text (a focused HUD checkbox must not eat WASD). */
|
|
2
|
+
const NON_TEXT_INPUTS = new Set([
|
|
3
|
+
"checkbox",
|
|
4
|
+
"radio",
|
|
5
|
+
"button",
|
|
6
|
+
"submit",
|
|
7
|
+
"reset",
|
|
8
|
+
"range",
|
|
9
|
+
"color",
|
|
10
|
+
"file",
|
|
11
|
+
"image",
|
|
12
|
+
]);
|
|
13
|
+
function isTextInput(target) {
|
|
14
|
+
const el = target;
|
|
15
|
+
if (!el || typeof el.tagName !== "string")
|
|
16
|
+
return false;
|
|
17
|
+
const tag = el.tagName;
|
|
18
|
+
if (tag === "INPUT")
|
|
19
|
+
return !NON_TEXT_INPUTS.has((el.type ?? "text").toLowerCase());
|
|
20
|
+
return tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable === true;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Listen for the bound keys on `window`. `isActive()` gates everything: while it is
|
|
24
|
+
* false no key is captured (and none is swallowed). Held keys are cleared on window
|
|
25
|
+
* blur / tab hide so a key released elsewhere can't get stuck "down".
|
|
26
|
+
*/
|
|
27
|
+
export function createKeyInput(initialKeys, isActive) {
|
|
28
|
+
let keys = initialKeys;
|
|
29
|
+
const held = new Set();
|
|
30
|
+
const directionOf = (code) => {
|
|
31
|
+
for (const dir of ["up", "down", "left", "right"]) {
|
|
32
|
+
if (keys[dir].includes(code))
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const onKeyDown = (e) => {
|
|
38
|
+
if (!isActive() || isTextInput(e.target))
|
|
39
|
+
return;
|
|
40
|
+
if (e.ctrlKey || e.metaKey || e.altKey)
|
|
41
|
+
return; // leave chords (Cmd+S …) alone
|
|
42
|
+
if (!directionOf(e.code))
|
|
43
|
+
return;
|
|
44
|
+
held.add(e.code);
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
e.stopImmediatePropagation();
|
|
47
|
+
};
|
|
48
|
+
const onKeyUp = (e) => {
|
|
49
|
+
if (!held.delete(e.code))
|
|
50
|
+
return;
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopImmediatePropagation();
|
|
53
|
+
};
|
|
54
|
+
const clear = () => held.clear();
|
|
55
|
+
const onVisibility = () => {
|
|
56
|
+
if (typeof document !== "undefined" && document.hidden)
|
|
57
|
+
clear();
|
|
58
|
+
};
|
|
59
|
+
const hasWindow = typeof window !== "undefined";
|
|
60
|
+
if (hasWindow) {
|
|
61
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
62
|
+
window.addEventListener("keyup", onKeyUp, true);
|
|
63
|
+
window.addEventListener("blur", clear);
|
|
64
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
65
|
+
}
|
|
66
|
+
const isHeld = (dir) => keys[dir].some((c) => held.has(c));
|
|
67
|
+
return {
|
|
68
|
+
read: () => ({
|
|
69
|
+
up: isHeld("up"),
|
|
70
|
+
down: isHeld("down"),
|
|
71
|
+
left: isHeld("left"),
|
|
72
|
+
right: isHeld("right"),
|
|
73
|
+
}),
|
|
74
|
+
anyHeld: () => held.size > 0,
|
|
75
|
+
setKeys(next) {
|
|
76
|
+
keys = next;
|
|
77
|
+
held.clear();
|
|
78
|
+
},
|
|
79
|
+
dispose() {
|
|
80
|
+
if (!hasWindow)
|
|
81
|
+
return;
|
|
82
|
+
window.removeEventListener("keydown", onKeyDown, true);
|
|
83
|
+
window.removeEventListener("keyup", onKeyUp, true);
|
|
84
|
+
window.removeEventListener("blur", clear);
|
|
85
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=character-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-input.js","sourceRoot":"","sources":["../src/character-input.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Viewport } from "@edv4h/usketch-shared";
|
|
2
|
+
import type { CharacterStore } from "./character-store.js";
|
|
3
|
+
export declare function CharacterLayer({ store, viewport }: {
|
|
4
|
+
store: CharacterStore;
|
|
5
|
+
viewport: Viewport;
|
|
6
|
+
}): import("react").JSX.Element | null;
|
|
7
|
+
//# sourceMappingURL=character-layer.d.ts.map
|
|
@@ -0,0 +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,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0B3D,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,sCAuDhG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The one fixed (screen-space) layer that places every character — self and remote
|
|
3
|
+
// — at `worldToScreen(position)`. It draws NOTHING itself: each spot is a zero-size,
|
|
4
|
+
// pointer-transparent wrapper whose content is the host's `renderCharacter`. One
|
|
5
|
+
// screen-space layer keeps self/remote consistent (same size under zoom, same
|
|
6
|
+
// `screenHeading` math under camera rotation).
|
|
7
|
+
import { viewportRotation, worldToScreen, wrapDeg } from "@edv4h/usketch-shared";
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
function Spot({ x, y, renderer, props, }) {
|
|
10
|
+
return (_jsx("div", { "data-character-id": props.id, style: { position: "absolute", left: x, top: y, width: 0, height: 0, pointerEvents: "none" }, children: _jsx("div", { style: { position: "absolute", left: 0, top: 0, transform: "translate(-50%, -50%)" }, children: renderer(props) }) }));
|
|
11
|
+
}
|
|
12
|
+
export function CharacterLayer({ store, viewport }) {
|
|
13
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
14
|
+
const renderer = store.getRenderer();
|
|
15
|
+
if (!renderer)
|
|
16
|
+
return null; // headless: no look supplied → draw nothing
|
|
17
|
+
const rot = viewportRotation(viewport);
|
|
18
|
+
const spots = [];
|
|
19
|
+
for (const r of state.remotes.values()) {
|
|
20
|
+
const p = worldToScreen(r.x, r.y, viewport);
|
|
21
|
+
spots.push(_jsx(Spot, { x: p.x, y: p.y, renderer: renderer, props: {
|
|
22
|
+
id: r.id,
|
|
23
|
+
isSelf: false,
|
|
24
|
+
heading: r.heading,
|
|
25
|
+
screenHeading: wrapDeg(r.heading + rot),
|
|
26
|
+
moving: r.moving,
|
|
27
|
+
speed: r.speed,
|
|
28
|
+
zoom: viewport.zoom,
|
|
29
|
+
appearance: r.appearance,
|
|
30
|
+
name: r.name,
|
|
31
|
+
} }, `remote-${r.id}`));
|
|
32
|
+
}
|
|
33
|
+
if (state.enabled && state.spawned) {
|
|
34
|
+
const { pose } = state;
|
|
35
|
+
const p = worldToScreen(pose.x, pose.y, viewport);
|
|
36
|
+
// Self last → drawn on top of the others.
|
|
37
|
+
spots.push(_jsx(Spot, { x: p.x, y: p.y, renderer: renderer, props: {
|
|
38
|
+
id: "self",
|
|
39
|
+
isSelf: true,
|
|
40
|
+
heading: pose.heading,
|
|
41
|
+
screenHeading: wrapDeg(pose.heading + rot),
|
|
42
|
+
moving: pose.moving,
|
|
43
|
+
speed: pose.speed,
|
|
44
|
+
zoom: viewport.zoom,
|
|
45
|
+
appearance: state.appearance,
|
|
46
|
+
name: state.name,
|
|
47
|
+
} }, "self"));
|
|
48
|
+
}
|
|
49
|
+
if (spots.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return _jsx("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: spots });
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=character-layer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-layer.js","sourceRoot":"","sources":["../src/character-layer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type ControlScheme = "directional" | "vehicle";
|
|
2
|
+
/** Which movement directions are currently held. */
|
|
3
|
+
export interface MoveInput {
|
|
4
|
+
up: boolean;
|
|
5
|
+
down: boolean;
|
|
6
|
+
left: boolean;
|
|
7
|
+
right: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const NO_INPUT: MoveInput;
|
|
10
|
+
export interface CharacterPose {
|
|
11
|
+
/** World position. */
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
/** Facing, degrees (0 = up / -y, clockwise). */
|
|
15
|
+
heading: number;
|
|
16
|
+
/** Signed speed along the heading, world units / s (negative = reversing). */
|
|
17
|
+
speed: number;
|
|
18
|
+
/** Whether the character moved this step. */
|
|
19
|
+
moving: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface MotionParams {
|
|
22
|
+
/** Top speed, world units / s. */
|
|
23
|
+
moveSpeed: number;
|
|
24
|
+
/** Turn rate, degrees / s. */
|
|
25
|
+
turnRate: number;
|
|
26
|
+
/** Vehicle: throttle acceleration, world units / s². */
|
|
27
|
+
acceleration: number;
|
|
28
|
+
/** Vehicle: coasting / braking deceleration, world units / s². */
|
|
29
|
+
friction: number;
|
|
30
|
+
/** Vehicle: reverse top speed as a fraction of `moveSpeed`. */
|
|
31
|
+
reverseRatio: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_MOTION: MotionParams;
|
|
34
|
+
/** Unit vector of a heading in world space (0° → (0, -1), 90° → (1, 0)). */
|
|
35
|
+
export declare function headingVector(deg: number): {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
};
|
|
39
|
+
/** Heading (degrees) of a non-zero world vector. */
|
|
40
|
+
export declare function vectorHeading(x: number, y: number): number;
|
|
41
|
+
/** Move `from` toward `to` by at most `maxStep` degrees, the short way round. */
|
|
42
|
+
export declare function turnToward(from: number, to: number, maxStep: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* Advance `pose` by `dt` seconds.
|
|
45
|
+
*
|
|
46
|
+
* - `directional`: the held keys give a SCREEN-space direction (up = screen up),
|
|
47
|
+
* turned into world space through the camera rotation; the character moves that
|
|
48
|
+
* way at `moveSpeed` and its heading turns toward it at `turnRate`. (Turning
|
|
49
|
+
* rather than snapping keeps a heading-following camera from spinning.)
|
|
50
|
+
* - `vehicle`: up/down are throttle/reverse along the heading, left/right steer.
|
|
51
|
+
* Steering authority scales with speed (a stopped car can't turn) and inverts
|
|
52
|
+
* when reversing, like a car.
|
|
53
|
+
*/
|
|
54
|
+
export declare function stepCharacter(pose: CharacterPose, input: MoveInput, dt: number, scheme: ControlScheme, params: MotionParams, cameraRotation?: number): CharacterPose;
|
|
55
|
+
//# sourceMappingURL=character-physics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-physics.d.ts","sourceRoot":"","sources":["../src/character-physics.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,SAAS,CAAC;AAEtD,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CACf;AAED,eAAO,MAAM,QAAQ,EAAE,SAAiE,CAAC;AAEzF,MAAM,WAAW,aAAa;IAC7B,sBAAsB;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC5B,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,EAAE,YAM5B,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAGnE;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAI5E;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,SAAS,EAChB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,YAAY,EACpB,cAAc,SAAI,GAChB,aAAa,CAoDf"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Pure motion model (no React / DOM): input + dt → next pose. Headings are degrees,
|
|
2
|
+
// 0 = up (world -y), clockwise-positive — the same convention as CSS `rotate()` and
|
|
3
|
+
// the Core viewport `rotation`, so `screenHeading = heading + viewport.rotation`.
|
|
4
|
+
import { shortestAngleDelta, wrapDeg } from "@edv4h/usketch-shared";
|
|
5
|
+
export const NO_INPUT = { up: false, down: false, left: false, right: false };
|
|
6
|
+
export const DEFAULT_MOTION = {
|
|
7
|
+
moveSpeed: 400,
|
|
8
|
+
turnRate: 240,
|
|
9
|
+
acceleration: 900,
|
|
10
|
+
friction: 700,
|
|
11
|
+
reverseRatio: 0.5,
|
|
12
|
+
};
|
|
13
|
+
/** Unit vector of a heading in world space (0° → (0, -1), 90° → (1, 0)). */
|
|
14
|
+
export function headingVector(deg) {
|
|
15
|
+
const rad = (deg * Math.PI) / 180;
|
|
16
|
+
return { x: Math.sin(rad), y: -Math.cos(rad) };
|
|
17
|
+
}
|
|
18
|
+
/** Heading (degrees) of a non-zero world vector. */
|
|
19
|
+
export function vectorHeading(x, y) {
|
|
20
|
+
return wrapDeg((Math.atan2(x, -y) * 180) / Math.PI);
|
|
21
|
+
}
|
|
22
|
+
/** Move `from` toward `to` by at most `maxStep` degrees, the short way round. */
|
|
23
|
+
export function turnToward(from, to, maxStep) {
|
|
24
|
+
const d = shortestAngleDelta(from, to);
|
|
25
|
+
if (Math.abs(d) <= maxStep)
|
|
26
|
+
return wrapDeg(to);
|
|
27
|
+
return wrapDeg(from + Math.sign(d) * maxStep);
|
|
28
|
+
}
|
|
29
|
+
function approach(v, target, step) {
|
|
30
|
+
if (v < target)
|
|
31
|
+
return Math.min(target, v + step);
|
|
32
|
+
return Math.max(target, v - step);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Advance `pose` by `dt` seconds.
|
|
36
|
+
*
|
|
37
|
+
* - `directional`: the held keys give a SCREEN-space direction (up = screen up),
|
|
38
|
+
* turned into world space through the camera rotation; the character moves that
|
|
39
|
+
* way at `moveSpeed` and its heading turns toward it at `turnRate`. (Turning
|
|
40
|
+
* rather than snapping keeps a heading-following camera from spinning.)
|
|
41
|
+
* - `vehicle`: up/down are throttle/reverse along the heading, left/right steer.
|
|
42
|
+
* Steering authority scales with speed (a stopped car can't turn) and inverts
|
|
43
|
+
* when reversing, like a car.
|
|
44
|
+
*/
|
|
45
|
+
export function stepCharacter(pose, input, dt, scheme, params, cameraRotation = 0) {
|
|
46
|
+
if (dt <= 0)
|
|
47
|
+
return pose;
|
|
48
|
+
if (scheme === "directional") {
|
|
49
|
+
const sx = (input.right ? 1 : 0) - (input.left ? 1 : 0);
|
|
50
|
+
const sy = (input.down ? 1 : 0) - (input.up ? 1 : 0);
|
|
51
|
+
if (sx === 0 && sy === 0) {
|
|
52
|
+
return pose.moving || pose.speed !== 0 ? { ...pose, speed: 0, moving: false } : pose;
|
|
53
|
+
}
|
|
54
|
+
// Screen direction → world direction: undo the camera rotation.
|
|
55
|
+
const target = wrapDeg(vectorHeading(sx, sy) - cameraRotation);
|
|
56
|
+
const dir = headingVector(target);
|
|
57
|
+
const dist = params.moveSpeed * dt;
|
|
58
|
+
return {
|
|
59
|
+
x: pose.x + dir.x * dist,
|
|
60
|
+
y: pose.y + dir.y * dist,
|
|
61
|
+
heading: turnToward(pose.heading, target, params.turnRate * dt),
|
|
62
|
+
speed: params.moveSpeed,
|
|
63
|
+
moving: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const throttle = (input.up ? 1 : 0) - (input.down ? 1 : 0);
|
|
67
|
+
let speed = pose.speed;
|
|
68
|
+
if (throttle > 0) {
|
|
69
|
+
// Braking from reverse uses friction + throttle for a snappier stop.
|
|
70
|
+
const accel = speed < 0 ? params.acceleration + params.friction : params.acceleration;
|
|
71
|
+
speed = approach(speed, params.moveSpeed, accel * dt);
|
|
72
|
+
}
|
|
73
|
+
else if (throttle < 0) {
|
|
74
|
+
const accel = speed > 0 ? params.acceleration + params.friction : params.acceleration;
|
|
75
|
+
speed = approach(speed, -params.moveSpeed * params.reverseRatio, accel * dt);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
speed = approach(speed, 0, params.friction * dt);
|
|
79
|
+
}
|
|
80
|
+
const steer = (input.right ? 1 : 0) - (input.left ? 1 : 0);
|
|
81
|
+
const authority = Math.min(1, (Math.abs(speed) / Math.max(1, params.moveSpeed)) * 3);
|
|
82
|
+
const heading = steer === 0 || authority === 0
|
|
83
|
+
? pose.heading
|
|
84
|
+
: wrapDeg(pose.heading + steer * Math.sign(speed) * authority * params.turnRate * dt);
|
|
85
|
+
if (speed === 0 && heading === pose.heading) {
|
|
86
|
+
return pose.moving || pose.speed !== 0 ? { ...pose, speed: 0, moving: false } : pose;
|
|
87
|
+
}
|
|
88
|
+
const dir = headingVector(heading);
|
|
89
|
+
return {
|
|
90
|
+
x: pose.x + dir.x * speed * dt,
|
|
91
|
+
y: pose.y + dir.y * speed * dt,
|
|
92
|
+
heading,
|
|
93
|
+
speed,
|
|
94
|
+
moving: speed !== 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=character-physics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-physics.js","sourceRoot":"","sources":["../src/character-physics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type PluginContext, type Point } from "@edv4h/usketch-shared";
|
|
2
|
+
import type { CharacterStore } from "./character-store.js";
|
|
3
|
+
import { type CharacterAwareness } from "./character-sync.js";
|
|
4
|
+
import type { CharacterKeys } from "./character-types.js";
|
|
5
|
+
export declare const CHARACTER_LAYER_ID = "character";
|
|
6
|
+
/** `viewport:claimed` priority: above start-position (10), below deep links (100). */
|
|
7
|
+
export declare const CHARACTER_VIEWPORT_PRIORITY = 20;
|
|
8
|
+
/**
|
|
9
|
+
* The main canvas's pixel size. Found via `[data-layer-id]` wrappers' parent (the
|
|
10
|
+
* app build strips `data-testid`, so `canvas-container` can't be queried); the
|
|
11
|
+
* largest wins when a minimap also renders layers. Falls back to the window.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canvasSize(): {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
export interface CharacterRuntimeOptions {
|
|
18
|
+
keys: CharacterKeys;
|
|
19
|
+
awareness?: CharacterAwareness;
|
|
20
|
+
}
|
|
21
|
+
export interface CharacterRuntime {
|
|
22
|
+
/** Move the character to the world point under its screen anchor. */
|
|
23
|
+
placeAtAnchor(): void;
|
|
24
|
+
/** The screen-px anchor (for rotating the camera about the character's spot). */
|
|
25
|
+
anchorPoint(): Point;
|
|
26
|
+
setKeys(keys: CharacterKeys): void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function setupCharacterRuntime(ctx: PluginContext, store: CharacterStore, options: CharacterRuntimeOptions): CharacterRuntime;
|
|
30
|
+
//# sourceMappingURL=character-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-runtime.d.ts","sourceRoot":"","sources":["../src/character-runtime.tsx"],"names":[],"mappings":"AAGA,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,KAAK,EAIV,MAAM,uBAAuB,CAAC;AAa/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACN,KAAK,kBAAkB,EAIvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,sBAAsB,CAAC;AAE3E,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAG9C,sFAAsF;AACtF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAe9C;;;;GAIG;AACH,wBAAgB,UAAU,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAkB9D;AAED,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAChC,qEAAqE;IACrE,aAAa,IAAI,IAAI,CAAC;IACtB,iFAAiF;IACjF,WAAW,IAAI,KAAK,CAAC;IACrB,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,uBAAuB,GAC9B,gBAAgB,CAmKlB"}
|