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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset definition helpers (T7.2).
|
|
3
|
+
*
|
|
4
|
+
* `image(...)` and `spriteSheet(...)` build immutable descriptors;
|
|
5
|
+
* `defineAssets(...)` groups them into a deeply immutable typed manifest.
|
|
6
|
+
* Definition performs no I/O and allocates no native handle.
|
|
7
|
+
*/
|
|
8
|
+
import { createDeepFreeze } from '../core/session/deepFreeze';
|
|
9
|
+
import { validateImageSource, validateManifest, validateSpriteSheet } from './validation';
|
|
10
|
+
import type {
|
|
11
|
+
AssetGroupMap,
|
|
12
|
+
GameAssetManifest,
|
|
13
|
+
ImageDescriptor,
|
|
14
|
+
SpriteClip,
|
|
15
|
+
SpriteFrameRect,
|
|
16
|
+
SpriteSheetDescriptor,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
/** Declare a single full-image asset from a static module handle. */
|
|
20
|
+
export function image(source: number): ImageDescriptor {
|
|
21
|
+
validateImageSource(source);
|
|
22
|
+
return Object.freeze({ kind: 'image', source });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Declare a sprite-sheet asset: named source frames plus named animation
|
|
27
|
+
* clips. Frame and clip names are preserved as string literals; clip frame
|
|
28
|
+
* references are restricted to the declared frames.
|
|
29
|
+
*/
|
|
30
|
+
export function spriteSheet<
|
|
31
|
+
TFrames extends Record<string, SpriteFrameRect>,
|
|
32
|
+
TClips extends Record<string, SpriteClip<Extract<keyof TFrames, string>>>,
|
|
33
|
+
>(
|
|
34
|
+
source: number,
|
|
35
|
+
spec: {
|
|
36
|
+
readonly frames: TFrames;
|
|
37
|
+
readonly animations: TClips;
|
|
38
|
+
},
|
|
39
|
+
): SpriteSheetDescriptor<TFrames, TClips> {
|
|
40
|
+
validateSpriteSheet(source, spec);
|
|
41
|
+
const freezer = createDeepFreeze();
|
|
42
|
+
return freezer({
|
|
43
|
+
kind: 'sprite-sheet',
|
|
44
|
+
source,
|
|
45
|
+
frames: spec.frames,
|
|
46
|
+
animations: spec.animations,
|
|
47
|
+
}) as SpriteSheetDescriptor<TFrames, TClips>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Declare the game's asset manifest: a deeply immutable, fully typed group
|
|
52
|
+
* map. Group, asset, frame, and clip names are preserved as string literals
|
|
53
|
+
* and every descriptor is branded with the manifest type so lookups stay
|
|
54
|
+
* typed. Allocates no native resources and performs no I/O.
|
|
55
|
+
*/
|
|
56
|
+
export function defineAssets<TGroups extends AssetGroupMap>(
|
|
57
|
+
groups: TGroups,
|
|
58
|
+
): GameAssetManifest<TGroups> {
|
|
59
|
+
validateManifest(groups);
|
|
60
|
+
const freezer = createDeepFreeze();
|
|
61
|
+
return freezer(groups) as GameAssetManifest<TGroups>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured asset errors (T7.2).
|
|
3
|
+
*
|
|
4
|
+
* Every definition/loading failure carries a stable machine-readable code
|
|
5
|
+
* and the field path that caused it. Paths are documented in the public
|
|
6
|
+
* error reference; messages never embed machine-specific paths.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Stable error codes for asset definition and loading. */
|
|
10
|
+
export type GameAssetErrorCode =
|
|
11
|
+
| 'ASSET_INVALID_IDENTIFIER'
|
|
12
|
+
| 'ASSET_INVALID_SOURCE'
|
|
13
|
+
| 'ASSET_INVALID_FRAME_RECT'
|
|
14
|
+
| 'ASSET_EMPTY_CLIP'
|
|
15
|
+
| 'ASSET_UNKNOWN_FRAME'
|
|
16
|
+
| 'ASSET_UNKNOWN_CLIP'
|
|
17
|
+
| 'ASSET_INVALID_DURATION'
|
|
18
|
+
| 'ASSET_INVALID_MODE'
|
|
19
|
+
| 'ASSET_UNSUPPORTED_SOURCE'
|
|
20
|
+
| 'ASSET_UNKNOWN_GROUP'
|
|
21
|
+
| 'ASSET_UNKNOWN_ASSET'
|
|
22
|
+
| 'ASSET_RESOLVE_FAILED'
|
|
23
|
+
| 'ASSET_DECODE_FAILED'
|
|
24
|
+
| 'ASSET_FRAME_OUT_OF_BOUNDS'
|
|
25
|
+
| 'ASSET_ABORTED'
|
|
26
|
+
| 'ASSET_STORE_DISPOSED';
|
|
27
|
+
|
|
28
|
+
/** A structured asset definition or loading failure. */
|
|
29
|
+
export class GameAssetError extends Error {
|
|
30
|
+
/** Stable machine-readable code. */
|
|
31
|
+
readonly code: GameAssetErrorCode;
|
|
32
|
+
/** Field path within the manifest/descriptor that caused the failure. */
|
|
33
|
+
readonly path: readonly string[];
|
|
34
|
+
|
|
35
|
+
constructor(code: GameAssetErrorCode, path: readonly string[], message: string) {
|
|
36
|
+
super(`${code}${path.length > 0 ? ` at ${path.join('.')}` : ''}: ${message}`);
|
|
37
|
+
this.name = 'GameAssetError';
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.path = path;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Throw when a definition-time identifier is invalid. */
|
|
44
|
+
export function assertValidIdentifier(
|
|
45
|
+
path: readonly string[],
|
|
46
|
+
value: string,
|
|
47
|
+
): void {
|
|
48
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
49
|
+
throw new GameAssetError(
|
|
50
|
+
'ASSET_INVALID_IDENTIFIER',
|
|
51
|
+
path,
|
|
52
|
+
'identifiers must be non-empty strings',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
// '/' is the diagnostic-id separator (group/key); reserving it keeps
|
|
56
|
+
// diagnostic ids unambiguous.
|
|
57
|
+
if (value.includes('/')) {
|
|
58
|
+
throw new GameAssetError(
|
|
59
|
+
'ASSET_INVALID_IDENTIFIER',
|
|
60
|
+
path,
|
|
61
|
+
`identifier ${JSON.stringify(value)} contains the reserved separator '/'`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset descriptor and manifest types (T7.2).
|
|
3
|
+
*
|
|
4
|
+
* The manifest is the static, deeply immutable declaration of a game's local
|
|
5
|
+
* assets. Group, asset, frame, and clip names are preserved as string
|
|
6
|
+
* literals; retrieval goes through typed descriptor references, never
|
|
7
|
+
* duplicated strings. Sources are static module handles (`number` from
|
|
8
|
+
* `require(...)`) only — string/URL sources are rejected at the type
|
|
9
|
+
* boundary and deliberately have no runtime branch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Static React Native module handle returned by `require(...)`. */
|
|
13
|
+
export type AssetSourceHandle = number;
|
|
14
|
+
|
|
15
|
+
/** A single full-image asset. */
|
|
16
|
+
export interface ImageDescriptor {
|
|
17
|
+
/** Discriminant. */
|
|
18
|
+
readonly kind: 'image';
|
|
19
|
+
/** Static module handle. */
|
|
20
|
+
readonly source: AssetSourceHandle;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A named rectangle inside a sprite sheet, in source pixels. */
|
|
24
|
+
export interface SpriteFrameRect {
|
|
25
|
+
/** Left edge in source pixels; non-negative finite number. */
|
|
26
|
+
readonly x: number;
|
|
27
|
+
/** Top edge in source pixels; non-negative finite number. */
|
|
28
|
+
readonly y: number;
|
|
29
|
+
/** Width in source pixels; greater than zero. */
|
|
30
|
+
readonly width: number;
|
|
31
|
+
/** Height in source pixels; greater than zero. */
|
|
32
|
+
readonly height: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Clip playback mode: looping or one-shot. */
|
|
36
|
+
export type SpriteAnimationMode = 'loop' | 'once';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A named animation clip over a sprite sheet's frames.
|
|
40
|
+
*
|
|
41
|
+
* Frame references are restricted to the sheet's declared frame names and
|
|
42
|
+
* durations are uniform per clip in this version (per-frame durations are
|
|
43
|
+
* deferred so precedence cannot be misunderstood).
|
|
44
|
+
*/
|
|
45
|
+
export interface SpriteClip<TFrameName extends string = string> {
|
|
46
|
+
/** Ordered frame references; at least one, all declared on the sheet. */
|
|
47
|
+
readonly frames: readonly TFrameName[];
|
|
48
|
+
/** Uniform per-frame duration in milliseconds; finite and greater than zero. */
|
|
49
|
+
readonly frameDurationMs: number;
|
|
50
|
+
/** Discriminated playback mode. */
|
|
51
|
+
readonly mode: SpriteAnimationMode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A sprite-sheet asset: named frames plus named animation clips. */
|
|
55
|
+
export interface SpriteSheetDescriptor<
|
|
56
|
+
TFrames extends Record<string, SpriteFrameRect> = Record<string, SpriteFrameRect>,
|
|
57
|
+
TClips extends Record<string, SpriteClip<Extract<keyof TFrames, string>>> = Record<
|
|
58
|
+
string,
|
|
59
|
+
SpriteClip<Extract<keyof TFrames, string>>
|
|
60
|
+
>,
|
|
61
|
+
> {
|
|
62
|
+
/** Discriminant. */
|
|
63
|
+
readonly kind: 'sprite-sheet';
|
|
64
|
+
/** Static module handle. */
|
|
65
|
+
readonly source: AssetSourceHandle;
|
|
66
|
+
/** Named source rectangles. */
|
|
67
|
+
readonly frames: TFrames;
|
|
68
|
+
/** Named animation clips restricted to the declared frames. */
|
|
69
|
+
readonly animations: TClips;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Union of all asset descriptors. */
|
|
73
|
+
export type AssetDescriptor = ImageDescriptor | SpriteSheetDescriptor;
|
|
74
|
+
|
|
75
|
+
/** A group of named asset descriptors. */
|
|
76
|
+
export type AssetGroup = Readonly<Record<string, AssetDescriptor>>;
|
|
77
|
+
|
|
78
|
+
/** The author-facing group map accepted by `defineAssets`. */
|
|
79
|
+
export type AssetGroupMap = Readonly<Record<string, AssetGroup>>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Type-level manifest brand. Every descriptor of a manifest carries a
|
|
83
|
+
* phantom reference to its originating manifest type, so a loaded store can
|
|
84
|
+
* reject descriptors from any other manifest at compile time.
|
|
85
|
+
*/
|
|
86
|
+
declare const manifestBrand: unique symbol;
|
|
87
|
+
|
|
88
|
+
export type BrandedAssetDescriptor<
|
|
89
|
+
TManifest,
|
|
90
|
+
TDescriptor extends AssetDescriptor,
|
|
91
|
+
> = { readonly [manifestBrand]?: TManifest } & TDescriptor;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The deeply immutable manifest produced by `defineAssets`.
|
|
95
|
+
*
|
|
96
|
+
* Groups and assets keep their literal names; every descriptor is branded
|
|
97
|
+
* with the manifest type so lookups stay typed.
|
|
98
|
+
*/
|
|
99
|
+
export type GameAssetManifest<TGroups extends AssetGroupMap = AssetGroupMap> = {
|
|
100
|
+
readonly [TGroup in keyof TGroups]: {
|
|
101
|
+
readonly [TAsset in keyof TGroups[TGroup]]: BrandedAssetDescriptor<
|
|
102
|
+
GameAssetManifest<TGroups>,
|
|
103
|
+
TGroups[TGroup][TAsset]
|
|
104
|
+
>;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** The manifest type of a descriptor's originating manifest. */
|
|
109
|
+
export type ManifestOf<TDescriptor> =
|
|
110
|
+
TDescriptor extends { readonly [manifestBrand]?: infer TManifest } ? TManifest : never;
|
|
111
|
+
|
|
112
|
+
/** Loaded-asset lookup keyed by typed descriptor reference.
|
|
113
|
+
*
|
|
114
|
+
* v1 semantics (R9): lookup is descriptor-REFERENCE membership — the exact
|
|
115
|
+
* descriptor object declared by the manifest. The compile-time brand is
|
|
116
|
+
* structural, so identically shaped manifests are not nominally distinct;
|
|
117
|
+
* the runtime reference check is the guarantee, and the type layer still
|
|
118
|
+
* rejects duplicated string keys and descriptors from differently shaped
|
|
119
|
+
* manifests.
|
|
120
|
+
*/
|
|
121
|
+
export interface LoadedAssets<TManifest> {
|
|
122
|
+
/** The manifest this store was created from. */
|
|
123
|
+
readonly manifest: TManifest;
|
|
124
|
+
/** Retrieve a loaded asset by its typed descriptor reference. */
|
|
125
|
+
readonly get: <TDescriptor extends AssetDescriptor>(
|
|
126
|
+
descriptor: BrandedAssetDescriptor<TManifest, TDescriptor>,
|
|
127
|
+
) => TDescriptor extends { readonly kind: 'sprite-sheet' }
|
|
128
|
+
? LoadedSpriteSheet
|
|
129
|
+
: LoadedImage;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A ready full-image resource. */
|
|
133
|
+
export interface LoadedImage {
|
|
134
|
+
/** Logical descriptor this resource was loaded for. */
|
|
135
|
+
readonly descriptor: ImageDescriptor;
|
|
136
|
+
/** Decoded source width in pixels. */
|
|
137
|
+
readonly width: number;
|
|
138
|
+
/** Decoded source height in pixels. */
|
|
139
|
+
readonly height: number;
|
|
140
|
+
/** The decoded image handle (Skia's SkImage satisfies this structurally). */
|
|
141
|
+
readonly image: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A ready sprite-sheet resource with validated frame rectangles. */
|
|
145
|
+
export interface LoadedSpriteSheet {
|
|
146
|
+
/** Logical descriptor this resource was loaded for. */
|
|
147
|
+
readonly descriptor: SpriteSheetDescriptor;
|
|
148
|
+
/** Validated frames in source pixels (offsets against the decoded image). */
|
|
149
|
+
readonly frames: Readonly<Record<string, SpriteFrameRect>>;
|
|
150
|
+
/** Decoded source width in pixels. */
|
|
151
|
+
readonly width: number;
|
|
152
|
+
/** Decoded source height in pixels. */
|
|
153
|
+
readonly height: number;
|
|
154
|
+
/** The decoded image handle (Skia's SkImage satisfies this structurally). */
|
|
155
|
+
readonly image: unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A ready, typed, caller-owned lease over loaded asset groups. */
|
|
159
|
+
export interface GameAssetLease<TManifest> {
|
|
160
|
+
/** Typed lookup; rejects descriptors from other manifests. */
|
|
161
|
+
readonly assets: LoadedAssets<TManifest>;
|
|
162
|
+
/** Release this lease; idempotent; the final release disposes the image. */
|
|
163
|
+
readonly dispose: () => void;
|
|
164
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest validation (T7.2).
|
|
3
|
+
*
|
|
4
|
+
* Everything knowable before decoding is validated here with stable error
|
|
5
|
+
* codes and field paths. Rectangles are additionally validated against the
|
|
6
|
+
* decoded image dimensions after load (T7.4).
|
|
7
|
+
*/
|
|
8
|
+
import { GameAssetError, assertValidIdentifier } from './errors';
|
|
9
|
+
import type {
|
|
10
|
+
AssetGroupMap,
|
|
11
|
+
SpriteClip,
|
|
12
|
+
SpriteFrameRect,
|
|
13
|
+
SpriteSheetDescriptor,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
/** Validate one frame rectangle. */
|
|
17
|
+
export function validateFrameRect(
|
|
18
|
+
path: readonly string[],
|
|
19
|
+
name: string,
|
|
20
|
+
rect: SpriteFrameRect,
|
|
21
|
+
): void {
|
|
22
|
+
assertValidIdentifier(path, name);
|
|
23
|
+
const { x, y, width, height } = rect;
|
|
24
|
+
const values = [x, y, width, height] as const;
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
27
|
+
throw new GameAssetError(
|
|
28
|
+
'ASSET_INVALID_FRAME_RECT',
|
|
29
|
+
path,
|
|
30
|
+
`frame ${JSON.stringify(name)} coordinates must be finite numbers`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (x < 0 || y < 0) {
|
|
35
|
+
throw new GameAssetError(
|
|
36
|
+
'ASSET_INVALID_FRAME_RECT',
|
|
37
|
+
path,
|
|
38
|
+
`frame ${JSON.stringify(name)} x/y must be non-negative`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (width <= 0 || height <= 0) {
|
|
42
|
+
throw new GameAssetError(
|
|
43
|
+
'ASSET_INVALID_FRAME_RECT',
|
|
44
|
+
path,
|
|
45
|
+
`frame ${JSON.stringify(name)} width/height must be greater than zero`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validate one animation clip against the sheet's declared frames. */
|
|
51
|
+
export function validateClip(
|
|
52
|
+
path: readonly string[],
|
|
53
|
+
name: string,
|
|
54
|
+
clip: SpriteClip,
|
|
55
|
+
frames: Readonly<Record<string, SpriteFrameRect>>,
|
|
56
|
+
): void {
|
|
57
|
+
assertValidIdentifier(path, name);
|
|
58
|
+
if (typeof clip.frames !== 'object' || clip.frames.length === 0) {
|
|
59
|
+
throw new GameAssetError(
|
|
60
|
+
'ASSET_EMPTY_CLIP',
|
|
61
|
+
path,
|
|
62
|
+
`clip ${JSON.stringify(name)} must reference at least one frame`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
for (const frame of clip.frames) {
|
|
66
|
+
if (typeof frame !== 'string' || !(frame in frames)) {
|
|
67
|
+
throw new GameAssetError(
|
|
68
|
+
'ASSET_UNKNOWN_FRAME',
|
|
69
|
+
[...path, 'frames'],
|
|
70
|
+
`clip ${JSON.stringify(name)} references unknown frame ${JSON.stringify(frame)}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const duration = clip.frameDurationMs;
|
|
75
|
+
if (typeof duration !== 'number' || !Number.isFinite(duration) || duration <= 0) {
|
|
76
|
+
throw new GameAssetError(
|
|
77
|
+
'ASSET_INVALID_DURATION',
|
|
78
|
+
[...path, 'frameDurationMs'],
|
|
79
|
+
`clip ${JSON.stringify(name)} duration must be finite and greater than zero`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (clip.mode !== 'loop' && clip.mode !== 'once') {
|
|
83
|
+
throw new GameAssetError(
|
|
84
|
+
'ASSET_INVALID_MODE',
|
|
85
|
+
[...path, 'mode'],
|
|
86
|
+
`clip ${JSON.stringify(name)} mode must be 'loop' or 'once'`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Validate a full sprite-sheet descriptor. */
|
|
92
|
+
export function validateSpriteSheet(
|
|
93
|
+
source: unknown,
|
|
94
|
+
spec: { readonly frames: Readonly<Record<string, SpriteFrameRect>>; readonly animations: Readonly<Record<string, SpriteClip>> },
|
|
95
|
+
): void {
|
|
96
|
+
if (typeof source !== 'number' || !Number.isInteger(source) || source < 0) {
|
|
97
|
+
throw new GameAssetError(
|
|
98
|
+
'ASSET_INVALID_SOURCE',
|
|
99
|
+
[],
|
|
100
|
+
'sprite-sheet source must be a static module handle (number)',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
for (const [name, rect] of Object.entries(spec.frames)) {
|
|
104
|
+
validateFrameRect(['frames', name], name, rect);
|
|
105
|
+
}
|
|
106
|
+
for (const [name, clip] of Object.entries(spec.animations)) {
|
|
107
|
+
validateClip(['animations', name], name, clip, spec.frames);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Validate an image descriptor's source. */
|
|
112
|
+
export function validateImageSource(source: unknown): void {
|
|
113
|
+
if (typeof source !== 'number' || !Number.isInteger(source) || source < 0) {
|
|
114
|
+
throw new GameAssetError(
|
|
115
|
+
'ASSET_INVALID_SOURCE',
|
|
116
|
+
[],
|
|
117
|
+
'image source must be a static module handle (number)',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validate a full asset manifest. */
|
|
123
|
+
export function validateManifest(manifest: AssetGroupMap): void {
|
|
124
|
+
for (const [group, assets] of Object.entries(manifest)) {
|
|
125
|
+
const groupPath = [group];
|
|
126
|
+
assertValidIdentifier([], group);
|
|
127
|
+
if (typeof assets !== 'object' || assets === null) {
|
|
128
|
+
throw new GameAssetError(
|
|
129
|
+
'ASSET_INVALID_IDENTIFIER',
|
|
130
|
+
groupPath,
|
|
131
|
+
`group ${JSON.stringify(group)} must be an object of named assets`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
for (const [name, descriptor] of Object.entries(assets)) {
|
|
135
|
+
const path = [...groupPath, name];
|
|
136
|
+
assertValidIdentifier(groupPath, name);
|
|
137
|
+
if (descriptor === null || typeof descriptor !== 'object') {
|
|
138
|
+
throw new GameAssetError(
|
|
139
|
+
'ASSET_INVALID_IDENTIFIER',
|
|
140
|
+
path,
|
|
141
|
+
`asset ${JSON.stringify(name)} must be a descriptor`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if ((descriptor as { readonly kind?: unknown }).kind === 'image') {
|
|
145
|
+
validateImageSource((descriptor as { readonly source?: unknown }).source);
|
|
146
|
+
} else if ((descriptor as { readonly kind?: unknown }).kind === 'sprite-sheet') {
|
|
147
|
+
const sheet = descriptor as unknown as SpriteSheetDescriptor;
|
|
148
|
+
validateSpriteSheet(sheet.source, sheet);
|
|
149
|
+
} else {
|
|
150
|
+
throw new GameAssetError(
|
|
151
|
+
'ASSET_INVALID_IDENTIFIER',
|
|
152
|
+
path,
|
|
153
|
+
`asset ${JSON.stringify(name)} has unknown descriptor kind`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Opaque handle returned by an animation-frame driver. */
|
|
2
|
+
export type FrameHandle = number;
|
|
3
|
+
|
|
4
|
+
/** Minimal presentation clock used by the fixed-step runtime. */
|
|
5
|
+
export interface FrameDriver {
|
|
6
|
+
/** Request the next presentation callback. */
|
|
7
|
+
requestFrame(callback: (timestampMs: number) => void): FrameHandle;
|
|
8
|
+
/** Cancel a previously requested callback. */
|
|
9
|
+
cancelFrame(handle: FrameHandle): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AnimationFrameHost {
|
|
13
|
+
requestAnimationFrame(callback: (timestampMs: number) => void): number;
|
|
14
|
+
cancelAnimationFrame(handle: number): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Create the platform animation-frame driver used by live sessions. */
|
|
18
|
+
export function createAnimationFrameDriver(): FrameDriver {
|
|
19
|
+
const host = globalThis as unknown as Partial<AnimationFrameHost>;
|
|
20
|
+
const requestAnimationFrame = host.requestAnimationFrame;
|
|
21
|
+
const cancelAnimationFrame = host.cancelAnimationFrame;
|
|
22
|
+
|
|
23
|
+
if (!requestAnimationFrame || !cancelAnimationFrame) {
|
|
24
|
+
throw new Error('GameSession requires requestAnimationFrame on this platform');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
requestFrame: (callback) => requestAnimationFrame(callback),
|
|
29
|
+
cancelFrame: (handle) => cancelAnimationFrame(handle),
|
|
30
|
+
};
|
|
31
|
+
}
|