@ankhorage/zora-game 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +2 -1
- package/dist/ZORA_GAME_COMPONENT_META.d.ts +118 -0
- package/dist/ZORA_GAME_COMPONENT_META.d.ts.map +1 -1
- package/dist/ZORA_GAME_COMPONENT_META.js +2 -0
- package/dist/ZORA_GAME_COMPONENT_META.js.map +1 -1
- package/dist/ZORA_GAME_PLUGIN.d.ts +126 -3
- package/dist/ZORA_GAME_PLUGIN.d.ts.map +1 -1
- package/dist/ZORA_GAME_PLUGIN.js +2 -0
- package/dist/ZORA_GAME_PLUGIN.js.map +1 -1
- package/dist/ZORA_PLUGIN_METADATA.d.ts +124 -3
- package/dist/ZORA_PLUGIN_METADATA.d.ts.map +1 -1
- package/dist/ZORA_PLUGIN_METADATA.js +7 -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/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/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 +2 -1
- package/dist/features/game-presentation/public.d.ts.map +1 -1
- package/dist/features/game-presentation/public.js +1 -0
- package/dist/features/game-presentation/public.js.map +1 -1
- 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 +9 -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 +2 -0
- package/src/ZORA_GAME_PLUGIN.ts +2 -0
- package/src/ZORA_PLUGIN_METADATA.test.ts +28 -5
- package/src/ZORA_PLUGIN_METADATA.ts +7 -3
- package/src/features/game-presentation/adapters/inbound/Game.tsx +27 -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/gameMeta.ts +99 -0
- package/src/features/game-presentation/public.ts +2 -0
- package/src/index.ts +2 -1
- package/src/types/gamePresentation.ts +10 -0
- package/src/types/gameRuntime.ts +13 -0
|
@@ -4,13 +4,30 @@ 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('GameOverlay');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('describes bindable Game inputs and domain-neutral outputs', () => {
|
|
25
|
+
const catalog = composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]);
|
|
26
|
+
const game = readComponentMeta(catalog.componentMeta.Game, 'Game');
|
|
27
|
+
|
|
28
|
+
expect(game.bindings?.props?.definition?.value.type).toBe('object');
|
|
29
|
+
expect(game.bindings?.props?.input?.value.type).toBe('record');
|
|
30
|
+
expect(game.events?.output?.eventType).toBe('game.output');
|
|
14
31
|
});
|
|
15
32
|
|
|
16
33
|
test('keeps the generic presentation vocabulary free of product-specific concepts', () => {
|
|
@@ -22,3 +39,9 @@ describe('ZORA game plugin metadata', () => {
|
|
|
22
39
|
expect(serialized).not.toContain('letterprojectile');
|
|
23
40
|
});
|
|
24
41
|
});
|
|
42
|
+
|
|
43
|
+
/*** Require one composed component metadata entry for focused assertions. */
|
|
44
|
+
function readComponentMeta<TValue>(value: TValue | undefined, name: string): TValue {
|
|
45
|
+
if (value === undefined) throw new Error(`Missing composed component metadata for ${name}.`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
@@ -7,19 +7,23 @@ 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: ['GameField', 'GameEntity', 'GameOverlay'],
|
|
10
|
+
extensionHosts: ['Game', 'GameField', 'GameEntity', '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'],
|
|
19
23
|
},
|
|
20
24
|
{
|
|
21
25
|
child: 'GameOverlay',
|
|
22
|
-
parents: ['GameField'],
|
|
26
|
+
parents: ['Game', 'GameField'],
|
|
23
27
|
},
|
|
24
28
|
],
|
|
25
29
|
} 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
|
+
}
|
|
@@ -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);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
advanceGameTime,
|
|
3
|
+
applyGameEvent,
|
|
4
|
+
createGameSession,
|
|
5
|
+
type GameInput,
|
|
6
|
+
type GameOutput,
|
|
7
|
+
} from '@ankhorage/game';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import type { GameProps } from '../../../types/gamePresentation';
|
|
11
|
+
import type { GameRuntimeContextValue, GameRuntimeState } from '../../../types/gameRuntime';
|
|
12
|
+
import { useGameScheduledEffects } from './useGameScheduledEffects';
|
|
13
|
+
|
|
14
|
+
const EMPTY_GAME_INPUT: GameInput = {};
|
|
15
|
+
|
|
16
|
+
/*** Own one local transient Game session while delegating all domain transitions to @ankhorage/game. */
|
|
17
|
+
export function useGameRuntime(props: GameProps): GameRuntimeContextValue {
|
|
18
|
+
const { definition, input = EMPTY_GAME_INPUT, seed = 0, resetKey = '' } = props;
|
|
19
|
+
const definitionRef = useLatestValue(definition);
|
|
20
|
+
const inputRef = useLatestValue(input);
|
|
21
|
+
const outputRef = useLatestValue(props.onOutput);
|
|
22
|
+
const autoAdvanceTime = props.autoAdvanceTime ?? true;
|
|
23
|
+
const clockRef = React.useRef(0);
|
|
24
|
+
const runtimeKey = createRuntimeKey(definition.id, resetKey, seed);
|
|
25
|
+
const [runtime, setRuntime] = React.useState<GameRuntimeState>(() =>
|
|
26
|
+
createRuntimeState(definition, input, seed, runtimeKey, 0),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
useGameRuntimeReset({
|
|
30
|
+
clockRef,
|
|
31
|
+
definitionRef,
|
|
32
|
+
inputRef,
|
|
33
|
+
runtimeKey,
|
|
34
|
+
seed,
|
|
35
|
+
setRuntime,
|
|
36
|
+
});
|
|
37
|
+
useGameOutputEmitter(runtime, outputRef);
|
|
38
|
+
|
|
39
|
+
const dispatch = React.useCallback<GameRuntimeContextValue['dispatch']>(
|
|
40
|
+
(event) => {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
setRuntime((current) =>
|
|
43
|
+
applyRuntimeEvent({
|
|
44
|
+
autoAdvanceTime,
|
|
45
|
+
current,
|
|
46
|
+
definition: definitionRef.current,
|
|
47
|
+
elapsedMs: Math.max(0, now - clockRef.current),
|
|
48
|
+
event,
|
|
49
|
+
input: inputRef.current,
|
|
50
|
+
runtimeKey,
|
|
51
|
+
seed,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
clockRef.current = now;
|
|
55
|
+
},
|
|
56
|
+
[autoAdvanceTime, definitionRef, inputRef, runtimeKey, seed],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useGameScheduledEffects({
|
|
60
|
+
clockRef,
|
|
61
|
+
definitionRef,
|
|
62
|
+
enabled: autoAdvanceTime,
|
|
63
|
+
inputRef,
|
|
64
|
+
runtimeKey,
|
|
65
|
+
scheduled: runtime.session.scheduled,
|
|
66
|
+
setRuntime,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return React.useMemo(() => ({ session: runtime.session, dispatch }), [dispatch, runtime.session]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface GameRuntimeResetArgs {
|
|
73
|
+
readonly clockRef: React.RefObject<number>;
|
|
74
|
+
readonly definitionRef: React.RefObject<GameProps['definition']>;
|
|
75
|
+
readonly inputRef: React.RefObject<GameInput>;
|
|
76
|
+
readonly runtimeKey: string;
|
|
77
|
+
readonly seed: number;
|
|
78
|
+
readonly setRuntime: React.Dispatch<React.SetStateAction<GameRuntimeState>>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*** Reset the transient session only when its explicit serializable runtime identity changes. */
|
|
82
|
+
function useGameRuntimeReset({
|
|
83
|
+
clockRef,
|
|
84
|
+
definitionRef,
|
|
85
|
+
inputRef,
|
|
86
|
+
runtimeKey,
|
|
87
|
+
seed,
|
|
88
|
+
setRuntime,
|
|
89
|
+
}: GameRuntimeResetArgs): void {
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
clockRef.current = Date.now();
|
|
92
|
+
setRuntime((current) =>
|
|
93
|
+
current.key === runtimeKey
|
|
94
|
+
? current
|
|
95
|
+
: createRuntimeState(
|
|
96
|
+
definitionRef.current,
|
|
97
|
+
inputRef.current,
|
|
98
|
+
seed,
|
|
99
|
+
runtimeKey,
|
|
100
|
+
current.revision + 1,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
}, [clockRef, definitionRef, inputRef, runtimeKey, seed, setRuntime]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/*** Emit each runtime output exactly when one execution revision becomes current. */
|
|
107
|
+
function useGameOutputEmitter(
|
|
108
|
+
runtime: GameRuntimeState,
|
|
109
|
+
outputRef: React.RefObject<GameProps['onOutput']>,
|
|
110
|
+
): void {
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
emitGameOutputs(runtime.outputs, outputRef.current);
|
|
113
|
+
}, [outputRef, runtime.outputs, runtime.revision]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface ApplyRuntimeEventArgs {
|
|
117
|
+
readonly autoAdvanceTime: boolean;
|
|
118
|
+
readonly current: GameRuntimeState;
|
|
119
|
+
readonly definition: GameProps['definition'];
|
|
120
|
+
readonly elapsedMs: number;
|
|
121
|
+
readonly event: Parameters<typeof applyGameEvent>[2];
|
|
122
|
+
readonly input: GameInput;
|
|
123
|
+
readonly runtimeKey: string;
|
|
124
|
+
readonly seed: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*** Apply elapsed time and one explicit game event against the matching local session. */
|
|
128
|
+
function applyRuntimeEvent(args: ApplyRuntimeEventArgs): GameRuntimeState {
|
|
129
|
+
const base =
|
|
130
|
+
args.current.key === args.runtimeKey
|
|
131
|
+
? args.current
|
|
132
|
+
: createRuntimeState(
|
|
133
|
+
args.definition,
|
|
134
|
+
args.input,
|
|
135
|
+
args.seed,
|
|
136
|
+
args.runtimeKey,
|
|
137
|
+
args.current.revision,
|
|
138
|
+
);
|
|
139
|
+
const timed = args.autoAdvanceTime
|
|
140
|
+
? advanceGameTime(args.definition, base.session, args.elapsedMs, args.input)
|
|
141
|
+
: { session: base.session, outputs: [] };
|
|
142
|
+
const result = applyGameEvent(args.definition, timed.session, args.event, args.input);
|
|
143
|
+
return {
|
|
144
|
+
key: args.runtimeKey,
|
|
145
|
+
session: result.session,
|
|
146
|
+
outputs: [...(base === args.current ? [] : base.outputs), ...timed.outputs, ...result.outputs],
|
|
147
|
+
revision: args.current.revision + 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*** Create one transient runtime state through the published Game owner API. */
|
|
152
|
+
function createRuntimeState(
|
|
153
|
+
definition: GameProps['definition'],
|
|
154
|
+
input: GameInput,
|
|
155
|
+
seed: number,
|
|
156
|
+
key: string,
|
|
157
|
+
revision: number,
|
|
158
|
+
): GameRuntimeState {
|
|
159
|
+
const result = createGameSession(definition, { input, seed });
|
|
160
|
+
return { key, session: result.session, outputs: result.outputs, revision };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*** Create one runtime identity from serializable definition/session initialization inputs. */
|
|
164
|
+
function createRuntimeKey(definitionId: string, resetKey: string, seed: number): string {
|
|
165
|
+
return `${definitionId}:${resetKey}:${seed}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*** Emit runtime outputs through the ZORA event callback boundary. */
|
|
169
|
+
function emitGameOutputs(outputs: readonly GameOutput[], onOutput: GameProps['onOutput']): void {
|
|
170
|
+
if (onOutput === undefined) return;
|
|
171
|
+
outputs.forEach((output) => onOutput(output));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*** Keep a stable ref object updated with the latest adapter input. */
|
|
175
|
+
function useLatestValue<TValue>(value: TValue): React.RefObject<TValue> {
|
|
176
|
+
const ref = React.useRef(value);
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
ref.current = value;
|
|
179
|
+
}, [value]);
|
|
180
|
+
return ref;
|
|
181
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { advanceGameTime, type GameDefinition, type GameInput } from '@ankhorage/game';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { GameRuntimeState } from '../../../types/gameRuntime';
|
|
5
|
+
|
|
6
|
+
type SetGameRuntime = React.Dispatch<React.SetStateAction<GameRuntimeState>>;
|
|
7
|
+
|
|
8
|
+
/*** Advance the nearest scheduled game effects through one platform timer. */
|
|
9
|
+
export function useGameScheduledEffects({
|
|
10
|
+
clockRef,
|
|
11
|
+
definitionRef,
|
|
12
|
+
enabled,
|
|
13
|
+
inputRef,
|
|
14
|
+
runtimeKey,
|
|
15
|
+
scheduled,
|
|
16
|
+
setRuntime,
|
|
17
|
+
}: UseGameScheduledEffectsArgs): void {
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!enabled || scheduled.length === 0) return undefined;
|
|
20
|
+
const delayMs = resolveNextScheduledDelay(scheduled);
|
|
21
|
+
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const elapsedMs = Math.max(0, now - clockRef.current);
|
|
25
|
+
clockRef.current = now;
|
|
26
|
+
setRuntime((current) =>
|
|
27
|
+
advanceScheduledRuntime({
|
|
28
|
+
current,
|
|
29
|
+
definition: definitionRef.current,
|
|
30
|
+
elapsedMs,
|
|
31
|
+
input: inputRef.current,
|
|
32
|
+
runtimeKey,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}, delayMs);
|
|
36
|
+
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, [clockRef, definitionRef, enabled, inputRef, runtimeKey, scheduled, setRuntime]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface UseGameScheduledEffectsArgs {
|
|
42
|
+
readonly clockRef: React.RefObject<number>;
|
|
43
|
+
readonly definitionRef: React.RefObject<GameDefinition>;
|
|
44
|
+
readonly enabled: boolean;
|
|
45
|
+
readonly inputRef: React.RefObject<GameInput>;
|
|
46
|
+
readonly runtimeKey: string;
|
|
47
|
+
readonly scheduled: GameRuntimeState['session']['scheduled'];
|
|
48
|
+
readonly setRuntime: SetGameRuntime;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface AdvanceScheduledRuntimeArgs {
|
|
52
|
+
readonly current: GameRuntimeState;
|
|
53
|
+
readonly definition: GameDefinition;
|
|
54
|
+
readonly elapsedMs: number;
|
|
55
|
+
readonly input: GameInput;
|
|
56
|
+
readonly runtimeKey: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*** Advance one matching runtime state while preserving sessions that have already reset. */
|
|
60
|
+
function advanceScheduledRuntime({
|
|
61
|
+
current,
|
|
62
|
+
definition,
|
|
63
|
+
elapsedMs,
|
|
64
|
+
input,
|
|
65
|
+
runtimeKey,
|
|
66
|
+
}: AdvanceScheduledRuntimeArgs): GameRuntimeState {
|
|
67
|
+
if (current.key !== runtimeKey) return current;
|
|
68
|
+
const result = advanceGameTime(definition, current.session, elapsedMs, input);
|
|
69
|
+
return {
|
|
70
|
+
key: runtimeKey,
|
|
71
|
+
session: result.session,
|
|
72
|
+
outputs: result.outputs,
|
|
73
|
+
revision: current.revision + 1,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*** Resolve the nearest scheduled game consequence without owning domain timing rules. */
|
|
78
|
+
function resolveNextScheduledDelay(
|
|
79
|
+
scheduledEffects: GameRuntimeState['session']['scheduled'],
|
|
80
|
+
): number {
|
|
81
|
+
return scheduledEffects.reduce(
|
|
82
|
+
(nearest, scheduled) => Math.min(nearest, scheduled.remainingMs),
|
|
83
|
+
Number.POSITIVE_INFINITY,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ZoraComponentMeta } from '@ankhorage/zora/metadata';
|
|
2
|
+
|
|
3
|
+
/*** Describe the embeddable config-driven Game runtime/presentation boundary for ZORA authoring. */
|
|
4
|
+
export const gameMeta = {
|
|
5
|
+
name: 'Game',
|
|
6
|
+
category: 'pattern',
|
|
7
|
+
description:
|
|
8
|
+
'Embeds one config-driven game session inside ordinary ZORA layout and emits domain-neutral game outputs.',
|
|
9
|
+
directManifestNode: true,
|
|
10
|
+
allowedChildren: ['GameEntity', 'GameOverlay', 'Gradient', 'Image', 'View'],
|
|
11
|
+
blueprint: {
|
|
12
|
+
label: 'Game',
|
|
13
|
+
defaultProps: {
|
|
14
|
+
definition: {
|
|
15
|
+
id: 'game',
|
|
16
|
+
initialPhase: 'playing',
|
|
17
|
+
stages: [{ id: 'main' }],
|
|
18
|
+
rules: [],
|
|
19
|
+
},
|
|
20
|
+
seed: 0,
|
|
21
|
+
resetKey: '',
|
|
22
|
+
autoAdvanceTime: true,
|
|
23
|
+
minHeight: 320,
|
|
24
|
+
clip: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
bindings: {
|
|
28
|
+
props: {
|
|
29
|
+
definition: {
|
|
30
|
+
value: { type: 'object' },
|
|
31
|
+
acceptsFallback: true,
|
|
32
|
+
},
|
|
33
|
+
input: {
|
|
34
|
+
value: { type: 'record' },
|
|
35
|
+
acceptsFallback: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
events: {
|
|
40
|
+
output: {
|
|
41
|
+
label: 'Game output',
|
|
42
|
+
eventType: 'game.output',
|
|
43
|
+
description: 'Emitted when the platform-neutral game runtime produces an app/domain output.',
|
|
44
|
+
payloadFields: [
|
|
45
|
+
{ path: 'type', type: 'string' },
|
|
46
|
+
{ path: 'payload', type: 'record' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
props: {
|
|
51
|
+
seed: {
|
|
52
|
+
type: 'number',
|
|
53
|
+
category: 'Runtime',
|
|
54
|
+
label: 'Initial seed',
|
|
55
|
+
default: 0,
|
|
56
|
+
authoring: { authority: 'instance' },
|
|
57
|
+
},
|
|
58
|
+
resetKey: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
category: 'Runtime',
|
|
61
|
+
label: 'Reset key',
|
|
62
|
+
default: '',
|
|
63
|
+
authoring: { authority: 'instance' },
|
|
64
|
+
},
|
|
65
|
+
autoAdvanceTime: {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
category: 'Runtime',
|
|
68
|
+
label: 'Advance scheduled effects',
|
|
69
|
+
default: true,
|
|
70
|
+
authoring: { authority: 'instance' },
|
|
71
|
+
},
|
|
72
|
+
aspectRatio: {
|
|
73
|
+
type: 'number',
|
|
74
|
+
category: 'Layout',
|
|
75
|
+
label: 'Aspect ratio',
|
|
76
|
+
authoring: { authority: 'instance' },
|
|
77
|
+
},
|
|
78
|
+
minHeight: {
|
|
79
|
+
type: 'number',
|
|
80
|
+
category: 'Layout',
|
|
81
|
+
label: 'Minimum height',
|
|
82
|
+
default: 320,
|
|
83
|
+
authoring: { authority: 'instance' },
|
|
84
|
+
},
|
|
85
|
+
clip: {
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
category: 'Layout',
|
|
88
|
+
label: 'Clip overflow',
|
|
89
|
+
default: true,
|
|
90
|
+
authoring: { authority: 'instance' },
|
|
91
|
+
},
|
|
92
|
+
accessibilityLabel: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
category: 'Accessibility',
|
|
95
|
+
label: 'Accessibility label',
|
|
96
|
+
authoring: { authority: 'instance' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
} as const satisfies ZoraComponentMeta;
|
|
@@ -4,7 +4,9 @@ export type {
|
|
|
4
4
|
GameOverlayPlacement,
|
|
5
5
|
GameOverlayProps,
|
|
6
6
|
GamePointerEvents,
|
|
7
|
+
GameProps,
|
|
7
8
|
} from '../../types/gamePresentation';
|
|
9
|
+
export { Game } from './adapters/inbound/Game';
|
|
8
10
|
export { GameEntity } from './adapters/inbound/GameEntity';
|
|
9
11
|
export { GameField } from './adapters/inbound/GameField';
|
|
10
12
|
export { GameOverlay } from './adapters/inbound/GameOverlay';
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
GameOverlayPlacement,
|
|
5
5
|
GameOverlayProps,
|
|
6
6
|
GamePointerEvents,
|
|
7
|
+
GameProps,
|
|
7
8
|
} from './features/game-presentation/public';
|
|
8
|
-
export { GameEntity, GameField, GameOverlay } from './features/game-presentation/public';
|
|
9
|
+
export { Game, GameEntity, GameField, GameOverlay } from './features/game-presentation/public';
|
|
9
10
|
export { ZORA_GAME_PLUGIN } from './ZORA_GAME_PLUGIN';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
|
|
3
4
|
type GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
@@ -39,3 +40,12 @@ export interface GameOverlayProps {
|
|
|
39
40
|
readonly accessibilityLabel?: string;
|
|
40
41
|
readonly testID?: string;
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
export interface GameProps extends GameFieldProps {
|
|
45
|
+
readonly definition: GameDefinition;
|
|
46
|
+
readonly input?: GameInput;
|
|
47
|
+
readonly seed?: number;
|
|
48
|
+
readonly resetKey?: string;
|
|
49
|
+
readonly autoAdvanceTime?: boolean;
|
|
50
|
+
readonly onOutput?: (output: GameOutput) => void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GameEvent, GameOutput, GameSession } from '@ankhorage/game';
|
|
2
|
+
|
|
3
|
+
export interface GameRuntimeContextValue {
|
|
4
|
+
readonly session: GameSession;
|
|
5
|
+
readonly dispatch: (event: GameEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GameRuntimeState {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
readonly session: GameSession;
|
|
11
|
+
readonly outputs: readonly GameOutput[];
|
|
12
|
+
readonly revision: number;
|
|
13
|
+
}
|