@ankhorage/zora-game 0.1.0 → 0.3.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 +14 -0
- package/README.md +3 -1
- package/dist/ZORA_GAME_COMPONENT_META.d.ts +228 -0
- package/dist/ZORA_GAME_COMPONENT_META.d.ts.map +1 -1
- package/dist/ZORA_GAME_COMPONENT_META.js +4 -0
- package/dist/ZORA_GAME_COMPONENT_META.js.map +1 -1
- package/dist/ZORA_GAME_PLUGIN.d.ts +241 -3
- package/dist/ZORA_GAME_PLUGIN.d.ts.map +1 -1
- package/dist/ZORA_GAME_PLUGIN.js +4 -0
- package/dist/ZORA_GAME_PLUGIN.js.map +1 -1
- package/dist/ZORA_PLUGIN_METADATA.d.ts +237 -3
- package/dist/ZORA_PLUGIN_METADATA.d.ts.map +1 -1
- package/dist/ZORA_PLUGIN_METADATA.js +11 -3
- package/dist/ZORA_PLUGIN_METADATA.js.map +1 -1
- package/dist/features/game-presentation/adapters/inbound/Game.d.ts +5 -0
- package/dist/features/game-presentation/adapters/inbound/Game.d.ts.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/Game.js +16 -0
- package/dist/features/game-presentation/adapters/inbound/Game.js.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/GameEntity.d.ts.map +1 -1
- package/dist/features/game-presentation/adapters/inbound/GameEntity.js +4 -11
- package/dist/features/game-presentation/adapters/inbound/GameEntity.js.map +1 -1
- package/dist/features/game-presentation/adapters/inbound/GameInputZone.d.ts +4 -0
- package/dist/features/game-presentation/adapters/inbound/GameInputZone.d.ts.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/GameInputZone.js +35 -0
- package/dist/features/game-presentation/adapters/inbound/GameInputZone.js.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/createGamePointerEvent.d.ts +17 -0
- package/dist/features/game-presentation/adapters/inbound/createGamePointerEvent.d.ts.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/createGamePointerEvent.js +20 -0
- package/dist/features/game-presentation/adapters/inbound/createGamePointerEvent.js.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/useGameInputZoneResponder.d.ts +9 -0
- package/dist/features/game-presentation/adapters/inbound/useGameInputZoneResponder.d.ts.map +1 -0
- package/dist/features/game-presentation/adapters/inbound/useGameInputZoneResponder.js +37 -0
- package/dist/features/game-presentation/adapters/inbound/useGameInputZoneResponder.js.map +1 -0
- package/dist/features/game-presentation/composition/GameRuntimeContext.d.ts +5 -0
- package/dist/features/game-presentation/composition/GameRuntimeContext.d.ts.map +1 -0
- package/dist/features/game-presentation/composition/GameRuntimeContext.js +4 -0
- package/dist/features/game-presentation/composition/GameRuntimeContext.js.map +1 -0
- package/dist/features/game-presentation/composition/useGameRuntime.d.ts +5 -0
- package/dist/features/game-presentation/composition/useGameRuntime.d.ts.map +1 -0
- package/dist/features/game-presentation/composition/useGameRuntime.js +103 -0
- package/dist/features/game-presentation/composition/useGameRuntime.js.map +1 -0
- package/dist/features/game-presentation/composition/useGameScheduledEffects.d.ts +17 -0
- package/dist/features/game-presentation/composition/useGameScheduledEffects.d.ts.map +1 -0
- package/dist/features/game-presentation/composition/useGameScheduledEffects.js +40 -0
- package/dist/features/game-presentation/composition/useGameScheduledEffects.js.map +1 -0
- package/dist/features/game-presentation/meta/gameInputZoneMeta.d.ts +112 -0
- package/dist/features/game-presentation/meta/gameInputZoneMeta.d.ts.map +1 -0
- package/dist/features/game-presentation/meta/gameInputZoneMeta.js +92 -0
- package/dist/features/game-presentation/meta/gameInputZoneMeta.js.map +1 -0
- package/dist/features/game-presentation/meta/gameMeta.d.ts +120 -0
- package/dist/features/game-presentation/meta/gameMeta.d.ts.map +1 -0
- package/dist/features/game-presentation/meta/gameMeta.js +97 -0
- package/dist/features/game-presentation/meta/gameMeta.js.map +1 -0
- package/dist/features/game-presentation/public.d.ts +3 -1
- package/dist/features/game-presentation/public.d.ts.map +1 -1
- package/dist/features/game-presentation/public.js +2 -0
- package/dist/features/game-presentation/public.js.map +1 -1
- package/dist/features/game-presentation/utils/clampGamePercentage.d.ts +3 -0
- package/dist/features/game-presentation/utils/clampGamePercentage.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/clampGamePercentage.js +5 -0
- package/dist/features/game-presentation/utils/clampGamePercentage.js.map +1 -0
- package/dist/features/game-presentation/utils/toGamePercentage.d.ts +3 -0
- package/dist/features/game-presentation/utils/toGamePercentage.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/toGamePercentage.js +6 -0
- package/dist/features/game-presentation/utils/toGamePercentage.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/gamePresentation.d.ts +22 -0
- package/dist/types/gamePresentation.d.ts.map +1 -1
- package/dist/types/gamePresentation.js.map +1 -1
- package/dist/types/gameRuntime.d.ts +12 -0
- package/dist/types/gameRuntime.d.ts.map +1 -0
- package/dist/types/gameRuntime.js +2 -0
- package/dist/types/gameRuntime.js.map +1 -0
- package/package.json +5 -2
- package/src/ZORA_GAME_COMPONENT_META.ts +4 -0
- package/src/ZORA_GAME_PLUGIN.ts +4 -0
- package/src/ZORA_PLUGIN_METADATA.test.ts +39 -5
- package/src/ZORA_PLUGIN_METADATA.ts +11 -3
- package/src/features/game-presentation/adapters/inbound/Game.tsx +27 -0
- package/src/features/game-presentation/adapters/inbound/GameEntity.tsx +4 -13
- package/src/features/game-presentation/adapters/inbound/GameInputZone.tsx +70 -0
- package/src/features/game-presentation/adapters/inbound/createGamePointerEvent.test.ts +56 -0
- package/src/features/game-presentation/adapters/inbound/createGamePointerEvent.ts +39 -0
- package/src/features/game-presentation/adapters/inbound/useGameInputZoneResponder.ts +49 -0
- package/src/features/game-presentation/composition/GameRuntimeContext.ts +6 -0
- package/src/features/game-presentation/composition/useGameRuntime.ts +181 -0
- package/src/features/game-presentation/composition/useGameScheduledEffects.ts +85 -0
- package/src/features/game-presentation/meta/gameInputZoneMeta.ts +94 -0
- package/src/features/game-presentation/meta/gameMeta.ts +99 -0
- package/src/features/game-presentation/public.ts +4 -0
- package/src/features/game-presentation/utils/clampGamePercentage.ts +4 -0
- package/src/features/game-presentation/utils/toGamePercentage.ts +6 -0
- package/src/index.ts +9 -1
- package/src/types/gamePresentation.ts +24 -0
- package/src/types/gameRuntime.ts +13 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export type { GameEntityProps, GameFieldProps, GameOverlayPlacement, GameOverlayProps, GamePointerEvents, } from '../../types/gamePresentation';
|
|
1
|
+
export type { GameEntityProps, GameFieldProps, GameInputZoneProps, GameOverlayPlacement, GameOverlayProps, GamePointerEvents, GameProps, } from '../../types/gamePresentation';
|
|
2
|
+
export { Game } from './adapters/inbound/Game';
|
|
2
3
|
export { GameEntity } from './adapters/inbound/GameEntity';
|
|
3
4
|
export { GameField } from './adapters/inbound/GameField';
|
|
5
|
+
export { GameInputZone } from './adapters/inbound/GameInputZone';
|
|
4
6
|
export { GameOverlay } from './adapters/inbound/GameOverlay';
|
|
5
7
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/game-presentation/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/game-presentation/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,GACV,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { Game } from './adapters/inbound/Game';
|
|
1
2
|
export { GameEntity } from './adapters/inbound/GameEntity';
|
|
2
3
|
export { GameField } from './adapters/inbound/GameField';
|
|
4
|
+
export { GameInputZone } from './adapters/inbound/GameInputZone';
|
|
3
5
|
export { GameOverlay } from './adapters/inbound/GameOverlay';
|
|
4
6
|
//# sourceMappingURL=public.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/game-presentation/public.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/game-presentation/public.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC","sourcesContent":["export type {\n GameEntityProps,\n GameFieldProps,\n GameInputZoneProps,\n GameOverlayPlacement,\n GameOverlayProps,\n GamePointerEvents,\n GameProps,\n} from '../../types/gamePresentation';\nexport { Game } from './adapters/inbound/Game';\nexport { GameEntity } from './adapters/inbound/GameEntity';\nexport { GameField } from './adapters/inbound/GameField';\nexport { GameInputZone } from './adapters/inbound/GameInputZone';\nexport { GameOverlay } from './adapters/inbound/GameOverlay';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clampGamePercentage.d.ts","sourceRoot":"","sources":["../../../../src/features/game-presentation/utils/clampGamePercentage.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clampGamePercentage.js","sourceRoot":"","sources":["../../../../src/features/game-presentation/utils/clampGamePercentage.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/*** Clamp one game presentation coordinate to the inclusive field-percentage range. */\nexport function clampGamePercentage(value: number): number {\n return Math.min(100, Math.max(0, value));\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toGamePercentage.d.ts","sourceRoot":"","sources":["../../../../src/features/game-presentation/utils/toGamePercentage.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,MAAM,GAAG,CAE5D"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { clampGamePercentage } from './clampGamePercentage';
|
|
2
|
+
/*** Convert one game percentage coordinate to React Native percentage style syntax. */
|
|
3
|
+
export function toGamePercentage(value) {
|
|
4
|
+
return `${clampGamePercentage(value)}%`;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=toGamePercentage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toGamePercentage.js","sourceRoot":"","sources":["../../../../src/features/game-presentation/utils/toGamePercentage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,uFAAuF;AACvF,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,GAAG,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC;AAC1C,CAAC","sourcesContent":["import { clampGamePercentage } from './clampGamePercentage';\n\n/*** Convert one game percentage coordinate to React Native percentage style syntax. */\nexport function toGamePercentage(value: number): `${number}%` {\n return `${clampGamePercentage(value)}%`;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { GameEntityProps, GameFieldProps, GameOverlayPlacement, GameOverlayProps, GamePointerEvents, } from './features/game-presentation/public';
|
|
2
|
-
export { GameEntity, GameField, GameOverlay } from './features/game-presentation/public';
|
|
1
|
+
export type { GameEntityProps, GameFieldProps, GameInputZoneProps, GameOverlayPlacement, GameOverlayProps, GamePointerEvents, GameProps, } from './features/game-presentation/public';
|
|
2
|
+
export { Game, GameEntity, GameField, GameInputZone, GameOverlay, } from './features/game-presentation/public';
|
|
3
3
|
export { ZORA_GAME_PLUGIN } from './ZORA_GAME_PLUGIN';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,GACV,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,IAAI,EACJ,UAAU,EACV,SAAS,EACT,aAAa,EACb,WAAW,GACZ,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { GameEntity, GameField, GameOverlay } from './features/game-presentation/public';
|
|
1
|
+
export { Game, GameEntity, GameField, GameInputZone, GameOverlay, } from './features/game-presentation/public';
|
|
2
2
|
export { ZORA_GAME_PLUGIN } from './ZORA_GAME_PLUGIN';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EACL,IAAI,EACJ,UAAU,EACV,SAAS,EACT,aAAa,EACb,WAAW,GACZ,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["export type {\n GameEntityProps,\n GameFieldProps,\n GameInputZoneProps,\n GameOverlayPlacement,\n GameOverlayProps,\n GamePointerEvents,\n GameProps,\n} from './features/game-presentation/public';\nexport {\n Game,\n GameEntity,\n GameField,\n GameInputZone,\n GameOverlay,\n} from './features/game-presentation/public';\nexport { ZORA_GAME_PLUGIN } from './ZORA_GAME_PLUGIN';\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
type GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
3
4
|
export type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';
|
|
@@ -25,6 +26,19 @@ export interface GameEntityProps {
|
|
|
25
26
|
readonly accessibilityLabel?: string;
|
|
26
27
|
readonly testID?: string;
|
|
27
28
|
}
|
|
29
|
+
export interface GameInputZoneProps {
|
|
30
|
+
readonly eventType: string;
|
|
31
|
+
readonly entityId?: string;
|
|
32
|
+
readonly x?: number;
|
|
33
|
+
readonly y?: number;
|
|
34
|
+
readonly width?: number;
|
|
35
|
+
readonly height?: number;
|
|
36
|
+
readonly zIndex?: number;
|
|
37
|
+
readonly enabled?: boolean;
|
|
38
|
+
readonly continuous?: boolean;
|
|
39
|
+
readonly accessibilityLabel?: string;
|
|
40
|
+
readonly testID?: string;
|
|
41
|
+
}
|
|
28
42
|
export interface GameOverlayProps {
|
|
29
43
|
readonly children?: React.ReactNode;
|
|
30
44
|
readonly placement?: GameOverlayPlacement;
|
|
@@ -33,5 +47,13 @@ export interface GameOverlayProps {
|
|
|
33
47
|
readonly accessibilityLabel?: string;
|
|
34
48
|
readonly testID?: string;
|
|
35
49
|
}
|
|
50
|
+
export interface GameProps extends GameFieldProps {
|
|
51
|
+
readonly definition: GameDefinition;
|
|
52
|
+
readonly input?: GameInput;
|
|
53
|
+
readonly seed?: number;
|
|
54
|
+
readonly resetKey?: string;
|
|
55
|
+
readonly autoAdvanceTime?: boolean;
|
|
56
|
+
readonly onOutput?: (output: GameOutput) => void;
|
|
57
|
+
}
|
|
36
58
|
export {};
|
|
37
59
|
//# sourceMappingURL=gamePresentation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gamePresentation.d.ts","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,0BAA0B,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;AAE5F,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;AAC1E,MAAM,MAAM,oBAAoB,GAC9B,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
|
1
|
+
{"version":3,"file":"gamePresentation.d.ts","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gamePresentation.js","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\ntype GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\nexport type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';\nexport type GameOverlayPlacement =\n 'fill' | 'center' | 'top' | 'bottom' | GameOverlayCornerPlacement;\n\nexport interface GameFieldProps {\n readonly children?: React.ReactNode;\n readonly aspectRatio?: number;\n readonly minHeight?: number;\n readonly clip?: boolean;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameEntityProps {\n readonly children?: React.ReactNode;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly opacity?: number;\n readonly scale?: number;\n readonly rotation?: number;\n readonly zIndex?: number;\n readonly hidden?: boolean;\n readonly pointerEvents?: GamePointerEvents;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameOverlayProps {\n readonly children?: React.ReactNode;\n readonly placement?: GameOverlayPlacement;\n readonly blocking?: boolean;\n readonly padding?: number;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gamePresentation.js","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"","sourcesContent":["import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';\nimport type React from 'react';\n\ntype GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\nexport type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';\nexport type GameOverlayPlacement =\n 'fill' | 'center' | 'top' | 'bottom' | GameOverlayCornerPlacement;\n\nexport interface GameFieldProps {\n readonly children?: React.ReactNode;\n readonly aspectRatio?: number;\n readonly minHeight?: number;\n readonly clip?: boolean;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameEntityProps {\n readonly children?: React.ReactNode;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly opacity?: number;\n readonly scale?: number;\n readonly rotation?: number;\n readonly zIndex?: number;\n readonly hidden?: boolean;\n readonly pointerEvents?: GamePointerEvents;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameInputZoneProps {\n readonly eventType: string;\n readonly entityId?: string;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly zIndex?: number;\n readonly enabled?: boolean;\n readonly continuous?: boolean;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameOverlayProps {\n readonly children?: React.ReactNode;\n readonly placement?: GameOverlayPlacement;\n readonly blocking?: boolean;\n readonly padding?: number;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameProps extends GameFieldProps {\n readonly definition: GameDefinition;\n readonly input?: GameInput;\n readonly seed?: number;\n readonly resetKey?: string;\n readonly autoAdvanceTime?: boolean;\n readonly onOutput?: (output: GameOutput) => void;\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GameEvent, GameOutput, GameSession } from '@ankhorage/game';
|
|
2
|
+
export interface GameRuntimeContextValue {
|
|
3
|
+
readonly session: GameSession;
|
|
4
|
+
readonly dispatch: (event: GameEvent) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface GameRuntimeState {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly session: GameSession;
|
|
9
|
+
readonly outputs: readonly GameOutput[];
|
|
10
|
+
readonly revision: number;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=gameRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gameRuntime.d.ts","sourceRoot":"","sources":["../../src/types/gameRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gameRuntime.js","sourceRoot":"","sources":["../../src/types/gameRuntime.ts"],"names":[],"mappings":"","sourcesContent":["import type { GameEvent, GameOutput, GameSession } from '@ankhorage/game';\n\nexport interface GameRuntimeContextValue {\n readonly session: GameSession;\n readonly dispatch: (event: GameEvent) => void;\n}\n\nexport interface GameRuntimeState {\n readonly key: string;\n readonly session: GameSession;\n readonly outputs: readonly GameOutput[];\n readonly revision: number;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora-game",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Generic game presentation primitives for React Native and React Native Web apps built on ZORA.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora-game#readme",
|
|
@@ -91,5 +91,8 @@
|
|
|
91
91
|
"react-native-web": "~0.21.0",
|
|
92
92
|
"typescript": "~6.0.3"
|
|
93
93
|
},
|
|
94
|
-
"packageManager": "bun@1.4.2"
|
|
94
|
+
"packageManager": "bun@1.4.2",
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"@ankhorage/game": "^0.1.0"
|
|
97
|
+
}
|
|
95
98
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { gameEntityMeta } from './features/game-presentation/meta/gameEntityMeta';
|
|
2
2
|
import { gameFieldMeta } from './features/game-presentation/meta/gameFieldMeta';
|
|
3
|
+
import { gameInputZoneMeta } from './features/game-presentation/meta/gameInputZoneMeta';
|
|
4
|
+
import { gameMeta } from './features/game-presentation/meta/gameMeta';
|
|
3
5
|
import { gameOverlayMeta } from './features/game-presentation/meta/gameOverlayMeta';
|
|
4
6
|
|
|
5
7
|
/*** Register the generic game presentation metadata owned by this package. */
|
|
6
8
|
export const ZORA_GAME_COMPONENT_META = {
|
|
9
|
+
Game: gameMeta,
|
|
7
10
|
GameEntity: gameEntityMeta,
|
|
8
11
|
GameField: gameFieldMeta,
|
|
12
|
+
GameInputZone: gameInputZoneMeta,
|
|
9
13
|
GameOverlay: gameOverlayMeta,
|
|
10
14
|
} as const;
|
package/src/ZORA_GAME_PLUGIN.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Game } from './features/game-presentation/adapters/inbound/Game';
|
|
1
2
|
import { GameEntity } from './features/game-presentation/adapters/inbound/GameEntity';
|
|
2
3
|
import { GameField } from './features/game-presentation/adapters/inbound/GameField';
|
|
4
|
+
import { GameInputZone } from './features/game-presentation/adapters/inbound/GameInputZone';
|
|
3
5
|
import { GameOverlay } from './features/game-presentation/adapters/inbound/GameOverlay';
|
|
4
6
|
import { ZORA_PLUGIN_METADATA } from './ZORA_PLUGIN_METADATA';
|
|
5
7
|
|
|
@@ -7,8 +9,10 @@ import { ZORA_PLUGIN_METADATA } from './ZORA_PLUGIN_METADATA';
|
|
|
7
9
|
export const ZORA_GAME_PLUGIN = {
|
|
8
10
|
...ZORA_PLUGIN_METADATA,
|
|
9
11
|
componentRegistry: {
|
|
12
|
+
Game,
|
|
10
13
|
GameEntity,
|
|
11
14
|
GameField,
|
|
15
|
+
GameInputZone,
|
|
12
16
|
GameOverlay,
|
|
13
17
|
},
|
|
14
18
|
} as const;
|
|
@@ -4,13 +4,41 @@ import { describe, expect, test } from 'bun:test';
|
|
|
4
4
|
import { ZORA_PLUGIN_METADATA } from './ZORA_PLUGIN_METADATA';
|
|
5
5
|
|
|
6
6
|
describe('ZORA game plugin metadata', () => {
|
|
7
|
-
test('makes GameField embeddable in ordinary ZORA containers', () => {
|
|
7
|
+
test('makes Game and GameField embeddable in ordinary ZORA containers', () => {
|
|
8
8
|
const catalog = composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]);
|
|
9
|
+
const game = readComponentMeta(catalog.componentMeta.Game, 'Game');
|
|
10
|
+
const gameField = readComponentMeta(catalog.componentMeta.GameField, 'GameField');
|
|
11
|
+
const screen = readComponentMeta(catalog.componentMeta.Screen, 'Screen');
|
|
12
|
+
const view = readComponentMeta(catalog.componentMeta.View, 'View');
|
|
9
13
|
|
|
10
|
-
expect(
|
|
11
|
-
expect(
|
|
12
|
-
expect(
|
|
13
|
-
expect(
|
|
14
|
+
expect(screen.allowedChildren).toContain('Game');
|
|
15
|
+
expect(view.allowedChildren).toContain('Game');
|
|
16
|
+
expect(screen.allowedChildren).toContain('GameField');
|
|
17
|
+
expect(view.allowedChildren).toContain('GameField');
|
|
18
|
+
expect(gameField.allowedChildren).toContain('GameEntity');
|
|
19
|
+
expect(gameField.allowedChildren).toContain('GameOverlay');
|
|
20
|
+
expect(game.allowedChildren).toContain('GameEntity');
|
|
21
|
+
expect(game.allowedChildren).toContain('GameInputZone');
|
|
22
|
+
expect(game.allowedChildren).toContain('GameOverlay');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('keeps Game input zones scoped to runtime-owning Game parents', () => {
|
|
26
|
+
const catalog = composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]);
|
|
27
|
+
const gameInputZone = readComponentMeta(catalog.componentMeta.GameInputZone, 'GameInputZone');
|
|
28
|
+
|
|
29
|
+
expect(gameInputZone.props.eventType?.type).toBe('string');
|
|
30
|
+
expect(gameInputZone.props.continuous?.type).toBe('boolean');
|
|
31
|
+
expect(catalog.componentMeta.Game?.allowedChildren).toContain('GameInputZone');
|
|
32
|
+
expect(catalog.componentMeta.GameField?.allowedChildren).not.toContain('GameInputZone');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('describes bindable Game inputs and domain-neutral outputs', () => {
|
|
36
|
+
const catalog = composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]);
|
|
37
|
+
const game = readComponentMeta(catalog.componentMeta.Game, 'Game');
|
|
38
|
+
|
|
39
|
+
expect(game.bindings?.props?.definition?.value.type).toBe('object');
|
|
40
|
+
expect(game.bindings?.props?.input?.value.type).toBe('record');
|
|
41
|
+
expect(game.events?.output?.eventType).toBe('game.output');
|
|
14
42
|
});
|
|
15
43
|
|
|
16
44
|
test('keeps the generic presentation vocabulary free of product-specific concepts', () => {
|
|
@@ -22,3 +50,9 @@ describe('ZORA game plugin metadata', () => {
|
|
|
22
50
|
expect(serialized).not.toContain('letterprojectile');
|
|
23
51
|
});
|
|
24
52
|
});
|
|
53
|
+
|
|
54
|
+
/*** Require one composed component metadata entry for focused assertions. */
|
|
55
|
+
function readComponentMeta<TValue>(value: TValue | undefined, name: string): TValue {
|
|
56
|
+
if (value === undefined) throw new Error(`Missing composed component metadata for ${name}.`);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
@@ -7,19 +7,27 @@ export const ZORA_PLUGIN_METADATA = {
|
|
|
7
7
|
packageName: '@ankhorage/zora-game',
|
|
8
8
|
displayName: 'ZORA Game',
|
|
9
9
|
componentMeta: ZORA_GAME_COMPONENT_META,
|
|
10
|
-
extensionHosts: ['
|
|
10
|
+
extensionHosts: ['Game', 'GameEntity', 'GameField', 'GameInputZone', 'GameOverlay'],
|
|
11
11
|
placements: [
|
|
12
|
+
{
|
|
13
|
+
child: 'Game',
|
|
14
|
+
parents: ['Card', 'Grid', 'Screen', 'ScreenSection', 'View'],
|
|
15
|
+
},
|
|
12
16
|
{
|
|
13
17
|
child: 'GameField',
|
|
14
18
|
parents: ['Card', 'Grid', 'Screen', 'ScreenSection', 'View'],
|
|
15
19
|
},
|
|
16
20
|
{
|
|
17
21
|
child: 'GameEntity',
|
|
18
|
-
parents: ['GameField', 'GameOverlay'],
|
|
22
|
+
parents: ['Game', 'GameField', 'GameOverlay'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
child: 'GameInputZone',
|
|
26
|
+
parents: ['Game'],
|
|
19
27
|
},
|
|
20
28
|
{
|
|
21
29
|
child: 'GameOverlay',
|
|
22
|
-
parents: ['GameField'],
|
|
30
|
+
parents: ['Game', 'GameField'],
|
|
23
31
|
},
|
|
24
32
|
],
|
|
25
33
|
} as const satisfies ZoraPluginMetadata;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { GameProps } from '../../../../types/gamePresentation';
|
|
4
|
+
import { GameRuntimeContext } from '../../composition/GameRuntimeContext';
|
|
5
|
+
import { useGameRuntime } from '../../composition/useGameRuntime';
|
|
6
|
+
import { GameField } from './GameField';
|
|
7
|
+
|
|
8
|
+
/*** Bind one serializable game definition to a local transient session and presentation field. */
|
|
9
|
+
export function Game(props: GameProps) {
|
|
10
|
+
const runtime = useGameRuntime(props);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<GameRuntimeContext.Provider value={runtime}>
|
|
14
|
+
<GameField
|
|
15
|
+
{...(props.aspectRatio === undefined ? {} : { aspectRatio: props.aspectRatio })}
|
|
16
|
+
{...(props.minHeight === undefined ? {} : { minHeight: props.minHeight })}
|
|
17
|
+
{...(props.clip === undefined ? {} : { clip: props.clip })}
|
|
18
|
+
{...(props.accessibilityLabel === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { accessibilityLabel: props.accessibilityLabel })}
|
|
21
|
+
{...(props.testID === undefined ? {} : { testID: props.testID })}
|
|
22
|
+
>
|
|
23
|
+
{props.children}
|
|
24
|
+
</GameField>
|
|
25
|
+
</GameRuntimeContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -2,6 +2,7 @@ import { View } from '@ankhorage/zora';
|
|
|
2
2
|
import { StyleSheet, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import type { GameEntityProps } from '../../../../types/gamePresentation';
|
|
5
|
+
import { toGamePercentage } from '../../utils/toGamePercentage';
|
|
5
6
|
|
|
6
7
|
/*** Render one generic positioned game entity without owning gameplay semantics. */
|
|
7
8
|
export function GameEntity({
|
|
@@ -60,10 +61,10 @@ function createEntityStyle({
|
|
|
60
61
|
}: GameEntityStyleInput): ViewStyle {
|
|
61
62
|
return {
|
|
62
63
|
display: hidden ? 'none' : 'flex',
|
|
63
|
-
left:
|
|
64
|
-
opacity:
|
|
64
|
+
left: toGamePercentage(x),
|
|
65
|
+
opacity: Math.min(1, Math.max(0, opacity)),
|
|
65
66
|
position: 'absolute',
|
|
66
|
-
top:
|
|
67
|
+
top: toGamePercentage(y),
|
|
67
68
|
transform: [{ scale }, { rotate: `${rotation}deg` }],
|
|
68
69
|
zIndex,
|
|
69
70
|
...(height === undefined ? {} : { height }),
|
|
@@ -71,16 +72,6 @@ function createEntityStyle({
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/*** Convert a bounded percentage number to React Native percentage syntax. */
|
|
75
|
-
function toPercent(value: number): `${number}%` {
|
|
76
|
-
return `${clamp(value, 0, 100)}%`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/*** Clamp one numeric presentation value to an inclusive range. */
|
|
80
|
-
function clamp(value: number, minimum: number, maximum: number): number {
|
|
81
|
-
return Math.min(maximum, Math.max(minimum, value));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
75
|
const styles = StyleSheet.create({
|
|
85
76
|
root: {
|
|
86
77
|
position: 'absolute',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { GameInputZoneProps } from '../../../../types/gamePresentation';
|
|
4
|
+
import { toGamePercentage } from '../../utils/toGamePercentage';
|
|
5
|
+
import { useGameInputZoneResponder } from './useGameInputZoneResponder';
|
|
6
|
+
|
|
7
|
+
/*** Capture pointer/touch geometry and dispatch only normalized generic Game events. */
|
|
8
|
+
export function GameInputZone({
|
|
9
|
+
x = 0,
|
|
10
|
+
y = 0,
|
|
11
|
+
width = 100,
|
|
12
|
+
height = 100,
|
|
13
|
+
zIndex = 0,
|
|
14
|
+
enabled = true,
|
|
15
|
+
continuous = true,
|
|
16
|
+
accessibilityLabel,
|
|
17
|
+
testID,
|
|
18
|
+
...inputProps
|
|
19
|
+
}: GameInputZoneProps) {
|
|
20
|
+
const { dispatchPointer, handleLayout, shouldSetResponder } = useGameInputZoneResponder({
|
|
21
|
+
...inputProps,
|
|
22
|
+
x,
|
|
23
|
+
y,
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
enabled,
|
|
27
|
+
continuous,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
{...(accessibilityLabel === undefined ? {} : { accessibilityLabel })}
|
|
33
|
+
{...(testID === undefined ? {} : { testID })}
|
|
34
|
+
{...(continuous
|
|
35
|
+
? { onResponderGrant: dispatchPointer, onResponderMove: dispatchPointer }
|
|
36
|
+
: { onResponderRelease: dispatchPointer })}
|
|
37
|
+
collapsable={false}
|
|
38
|
+
onLayout={handleLayout}
|
|
39
|
+
onStartShouldSetResponder={shouldSetResponder}
|
|
40
|
+
pointerEvents={enabled ? 'auto' : 'none'}
|
|
41
|
+
style={createInputZoneStyle({ x, y, width, height, zIndex })}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface InputZoneStyleInput {
|
|
47
|
+
readonly x: number;
|
|
48
|
+
readonly y: number;
|
|
49
|
+
readonly width: number;
|
|
50
|
+
readonly height: number;
|
|
51
|
+
readonly zIndex: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*** Convert field-relative input-zone props into one absolute React Native style. */
|
|
55
|
+
function createInputZoneStyle({ x, y, width, height, zIndex }: InputZoneStyleInput): ViewStyle {
|
|
56
|
+
return {
|
|
57
|
+
left: toGamePercentage(x),
|
|
58
|
+
top: toGamePercentage(y),
|
|
59
|
+
width: toGamePercentage(width),
|
|
60
|
+
height: toGamePercentage(height),
|
|
61
|
+
zIndex,
|
|
62
|
+
...styles.root,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const styles = StyleSheet.create({
|
|
67
|
+
root: {
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { createGamePointerEvent } from './createGamePointerEvent';
|
|
4
|
+
|
|
5
|
+
describe('createGamePointerEvent', () => {
|
|
6
|
+
test('maps zone-local pointer coordinates into field percentages', () => {
|
|
7
|
+
expect(
|
|
8
|
+
createGamePointerEvent({
|
|
9
|
+
eventType: 'player.move',
|
|
10
|
+
entityId: 'player',
|
|
11
|
+
localX: 100,
|
|
12
|
+
localY: 25,
|
|
13
|
+
measuredWidth: 200,
|
|
14
|
+
measuredHeight: 100,
|
|
15
|
+
zoneX: 10,
|
|
16
|
+
zoneY: 60,
|
|
17
|
+
zoneWidth: 80,
|
|
18
|
+
zoneHeight: 30,
|
|
19
|
+
}),
|
|
20
|
+
).toEqual({
|
|
21
|
+
type: 'player.move',
|
|
22
|
+
entityId: 'player',
|
|
23
|
+
payload: { x: 50, y: 67.5 },
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('clamps out-of-bounds native coordinates and rejects unmeasured zones', () => {
|
|
28
|
+
expect(
|
|
29
|
+
createGamePointerEvent({
|
|
30
|
+
eventType: 'pointer.move',
|
|
31
|
+
localX: 500,
|
|
32
|
+
localY: -20,
|
|
33
|
+
measuredWidth: 100,
|
|
34
|
+
measuredHeight: 100,
|
|
35
|
+
zoneX: 20,
|
|
36
|
+
zoneY: 20,
|
|
37
|
+
zoneWidth: 50,
|
|
38
|
+
zoneHeight: 50,
|
|
39
|
+
}),
|
|
40
|
+
).toEqual({ type: 'pointer.move', payload: { x: 70, y: 20 } });
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
createGamePointerEvent({
|
|
44
|
+
eventType: 'pointer.move',
|
|
45
|
+
localX: 0,
|
|
46
|
+
localY: 0,
|
|
47
|
+
measuredWidth: 0,
|
|
48
|
+
measuredHeight: 100,
|
|
49
|
+
zoneX: 0,
|
|
50
|
+
zoneY: 0,
|
|
51
|
+
zoneWidth: 100,
|
|
52
|
+
zoneHeight: 100,
|
|
53
|
+
}),
|
|
54
|
+
).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GameEvent } from '@ankhorage/game';
|
|
2
|
+
|
|
3
|
+
import { clampGamePercentage } from '../../utils/clampGamePercentage';
|
|
4
|
+
|
|
5
|
+
interface GamePointerEventInput {
|
|
6
|
+
readonly eventType: string;
|
|
7
|
+
readonly entityId?: string;
|
|
8
|
+
readonly localX: number;
|
|
9
|
+
readonly localY: number;
|
|
10
|
+
readonly measuredWidth: number;
|
|
11
|
+
readonly measuredHeight: number;
|
|
12
|
+
readonly zoneX: number;
|
|
13
|
+
readonly zoneY: number;
|
|
14
|
+
readonly zoneWidth: number;
|
|
15
|
+
readonly zoneHeight: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*** Normalize one measured pointer/touch position into a field-relative generic Game event. */
|
|
19
|
+
export function createGamePointerEvent(input: GamePointerEventInput): GameEvent | undefined {
|
|
20
|
+
if (
|
|
21
|
+
!Number.isFinite(input.measuredWidth) ||
|
|
22
|
+
!Number.isFinite(input.measuredHeight) ||
|
|
23
|
+
input.measuredWidth <= 0 ||
|
|
24
|
+
input.measuredHeight <= 0
|
|
25
|
+
) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const localX = clampGamePercentage((input.localX / input.measuredWidth) * 100);
|
|
30
|
+
const localY = clampGamePercentage((input.localY / input.measuredHeight) * 100);
|
|
31
|
+
const x = clampGamePercentage(input.zoneX + (localX * input.zoneWidth) / 100);
|
|
32
|
+
const y = clampGamePercentage(input.zoneY + (localY * input.zoneHeight) / 100);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
type: input.eventType,
|
|
36
|
+
...(input.entityId === undefined ? {} : { entityId: input.entityId }),
|
|
37
|
+
payload: { x, y },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { GestureResponderEvent, LayoutChangeEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { GameInputZoneProps } from '../../../../types/gamePresentation';
|
|
5
|
+
import { GameRuntimeContext } from '../../composition/GameRuntimeContext';
|
|
6
|
+
import { createGamePointerEvent } from './createGamePointerEvent';
|
|
7
|
+
|
|
8
|
+
interface MeasuredSize {
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*** Own React Native responder measurement and event dispatch for one Game input zone. */
|
|
14
|
+
export function useGameInputZoneResponder(props: GameInputZoneProps) {
|
|
15
|
+
const runtime = React.useContext(GameRuntimeContext);
|
|
16
|
+
if (runtime === null) throw new Error('GameInputZone must be rendered inside Game.');
|
|
17
|
+
|
|
18
|
+
const measuredSizeRef = React.useRef<MeasuredSize>({ width: 0, height: 0 });
|
|
19
|
+
const handleLayout = React.useCallback((event: LayoutChangeEvent) => {
|
|
20
|
+
const { width, height } = event.nativeEvent.layout;
|
|
21
|
+
measuredSizeRef.current = { width, height };
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const dispatchPointer = React.useCallback(
|
|
25
|
+
(event: GestureResponderEvent) => {
|
|
26
|
+
const measured = measuredSizeRef.current;
|
|
27
|
+
const gameEvent = createGamePointerEvent({
|
|
28
|
+
eventType: props.eventType,
|
|
29
|
+
...(props.entityId === undefined ? {} : { entityId: props.entityId }),
|
|
30
|
+
localX: event.nativeEvent.locationX,
|
|
31
|
+
localY: event.nativeEvent.locationY,
|
|
32
|
+
measuredWidth: measured.width,
|
|
33
|
+
measuredHeight: measured.height,
|
|
34
|
+
zoneX: props.x ?? 0,
|
|
35
|
+
zoneY: props.y ?? 0,
|
|
36
|
+
zoneWidth: props.width ?? 100,
|
|
37
|
+
zoneHeight: props.height ?? 100,
|
|
38
|
+
});
|
|
39
|
+
if (gameEvent !== undefined) runtime.dispatch(gameEvent);
|
|
40
|
+
},
|
|
41
|
+
[props.entityId, props.eventType, props.height, props.width, props.x, props.y, runtime],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
dispatchPointer,
|
|
46
|
+
handleLayout,
|
|
47
|
+
shouldSetResponder: () => props.enabled ?? true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { GameRuntimeContextValue } from '../../../types/gameRuntime';
|
|
4
|
+
|
|
5
|
+
/*** Provide the local transient game session and event dispatcher to presentation adapters. */
|
|
6
|
+
export const GameRuntimeContext = React.createContext<GameRuntimeContextValue | null>(null);
|