rn-gamekit 0.1.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/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/lib/module/assets/defineAssets.js +48 -0
- package/lib/module/assets/defineAssets.js.map +1 -0
- package/lib/module/assets/errors.js +38 -0
- package/lib/module/assets/errors.js.map +1 -0
- package/lib/module/assets/types.js +2 -0
- package/lib/module/assets/types.js.map +1 -0
- package/lib/module/assets/validation.js +99 -0
- package/lib/module/assets/validation.js.map +1 -0
- package/lib/module/core/frameDriver.js +20 -0
- package/lib/module/core/frameDriver.js.map +1 -0
- package/lib/module/core/input/createInputBuffer.js +296 -0
- package/lib/module/core/input/createInputBuffer.js.map +1 -0
- package/lib/module/core/input/types.js +4 -0
- package/lib/module/core/input/types.js.map +1 -0
- package/lib/module/core/session/createGameSession.js +554 -0
- package/lib/module/core/session/createGameSession.js.map +1 -0
- package/lib/module/core/session/deepFreeze.js +155 -0
- package/lib/module/core/session/deepFreeze.js.map +1 -0
- package/lib/module/core/session/diagnostics.js +2 -0
- package/lib/module/core/session/diagnostics.js.map +1 -0
- package/lib/module/core/session/types.js +43 -0
- package/lib/module/core/session/types.js.map +1 -0
- package/lib/module/definition/defineGame.js +57 -0
- package/lib/module/definition/defineGame.js.map +1 -0
- package/lib/module/definition/types.js +4 -0
- package/lib/module/definition/types.js.map +1 -0
- package/lib/module/geometry/types.js +2 -0
- package/lib/module/geometry/types.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react/GamePointerInput.js +385 -0
- package/lib/module/react/GamePointerInput.js.map +1 -0
- package/lib/module/react/GameView.js +223 -0
- package/lib/module/react/GameView.js.map +1 -0
- package/lib/module/react/alphaClock.js +42 -0
- package/lib/module/react/alphaClock.js.map +1 -0
- package/lib/module/react/assets/createGameAssetStore.js +358 -0
- package/lib/module/react/assets/createGameAssetStore.js.map +1 -0
- package/lib/module/react/assets/decodeSkiaImage.js +54 -0
- package/lib/module/react/assets/decodeSkiaImage.js.map +1 -0
- package/lib/module/react/assets/errors.js +10 -0
- package/lib/module/react/assets/errors.js.map +1 -0
- package/lib/module/react/assets/useGameAssets.js +160 -0
- package/lib/module/react/assets/useGameAssets.js.map +1 -0
- package/lib/module/react/bindAppLifecycle.js +58 -0
- package/lib/module/react/bindAppLifecycle.js.map +1 -0
- package/lib/module/react/bindGameSession.js +37 -0
- package/lib/module/react/bindGameSession.js.map +1 -0
- package/lib/module/react/gestureLifecycle.js +68 -0
- package/lib/module/react/gestureLifecycle.js.map +1 -0
- package/lib/module/react/instrumentation.js +4 -0
- package/lib/module/react/instrumentation.js.map +1 -0
- package/lib/module/react/pointerBinding.js +196 -0
- package/lib/module/react/pointerBinding.js.map +1 -0
- package/lib/module/react/pointerCoalescer.js +224 -0
- package/lib/module/react/pointerCoalescer.js.map +1 -0
- package/lib/module/react/pointerContainment.js +27 -0
- package/lib/module/react/pointerContainment.js.map +1 -0
- package/lib/module/react/sprites/GameSprite.js +116 -0
- package/lib/module/react/sprites/GameSprite.js.map +1 -0
- package/lib/module/react/sprites/GameWorld2D.js +44 -0
- package/lib/module/react/sprites/GameWorld2D.js.map +1 -0
- package/lib/module/react/sprites/Sprite.js +147 -0
- package/lib/module/react/sprites/Sprite.js.map +1 -0
- package/lib/module/react/sprites/SpriteBatch.js +173 -0
- package/lib/module/react/sprites/SpriteBatch.js.map +1 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js +25 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js.map +1 -0
- package/lib/module/react/sprites/spriteTransform.js +135 -0
- package/lib/module/react/sprites/spriteTransform.js.map +1 -0
- package/lib/module/react/viewportBinding.js +101 -0
- package/lib/module/react/viewportBinding.js.map +1 -0
- package/lib/module/react.js +13 -0
- package/lib/module/react.js.map +1 -0
- package/lib/module/scene/defineScene.js +29 -0
- package/lib/module/scene/defineScene.js.map +1 -0
- package/lib/module/scene/types.js +4 -0
- package/lib/module/scene/types.js.map +1 -0
- package/lib/module/sprites/sampleSpriteClip.js +50 -0
- package/lib/module/sprites/sampleSpriteClip.js.map +1 -0
- package/lib/module/sprites/spriteAnimationState.js +165 -0
- package/lib/module/sprites/spriteAnimationState.js.map +1 -0
- package/lib/module/testing.js +57 -0
- package/lib/module/testing.js.map +1 -0
- package/lib/module/viewport2d/index.js +4 -0
- package/lib/module/viewport2d/index.js.map +1 -0
- package/lib/module/viewport2d/math.js +129 -0
- package/lib/module/viewport2d/math.js.map +1 -0
- package/lib/module/viewport2d/types.js +2 -0
- package/lib/module/viewport2d/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/defineAssets.d.ts +20 -0
- package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -0
- package/lib/typescript/src/assets/errors.d.ts +20 -0
- package/lib/typescript/src/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/assets/types.d.ts +138 -0
- package/lib/typescript/src/assets/types.d.ts.map +1 -0
- package/lib/typescript/src/assets/validation.d.ts +15 -0
- package/lib/typescript/src/assets/validation.d.ts.map +1 -0
- package/lib/typescript/src/core/frameDriver.d.ts +12 -0
- package/lib/typescript/src/core/frameDriver.d.ts.map +1 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts +12 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -0
- package/lib/typescript/src/core/input/types.d.ts +64 -0
- package/lib/typescript/src/core/input/types.d.ts.map +1 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts +18 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts +27 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts +36 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/core/session/types.d.ts +104 -0
- package/lib/typescript/src/core/session/types.d.ts.map +1 -0
- package/lib/typescript/src/definition/defineGame.d.ts +66 -0
- package/lib/typescript/src/definition/defineGame.d.ts.map +1 -0
- package/lib/typescript/src/definition/types.d.ts +56 -0
- package/lib/typescript/src/definition/types.d.ts.map +1 -0
- package/lib/typescript/src/geometry/types.d.ts +8 -0
- package/lib/typescript/src/geometry/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts +37 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -0
- package/lib/typescript/src/react/GameView.d.ts +61 -0
- package/lib/typescript/src/react/GameView.d.ts.map +1 -0
- package/lib/typescript/src/react/alphaClock.d.ts +29 -0
- package/lib/typescript/src/react/alphaClock.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts +33 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts +12 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/errors.d.ts +8 -0
- package/lib/typescript/src/react/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts +35 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts +32 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/bindGameSession.d.ts +14 -0
- package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts +37 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/instrumentation.d.ts +47 -0
- package/lib/typescript/src/react/instrumentation.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerBinding.d.ts +104 -0
- package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts +106 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerContainment.d.ts +12 -0
- package/lib/typescript/src/react/pointerContainment.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts +75 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +29 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts +43 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +41 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts +10 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts +86 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -0
- package/lib/typescript/src/react/viewportBinding.d.ts +42 -0
- package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -0
- package/lib/typescript/src/react.d.ts +20 -0
- package/lib/typescript/src/react.d.ts.map +1 -0
- package/lib/typescript/src/scene/defineScene.d.ts +26 -0
- package/lib/typescript/src/scene/defineScene.d.ts.map +1 -0
- package/lib/typescript/src/scene/types.d.ts +71 -0
- package/lib/typescript/src/scene/types.d.ts.map +1 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +24 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts +45 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -0
- package/lib/typescript/src/testing.d.ts +28 -0
- package/lib/typescript/src/testing.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/index.d.ts +3 -0
- package/lib/typescript/src/viewport2d/index.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/math.d.ts +25 -0
- package/lib/typescript/src/viewport2d/math.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/types.d.ts +59 -0
- package/lib/typescript/src/viewport2d/types.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/assets/defineAssets.ts +62 -0
- package/src/assets/errors.ts +64 -0
- package/src/assets/types.ts +164 -0
- package/src/assets/validation.ts +158 -0
- package/src/core/frameDriver.ts +31 -0
- package/src/core/input/createInputBuffer.ts +337 -0
- package/src/core/input/types.ts +67 -0
- package/src/core/session/createGameSession.ts +652 -0
- package/src/core/session/deepFreeze.ts +158 -0
- package/src/core/session/diagnostics.ts +35 -0
- package/src/core/session/types.ts +122 -0
- package/src/definition/defineGame.ts +94 -0
- package/src/definition/types.ts +67 -0
- package/src/geometry/types.ts +7 -0
- package/src/index.ts +84 -0
- package/src/react/GamePointerInput.tsx +478 -0
- package/src/react/GameView.tsx +317 -0
- package/src/react/alphaClock.ts +59 -0
- package/src/react/assets/createGameAssetStore.ts +459 -0
- package/src/react/assets/decodeSkiaImage.ts +64 -0
- package/src/react/assets/errors.ts +7 -0
- package/src/react/assets/useGameAssets.ts +171 -0
- package/src/react/bindAppLifecycle.ts +79 -0
- package/src/react/bindGameSession.ts +41 -0
- package/src/react/gestureLifecycle.ts +73 -0
- package/src/react/instrumentation.ts +54 -0
- package/src/react/pointerBinding.ts +226 -0
- package/src/react/pointerCoalescer.ts +234 -0
- package/src/react/pointerContainment.ts +23 -0
- package/src/react/sprites/GameSprite.tsx +192 -0
- package/src/react/sprites/GameWorld2D.tsx +46 -0
- package/src/react/sprites/Sprite.tsx +194 -0
- package/src/react/sprites/SpriteBatch.tsx +219 -0
- package/src/react/sprites/spriteBatchPolicy.ts +21 -0
- package/src/react/sprites/spriteTransform.ts +163 -0
- package/src/react/viewportBinding.ts +104 -0
- package/src/react.ts +37 -0
- package/src/scene/defineScene.ts +38 -0
- package/src/scene/types.ts +79 -0
- package/src/sprites/sampleSpriteClip.ts +51 -0
- package/src/sprites/spriteAnimationState.ts +176 -0
- package/src/testing.ts +61 -0
- package/src/viewport2d/index.ts +7 -0
- package/src/viewport2d/math.ts +134 -0
- package/src/viewport2d/types.ts +63 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic clip frame sampling (T7.3).
|
|
3
|
+
*
|
|
4
|
+
* Pure, allocation-free, worklet-compatible frame selection. The same clip
|
|
5
|
+
* metadata and elapsed time always select the same frame, so equivalent
|
|
6
|
+
* game time produces identical presentation at 30/60/90/120 Hz.
|
|
7
|
+
*/
|
|
8
|
+
import type { SpriteClip } from '../assets/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select the frame index for `elapsedMs` of playback.
|
|
12
|
+
*
|
|
13
|
+
* Loop clips wrap with modulo — the boundary frame is never duplicated and
|
|
14
|
+
* no zero-length boundary frame exists. One-shot clips clamp to their final
|
|
15
|
+
* frame; completion is reported separately.
|
|
16
|
+
*/
|
|
17
|
+
export function sampleSpriteClipFrame(
|
|
18
|
+
clip: SpriteClip,
|
|
19
|
+
elapsedMs: number,
|
|
20
|
+
): number {
|
|
21
|
+
'worklet';
|
|
22
|
+
const frameCount = clip.frames.length;
|
|
23
|
+
const duration = clip.frameDurationMs;
|
|
24
|
+
if (clip.mode === 'loop') {
|
|
25
|
+
const index = Math.floor(elapsedMs / duration) % frameCount;
|
|
26
|
+
return index >= 0 ? index : index + frameCount;
|
|
27
|
+
}
|
|
28
|
+
const index = Math.floor(elapsedMs / duration);
|
|
29
|
+
return index >= frameCount ? frameCount - 1 : index;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The frame NAME selected for `elapsedMs` of playback (the ordered frame
|
|
34
|
+
* references of the clip).
|
|
35
|
+
*/
|
|
36
|
+
export function sampleSpriteClipFrameName(
|
|
37
|
+
clip: SpriteClip,
|
|
38
|
+
elapsedMs: number,
|
|
39
|
+
): string {
|
|
40
|
+
'worklet';
|
|
41
|
+
const frames = clip.frames;
|
|
42
|
+
const index = sampleSpriteClipFrame(clip, elapsedMs);
|
|
43
|
+
const frame = frames[index];
|
|
44
|
+
return frame === undefined ? frames[0] ?? '' : frame;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Total clip duration in milliseconds (one-shot: its full timeline). */
|
|
48
|
+
export function spriteClipDurationMs(clip: SpriteClip): number {
|
|
49
|
+
'worklet';
|
|
50
|
+
return clip.frames.length * clip.frameDurationMs;
|
|
51
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic sprite animation playback state (T7.3).
|
|
3
|
+
*
|
|
4
|
+
* Animation state is plain serializable data advanced from fixed-step game
|
|
5
|
+
* time: `clip`, elapsed time, pause flag, speed, and completion. Changing,
|
|
6
|
+
* pausing, restarting, or completing a clip returns a NEW state object; it
|
|
7
|
+
* never mutates scene state or emits a side effect. The same input and
|
|
8
|
+
* elapsed game time always select the same presented frame (see
|
|
9
|
+
* `sampleSpriteClip`).
|
|
10
|
+
*
|
|
11
|
+
* Separation of concerns: gameplay-significant animation state lives in the
|
|
12
|
+
* scene snapshot and advances from fixed-step game time; a renderer-only
|
|
13
|
+
* loop may interpolate presentation from the latest committed state plus
|
|
14
|
+
* the interpolation alpha, but must never emit gameplay events from a
|
|
15
|
+
* presentation-only frame.
|
|
16
|
+
*/
|
|
17
|
+
import { GameAssetError } from '../assets/errors';
|
|
18
|
+
import type { SpriteSheetDescriptor } from '../assets/types';
|
|
19
|
+
import { spriteClipDurationMs } from './sampleSpriteClip';
|
|
20
|
+
|
|
21
|
+
/** Immutable, serializable animation playback state. */
|
|
22
|
+
export interface SpriteAnimationState<TClipName extends string = string> {
|
|
23
|
+
/** The clip currently selected on the descriptor. */
|
|
24
|
+
readonly clip: TClipName;
|
|
25
|
+
/** Elapsed milliseconds within the current playback of the clip. */
|
|
26
|
+
readonly elapsedMs: number;
|
|
27
|
+
/** Playback paused: advance is a no-op while paused. */
|
|
28
|
+
readonly paused: boolean;
|
|
29
|
+
/** Finite, positive playback speed multiplier. */
|
|
30
|
+
readonly speed: number;
|
|
31
|
+
/** One-shot clips report completion after their final frame. */
|
|
32
|
+
readonly completed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertFinitePositiveSpeed(speed: number): void {
|
|
36
|
+
if (typeof speed !== 'number' || !Number.isFinite(speed) || speed <= 0) {
|
|
37
|
+
throw new Error('sprite animation speed must be a finite number greater than zero');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertFiniteDelta(deltaSeconds: number): void {
|
|
42
|
+
if (typeof deltaSeconds !== 'number' || !Number.isFinite(deltaSeconds)) {
|
|
43
|
+
throw new Error('sprite animation delta must be a finite number of seconds');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The clip names of a sprite-sheet descriptor (works through the manifest brand). */
|
|
47
|
+
export type SpriteClipNames<TDescriptor extends SpriteSheetDescriptor> = Extract<
|
|
48
|
+
keyof TDescriptor['animations'],
|
|
49
|
+
string
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
/** Runtime clip lookup; throws a structured error for unknown clips. */
|
|
53
|
+
function clipOf(
|
|
54
|
+
descriptor: SpriteSheetDescriptor,
|
|
55
|
+
clip: string,
|
|
56
|
+
): SpriteSheetDescriptor['animations'][string] {
|
|
57
|
+
'worklet';
|
|
58
|
+
const animation = descriptor.animations[clip];
|
|
59
|
+
if (animation === undefined) {
|
|
60
|
+
throw new GameAssetError(
|
|
61
|
+
'ASSET_UNKNOWN_CLIP',
|
|
62
|
+
['animations', clip],
|
|
63
|
+
`unknown animation clip ${JSON.stringify(clip)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return animation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start playback of `clip` from its beginning. The clip name is preserved
|
|
71
|
+
* as a string literal in the returned state; unknown clips are rejected at
|
|
72
|
+
* runtime (the manifest descriptor keeps the sheet-level frame and clip
|
|
73
|
+
* names literal via the spriteSheet contract).
|
|
74
|
+
*/
|
|
75
|
+
export function startSpriteAnimation<TClipName extends string>(
|
|
76
|
+
descriptor: SpriteSheetDescriptor,
|
|
77
|
+
clip: TClipName,
|
|
78
|
+
): SpriteAnimationState<TClipName> {
|
|
79
|
+
'worklet';
|
|
80
|
+
clipOf(descriptor, clip);
|
|
81
|
+
return {
|
|
82
|
+
clip,
|
|
83
|
+
elapsedMs: 0,
|
|
84
|
+
paused: false,
|
|
85
|
+
speed: 1,
|
|
86
|
+
completed: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Advance playback by `deltaSeconds` of fixed-step game time.
|
|
92
|
+
*
|
|
93
|
+
* Finite negative deltas clamp elapsed time to zero; NaN/infinity fail
|
|
94
|
+
* clearly. Loop clips keep their elapsed time bounded within one timeline
|
|
95
|
+
* (modulo), so arbitrarily large deltas cost constant arithmetic.
|
|
96
|
+
*/
|
|
97
|
+
export function advanceSpriteAnimation<TClipName extends string>(
|
|
98
|
+
descriptor: SpriteSheetDescriptor,
|
|
99
|
+
state: SpriteAnimationState<TClipName>,
|
|
100
|
+
deltaSeconds: number,
|
|
101
|
+
): SpriteAnimationState<TClipName> {
|
|
102
|
+
'worklet';
|
|
103
|
+
assertFiniteDelta(deltaSeconds);
|
|
104
|
+
if (state.paused || state.completed) {
|
|
105
|
+
return state;
|
|
106
|
+
}
|
|
107
|
+
const clip = clipOf(descriptor, state.clip);
|
|
108
|
+
const totalMs = spriteClipDurationMs(clip);
|
|
109
|
+
const next = state.elapsedMs + deltaSeconds * 1000 * state.speed;
|
|
110
|
+
if (clip.mode === 'once') {
|
|
111
|
+
if (next >= totalMs) {
|
|
112
|
+
return { ...state, elapsedMs: totalMs, completed: true };
|
|
113
|
+
}
|
|
114
|
+
return { ...state, elapsedMs: next < 0 ? 0 : next };
|
|
115
|
+
}
|
|
116
|
+
// Loop: bounded elapsed within one timeline; negative clamps to zero.
|
|
117
|
+
const wrapped = next < 0 ? 0 : next % totalMs;
|
|
118
|
+
return { ...state, elapsedMs: wrapped };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Switch playback to `clip`, restarting it from the beginning. The clip
|
|
123
|
+
* name is typed against the descriptor's animation table.
|
|
124
|
+
*/
|
|
125
|
+
export function playSpriteAnimation<TClipName extends string>(
|
|
126
|
+
descriptor: SpriteSheetDescriptor,
|
|
127
|
+
state: SpriteAnimationState<TClipName>,
|
|
128
|
+
clip: TClipName,
|
|
129
|
+
): SpriteAnimationState<TClipName> {
|
|
130
|
+
'worklet';
|
|
131
|
+
if (clip === state.clip) {
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
134
|
+
clipOf(descriptor, clip);
|
|
135
|
+
return {
|
|
136
|
+
clip,
|
|
137
|
+
elapsedMs: 0,
|
|
138
|
+
paused: state.paused,
|
|
139
|
+
speed: state.speed,
|
|
140
|
+
completed: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Pause playback; `advance` becomes a no-op while paused. */
|
|
145
|
+
export function pauseSpriteAnimation<TClipName extends string>(
|
|
146
|
+
state: SpriteAnimationState<TClipName>,
|
|
147
|
+
): SpriteAnimationState<TClipName> {
|
|
148
|
+
'worklet';
|
|
149
|
+
return state.paused ? state : { ...state, paused: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Resume playback from the paused position. */
|
|
153
|
+
export function resumeSpriteAnimation<TClipName extends string>(
|
|
154
|
+
state: SpriteAnimationState<TClipName>,
|
|
155
|
+
): SpriteAnimationState<TClipName> {
|
|
156
|
+
'worklet';
|
|
157
|
+
return state.paused ? { ...state, paused: false } : state;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Restart the current clip from its beginning. */
|
|
161
|
+
export function resetSpriteAnimation<TClipName extends string>(
|
|
162
|
+
state: SpriteAnimationState<TClipName>,
|
|
163
|
+
): SpriteAnimationState<TClipName> {
|
|
164
|
+
'worklet';
|
|
165
|
+
return { ...state, elapsedMs: 0, completed: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Set a finite positive speed multiplier. */
|
|
169
|
+
export function setSpriteAnimationSpeed<TClipName extends string>(
|
|
170
|
+
state: SpriteAnimationState<TClipName>,
|
|
171
|
+
speed: number,
|
|
172
|
+
): SpriteAnimationState<TClipName> {
|
|
173
|
+
'worklet';
|
|
174
|
+
assertFinitePositiveSpeed(speed);
|
|
175
|
+
return state.speed === speed ? state : { ...state, speed };
|
|
176
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createGameSessionWithDriver } from './core/session/createGameSession';
|
|
2
|
+
import { createAnimationFrameDriver } from './core/frameDriver';
|
|
3
|
+
import type { FrameDriver, FrameHandle } from './core/frameDriver';
|
|
4
|
+
import type { SessionDiagnostics } from './core/session/diagnostics';
|
|
5
|
+
|
|
6
|
+
export { createAnimationFrameDriver, createGameSessionWithDriver };
|
|
7
|
+
export type { FrameDriver, FrameHandle, SessionDiagnostics };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic animation-frame driver for headless tests.
|
|
11
|
+
*
|
|
12
|
+
* Frames are fired manually with explicit timestamps, so games can be driven
|
|
13
|
+
* with a fixed step and exact wall-clock schedules in Node without a
|
|
14
|
+
* simulator.
|
|
15
|
+
*/
|
|
16
|
+
export class ManualFrameDriver implements FrameDriver {
|
|
17
|
+
readonly #callbacks = new Map<number, (timestampMs: number) => void>();
|
|
18
|
+
readonly #allCallbacks = new Map<number, (timestampMs: number) => void>();
|
|
19
|
+
#nextHandle = 1;
|
|
20
|
+
|
|
21
|
+
/** Number of currently scheduled callbacks. */
|
|
22
|
+
get pendingCount(): number {
|
|
23
|
+
return this.#callbacks.size;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
requestFrame(callback: (timestampMs: number) => void): FrameHandle {
|
|
27
|
+
const handle = this.#nextHandle;
|
|
28
|
+
this.#nextHandle += 1;
|
|
29
|
+
this.#callbacks.set(handle, callback);
|
|
30
|
+
this.#allCallbacks.set(handle, callback);
|
|
31
|
+
return handle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cancelFrame(handle: FrameHandle): void {
|
|
35
|
+
this.#callbacks.delete(handle);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fire the next pending callback with the given timestamp. */
|
|
39
|
+
fireNext(timestampMs: number): FrameHandle {
|
|
40
|
+
const next = this.#callbacks.entries().next();
|
|
41
|
+
if (next.done) {
|
|
42
|
+
throw new Error('No frame is pending');
|
|
43
|
+
}
|
|
44
|
+
const [handle, callback] = next.value;
|
|
45
|
+
this.#callbacks.delete(handle);
|
|
46
|
+
callback(timestampMs);
|
|
47
|
+
return handle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fire a specific handle even if it was already fired, simulating a stale
|
|
52
|
+
* callback racing with a lifecycle change.
|
|
53
|
+
*/
|
|
54
|
+
fireCancelled(handle: FrameHandle, timestampMs: number): void {
|
|
55
|
+
const callback = this.#allCallbacks.get(handle);
|
|
56
|
+
if (!callback) {
|
|
57
|
+
throw new Error(`Unknown frame handle: ${handle}`);
|
|
58
|
+
}
|
|
59
|
+
callback(timestampMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Point2D } from '../geometry/types';
|
|
2
|
+
import type { Rect, ResolvedViewport2D, SurfaceSize, Viewport } from './types';
|
|
3
|
+
|
|
4
|
+
function freezeRect(value: Rect): Rect {
|
|
5
|
+
return Object.freeze(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function rect(x: number, y: number, width: number, height: number): Rect {
|
|
9
|
+
return freezeRect({ x, y, width, height });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function invalidLogicalSize(config: Viewport): string {
|
|
13
|
+
const { width, height } = config.logicalSize;
|
|
14
|
+
return `Viewport logical size must be finite and positive (got ${width} x ${height})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isUsableSurface(size: SurfaceSize): boolean {
|
|
18
|
+
return (
|
|
19
|
+
Number.isFinite(size.width) &&
|
|
20
|
+
Number.isFinite(size.height) &&
|
|
21
|
+
size.width > 0 &&
|
|
22
|
+
size.height > 0
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve an authored viewport configuration against an actual surface size.
|
|
28
|
+
*
|
|
29
|
+
* Returns `undefined` until both surface dimensions are positive — a
|
|
30
|
+
* zero-sized layout is normal before the first layout pass. Invalid authored
|
|
31
|
+
* sizes throw a `RangeError` naming the invalid dimension. The result is
|
|
32
|
+
* immutable plain data with no platform dependencies.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveViewport2D(
|
|
35
|
+
config: Viewport,
|
|
36
|
+
surfaceSize: SurfaceSize,
|
|
37
|
+
): ResolvedViewport2D | undefined {
|
|
38
|
+
const { width: logicalWidth, height: logicalHeight } = config.logicalSize;
|
|
39
|
+
|
|
40
|
+
if (!Number.isFinite(logicalWidth) || !Number.isFinite(logicalHeight)) {
|
|
41
|
+
throw new RangeError(invalidLogicalSize(config));
|
|
42
|
+
}
|
|
43
|
+
if (!(logicalWidth > 0) || !(logicalHeight > 0)) {
|
|
44
|
+
throw new RangeError(invalidLogicalSize(config));
|
|
45
|
+
}
|
|
46
|
+
if (!isUsableSurface(surfaceSize)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (config.mode !== 'fit' && config.mode !== 'fill' && config.mode !== 'extend-world') {
|
|
51
|
+
throw new RangeError(`Unknown viewport mode: ${String(config.mode)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { width: surfaceWidth, height: surfaceHeight } = surfaceSize;
|
|
55
|
+
const widthScale = surfaceWidth / logicalWidth;
|
|
56
|
+
const heightScale = surfaceHeight / logicalHeight;
|
|
57
|
+
|
|
58
|
+
let scale: number;
|
|
59
|
+
let offsetX: number;
|
|
60
|
+
let offsetY: number;
|
|
61
|
+
let visibleLogicalBounds: Rect;
|
|
62
|
+
let contentBounds: Rect;
|
|
63
|
+
|
|
64
|
+
if (config.mode === 'fill') {
|
|
65
|
+
scale = Math.max(widthScale, heightScale);
|
|
66
|
+
offsetX = (surfaceWidth - logicalWidth * scale) / 2;
|
|
67
|
+
offsetY = (surfaceHeight - logicalHeight * scale) / 2;
|
|
68
|
+
visibleLogicalBounds = rect(
|
|
69
|
+
-offsetX / scale,
|
|
70
|
+
-offsetY / scale,
|
|
71
|
+
surfaceWidth / scale,
|
|
72
|
+
surfaceHeight / scale,
|
|
73
|
+
);
|
|
74
|
+
contentBounds = rect(0, 0, surfaceWidth, surfaceHeight);
|
|
75
|
+
} else {
|
|
76
|
+
// `fit` and `extend-world` share the fit (minimum uniform) scale.
|
|
77
|
+
scale = Math.min(widthScale, heightScale);
|
|
78
|
+
offsetX = (surfaceWidth - logicalWidth * scale) / 2;
|
|
79
|
+
offsetY = (surfaceHeight - logicalHeight * scale) / 2;
|
|
80
|
+
if (config.mode === 'extend-world') {
|
|
81
|
+
const worldWidth = surfaceWidth / scale;
|
|
82
|
+
const worldHeight = surfaceHeight / scale;
|
|
83
|
+
visibleLogicalBounds = rect(
|
|
84
|
+
(logicalWidth - worldWidth) / 2,
|
|
85
|
+
(logicalHeight - worldHeight) / 2,
|
|
86
|
+
worldWidth,
|
|
87
|
+
worldHeight,
|
|
88
|
+
);
|
|
89
|
+
contentBounds = rect(0, 0, surfaceWidth, surfaceHeight);
|
|
90
|
+
} else {
|
|
91
|
+
visibleLogicalBounds = rect(0, 0, logicalWidth, logicalHeight);
|
|
92
|
+
contentBounds = rect(offsetX, offsetY, logicalWidth * scale, logicalHeight * scale);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
surfaceSize: Object.freeze({ width: surfaceWidth, height: surfaceHeight }),
|
|
98
|
+
logicalBounds: rect(0, 0, logicalWidth, logicalHeight),
|
|
99
|
+
visibleLogicalBounds,
|
|
100
|
+
contentBounds,
|
|
101
|
+
scale,
|
|
102
|
+
offsetX,
|
|
103
|
+
offsetY,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Convert a logical/world point into surface coordinates. */
|
|
108
|
+
export function worldToSurface(viewport: ResolvedViewport2D, point: Point2D): Point2D {
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
x: point.x * viewport.scale + viewport.offsetX,
|
|
111
|
+
y: point.y * viewport.scale + viewport.offsetY,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Convert a surface point into logical/world coordinates. */
|
|
116
|
+
export function surfaceToWorld(viewport: ResolvedViewport2D, point: Point2D): Point2D {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
x: (point.x - viewport.offsetX) / viewport.scale,
|
|
119
|
+
y: (point.y - viewport.offsetY) / viewport.scale,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Decide whether a surface point lies inside the interactive content area.
|
|
125
|
+
*
|
|
126
|
+
* For `fit` this rejects letterbox space so a pointer cannot begin there. For
|
|
127
|
+
* `fill` and `extend-world` the entire surface is interactive. Conversion
|
|
128
|
+
* itself remains mathematical everywhere; this operation decides whether a
|
|
129
|
+
* gesture may begin.
|
|
130
|
+
*/
|
|
131
|
+
export function containsSurfacePoint(viewport: ResolvedViewport2D, point: Point2D): boolean {
|
|
132
|
+
const { x, y, width, height } = viewport.contentBounds;
|
|
133
|
+
return point.x >= x && point.y >= y && point.x <= x + width && point.y <= y + height;
|
|
134
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The logical size of the game viewport in design points. */
|
|
2
|
+
export interface LogicalSize {
|
|
3
|
+
/** Logical width in points. */
|
|
4
|
+
readonly width: number;
|
|
5
|
+
/** Logical height in points. */
|
|
6
|
+
readonly height: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** How the authored logical viewport maps onto the mounted surface. */
|
|
10
|
+
export type ViewportMode = 'fit' | 'fill' | 'extend-world';
|
|
11
|
+
|
|
12
|
+
/** The authored viewport configuration of a game. */
|
|
13
|
+
export interface Viewport {
|
|
14
|
+
/** The logical coordinate space the game is authored against. */
|
|
15
|
+
readonly logicalSize: LogicalSize;
|
|
16
|
+
/** How the authored viewport scales onto the mounted game surface. */
|
|
17
|
+
readonly mode: ViewportMode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The actual mounted size of a game surface in React Native layout points. */
|
|
21
|
+
export interface SurfaceSize {
|
|
22
|
+
/** Surface width in points. */
|
|
23
|
+
readonly width: number;
|
|
24
|
+
/** Surface height in points. */
|
|
25
|
+
readonly height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** An axis-aligned rectangle in a coordinate space. */
|
|
29
|
+
export interface Rect {
|
|
30
|
+
/** Left edge. */
|
|
31
|
+
readonly x: number;
|
|
32
|
+
/** Top edge. */
|
|
33
|
+
readonly y: number;
|
|
34
|
+
/** Width. */
|
|
35
|
+
readonly width: number;
|
|
36
|
+
/** Height. */
|
|
37
|
+
readonly height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A resolved, immutable viewport mapping between logical/world coordinates
|
|
42
|
+
* and surface coordinates.
|
|
43
|
+
*
|
|
44
|
+
* The transformation is linear: `surface = world * scale + offset`. All
|
|
45
|
+
* fields are plain data and are safe to share between drawing and hit
|
|
46
|
+
* testing.
|
|
47
|
+
*/
|
|
48
|
+
export interface ResolvedViewport2D {
|
|
49
|
+
/** The surface size this viewport was resolved against. */
|
|
50
|
+
readonly surfaceSize: SurfaceSize;
|
|
51
|
+
/** The authored logical bounds: `{ x: 0, y: 0, width, height }`. */
|
|
52
|
+
readonly logicalBounds: Rect;
|
|
53
|
+
/** The portion of the logical/world space visible on the surface. */
|
|
54
|
+
readonly visibleLogicalBounds: Rect;
|
|
55
|
+
/** The surface region covered by visible world content. */
|
|
56
|
+
readonly contentBounds: Rect;
|
|
57
|
+
/** The one uniform scale from world units to surface points. */
|
|
58
|
+
readonly scale: number;
|
|
59
|
+
/** Horizontal translation offset in surface points. */
|
|
60
|
+
readonly offsetX: number;
|
|
61
|
+
/** Vertical translation offset in surface points. */
|
|
62
|
+
readonly offsetY: number;
|
|
63
|
+
}
|