@rogue-x/engine 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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/actions.d.ts +84 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +170 -0
  6. package/dist/assemble.d.ts +46 -0
  7. package/dist/assemble.d.ts.map +1 -0
  8. package/dist/assemble.js +68 -0
  9. package/dist/assets.d.ts +61 -0
  10. package/dist/assets.d.ts.map +1 -0
  11. package/dist/assets.js +58 -0
  12. package/dist/audio.d.ts +20 -0
  13. package/dist/audio.d.ts.map +1 -0
  14. package/dist/audio.js +12 -0
  15. package/dist/camera.d.ts +54 -0
  16. package/dist/camera.d.ts.map +1 -0
  17. package/dist/camera.js +51 -0
  18. package/dist/clock.d.ts +41 -0
  19. package/dist/clock.d.ts.map +1 -0
  20. package/dist/clock.js +26 -0
  21. package/dist/coords.d.ts +16 -0
  22. package/dist/coords.d.ts.map +1 -0
  23. package/dist/coords.js +17 -0
  24. package/dist/engine.d.ts +89 -0
  25. package/dist/engine.d.ts.map +1 -0
  26. package/dist/engine.js +140 -0
  27. package/dist/events.d.ts +37 -0
  28. package/dist/events.d.ts.map +1 -0
  29. package/dist/events.js +41 -0
  30. package/dist/helpers.d.ts +41 -0
  31. package/dist/helpers.d.ts.map +1 -0
  32. package/dist/index.d.ts +48 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +53 -0
  35. package/dist/modes.d.ts +44 -0
  36. package/dist/modes.d.ts.map +1 -0
  37. package/dist/modes.js +69 -0
  38. package/dist/movement.d.ts +77 -0
  39. package/dist/movement.d.ts.map +1 -0
  40. package/dist/movement.js +135 -0
  41. package/dist/patches.d.ts +29 -0
  42. package/dist/patches.d.ts.map +1 -0
  43. package/dist/patches.js +163 -0
  44. package/dist/persistence.d.ts +31 -0
  45. package/dist/persistence.d.ts.map +1 -0
  46. package/dist/persistence.js +32 -0
  47. package/dist/profiles.d.ts +37 -0
  48. package/dist/profiles.d.ts.map +1 -0
  49. package/dist/profiles.js +52 -0
  50. package/dist/resolver.d.ts +62 -0
  51. package/dist/resolver.d.ts.map +1 -0
  52. package/dist/resolver.js +105 -0
  53. package/dist/rng.d.ts +7 -0
  54. package/dist/rng.d.ts.map +1 -0
  55. package/dist/rng.js +20 -0
  56. package/dist/scene.d.ts +18 -0
  57. package/dist/scene.d.ts.map +1 -0
  58. package/dist/scene.js +58 -0
  59. package/dist/scheduler.d.ts +63 -0
  60. package/dist/scheduler.d.ts.map +1 -0
  61. package/dist/scheduler.js +77 -0
  62. package/dist/stubs.d.ts +54 -0
  63. package/dist/stubs.d.ts.map +1 -0
  64. package/dist/stubs.js +8 -0
  65. package/dist/testing.d.ts +65 -0
  66. package/dist/testing.d.ts.map +1 -0
  67. package/dist/testing.js +67 -0
  68. package/dist/tools/assetManifest.d.ts +12 -0
  69. package/dist/tools/assetManifest.d.ts.map +1 -0
  70. package/dist/tools/index.d.ts +2 -0
  71. package/dist/tools/index.d.ts.map +1 -0
  72. package/dist/tools/index.js +1 -0
  73. package/dist/tools/validator.d.ts +21 -0
  74. package/dist/tools/validator.d.ts.map +1 -0
  75. package/dist/tools/validator.js +12 -0
  76. package/dist/tools/worldBuilder.d.ts +22 -0
  77. package/dist/tools/worldBuilder.d.ts.map +1 -0
  78. package/dist/topology.d.ts +36 -0
  79. package/dist/topology.d.ts.map +1 -0
  80. package/dist/topology.js +132 -0
  81. package/dist/transitions.d.ts +27 -0
  82. package/dist/transitions.d.ts.map +1 -0
  83. package/dist/transitions.js +31 -0
  84. package/dist/types.d.ts +487 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +9 -0
  87. package/dist/web/GameHost.d.ts +110 -0
  88. package/dist/web/GameHost.d.ts.map +1 -0
  89. package/dist/web/GameHost.js +472 -0
  90. package/dist/web/index.d.ts +14 -0
  91. package/dist/web/index.d.ts.map +1 -0
  92. package/dist/web/index.js +11 -0
  93. package/dist/web/sfx.d.ts +41 -0
  94. package/dist/web/sfx.d.ts.map +1 -0
  95. package/dist/web/sfx.js +108 -0
  96. package/dist/world.d.ts +13 -0
  97. package/dist/world.d.ts.map +1 -0
  98. package/dist/zone.d.ts +6 -0
  99. package/dist/zone.d.ts.map +1 -0
  100. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RogueX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @rogue-x/engine
2
+
3
+ A platform-free, data-driven game engine SDK. You describe a game as **data** — a
4
+ `GameDefinition` of tiles, scenes, entities, actions, and events — and the engine
5
+ runs it. The browser host (`@rogue-x/engine/web`) renders, drives the clock, binds
6
+ input, and plays audio, so a complete game can be a single definition file plus one
7
+ `<GameHost />`.
8
+
9
+ > **Status:** web-first. The browser host is the supported runtime today. A React
10
+ > Native / Expo host is a work in progress.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @rogue-x/engine react react-dom
16
+ ```
17
+
18
+ `react` is an optional peer dependency — only needed if you use the `/web` host.
19
+
20
+ ## Quick start
21
+
22
+ ```tsx
23
+ import { GameHost } from '@rogue-x/engine/web';
24
+ import type { GameDefinition } from '@rogue-x/engine';
25
+
26
+ const definition: GameDefinition = {
27
+ title: 'Hello',
28
+ profile: 'grid-turn-based',
29
+ tileTypes: { floor: { id: 'floor', visuals: ['plain'] } },
30
+ scenes: {
31
+ room: {
32
+ topology: { kind: 'square4', width: 5, height: 5 },
33
+ layers: [{ id: 'ground', role: 'logical', fill: 'floor' }],
34
+ entities: [
35
+ { id: 'hero', type: 'hero', position: { kind: 'discrete', loc: { x: 2, y: 2 } }, data: {} },
36
+ ],
37
+ },
38
+ },
39
+ initialSceneId: 'room',
40
+ onStep: () => ({ patch: {} }),
41
+ };
42
+
43
+ export default () => <GameHost definition={definition} tileSize={40} />;
44
+ ```
45
+
46
+ See **[GETTING_STARTED.md](./GETTING_STARTED.md)** for the full walkthrough — defining
47
+ a game, the action/patch model, events, modes, audio, and mounting the host.
48
+
49
+ ## Entry points
50
+
51
+ | Import | What it is |
52
+ | ------------------------- | ------------------------------------------------------ |
53
+ | `@rogue-x/engine` | Core: `createGame`, types, coordinate + patch helpers. |
54
+ | `@rogue-x/engine/web` | Browser host: `GameHost`, `VisualConfig`, controls. |
55
+ | `@rogue-x/engine/tools` | Authoring helpers (content validation). |
56
+ | `@rogue-x/engine/testing` | Headless test session helpers. |
57
+
58
+ Optional, swappable modules (e.g. pathfinding) live in
59
+ [`@rogue-x/engine-modules`](https://www.npmjs.com/package/@rogue-x/engine-modules).
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Actions / commands (ENG-47 … ENG-51).
3
+ *
4
+ * An action is mode-agnostic: it declares a target type, optional eligibility,
5
+ * optional cost, and an optional duration, and is invocable from any mode
6
+ * (ENG-47). These helpers are pure — they read a {@link StepContext} and return
7
+ * either a rejection (the engine writes nothing, ENG-48) or a {@link Transaction}
8
+ * the engine commits atomically (ENG-58):
9
+ *
10
+ * - {@link evaluateAction} validates and produces the commit for one invocation;
11
+ * an **instant** action (duration 0) folds its `resolve` into the commit, a
12
+ * **durative** action enqueues an {@link InProgressAction} (ENG-49);
13
+ * - {@link tickActions} advances the durative queue one step, resolving any that
14
+ * reach zero remaining;
15
+ * - {@link cancelAction} removes an in-progress action and refunds its cost iff
16
+ * the definition is refundable (ENG-50);
17
+ * - {@link queryActions} answers availability with the ENG-51 three-way result:
18
+ * `available` / `locked` (identifying the locking action) / `none`.
19
+ */
20
+ import type { ActionDefinition, ActionId, ActionTarget, DefaultGameTypes, GameTypes, InProgressAction, StepContext, Transaction } from './types.js';
21
+ /** Action definitions indexed by id for O(1) lookup. */
22
+ export type ActionRegistry<G extends GameTypes = DefaultGameTypes> = Record<ActionId, ActionDefinition<G>>;
23
+ /** Build a registry from a flat list (last definition per id wins). */
24
+ export declare function indexActions<G extends GameTypes = DefaultGameTypes>(defs: ActionDefinition<G>[]): ActionRegistry<G>;
25
+ /** Why an invocation was rejected as a no-op (ENG-48). */
26
+ export type ActionRejectReason = 'unknown' | 'ineligible' | 'locked' | 'concurrency' | 'cost';
27
+ /** Imperative invocation outcome surfaced by the engine (ENG-48). */
28
+ export type ActionResult = {
29
+ ok: true;
30
+ } | {
31
+ ok: false;
32
+ reason: ActionRejectReason;
33
+ lockedBy?: ActionId;
34
+ };
35
+ /** Pure evaluation: a rejection, or the transaction to commit. */
36
+ export type ActionEvaluation<G extends GameTypes = DefaultGameTypes> = {
37
+ ok: true;
38
+ transaction: Transaction<G>;
39
+ } | {
40
+ ok: false;
41
+ reason: ActionRejectReason;
42
+ lockedBy?: ActionId;
43
+ };
44
+ /** Availability answer for a target (ENG-51). */
45
+ export type ActionQuery = {
46
+ kind: 'available';
47
+ actions: ActionId[];
48
+ } | {
49
+ kind: 'locked';
50
+ by: ActionId;
51
+ } | {
52
+ kind: 'none';
53
+ };
54
+ /** Structural target equality (locking / availability, ENG-51). */
55
+ export declare function isActionTargetEqual(a: ActionTarget, b: ActionTarget): boolean;
56
+ /**
57
+ * Validate one invocation and, if valid, produce the transaction to commit.
58
+ * Rejection order mirrors ENG-48: unknown → ineligible → locked → concurrency →
59
+ * cost. Nothing here mutates state (ENG-58, rejection before commit).
60
+ */
61
+ export declare function evaluateAction<G extends GameTypes = DefaultGameTypes>(ctx: StepContext<G>, registry: ActionRegistry<G>, actionId: ActionId, target: ActionTarget): ActionEvaluation<G>;
62
+ export interface ActionTick<G extends GameTypes = DefaultGameTypes> {
63
+ /** The durative queue after one step's decrement. */
64
+ actions: InProgressAction[];
65
+ /** Resolutions from actions that completed this step. */
66
+ transactions: Transaction<G>[];
67
+ }
68
+ /**
69
+ * Advance the durative queue by one step: decrement each remaining, resolve any
70
+ * that hit zero (and drop them), keep the rest (ENG-49). Pure — the engine folds
71
+ * the returned `actions` and `transactions` into the step's atomic commit.
72
+ */
73
+ export declare function tickActions<G extends GameTypes = DefaultGameTypes>(ctx: StepContext<G>, registry: ActionRegistry<G>): ActionTick<G>;
74
+ /**
75
+ * Cancel the first in-progress action matching `predicate`, refunding its cost
76
+ * iff the definition is refundable (ENG-50). Returns `null` when none match.
77
+ */
78
+ export declare function cancelAction<G extends GameTypes = DefaultGameTypes>(ctx: StepContext<G>, registry: ActionRegistry<G>, predicate: (a: InProgressAction) => boolean): Transaction<G> | null;
79
+ /**
80
+ * Availability for a target (ENG-51): `locked` (with the locking action) takes
81
+ * precedence; otherwise the eligible/affordable/uncapped actions, or `none`.
82
+ */
83
+ export declare function queryActions<G extends GameTypes = DefaultGameTypes>(ctx: StepContext<G>, registry: ActionRegistry<G>, target: ActionTarget): ActionQuery;
84
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ import { composePatches } from './patches.js';
2
+ import { toLocKey } from './coords.js';
3
+ /** Build a registry from a flat list (last definition per id wins). */
4
+ export function indexActions(defs) {
5
+ const out = {};
6
+ for (const def of defs)
7
+ out[def.id] = def;
8
+ return out;
9
+ }
10
+ /** Structural target equality (locking / availability, ENG-51). */
11
+ export function isActionTargetEqual(a, b) {
12
+ if (a.kind !== b.kind)
13
+ return false;
14
+ switch (a.kind) {
15
+ case 'tile': {
16
+ const t = b;
17
+ return (a.scene === t.scene &&
18
+ a.layer === t.layer &&
19
+ toLocKey(a.loc) === toLocKey(t.loc));
20
+ }
21
+ case 'entity':
22
+ return a.id === b.id;
23
+ case 'abstract':
24
+ return a.key === b.key;
25
+ case 'global':
26
+ return true;
27
+ }
28
+ }
29
+ /** Discrete targets (tile/entity/abstract) lock; `global` is never lockable. */
30
+ function targetLockedBy(queue, target) {
31
+ if (target.kind === 'global')
32
+ return null;
33
+ for (const a of queue) {
34
+ if (isActionTargetEqual(a.target, target))
35
+ return a.actionId;
36
+ }
37
+ return null;
38
+ }
39
+ function costOf(def, ctx, target) {
40
+ return def.cost ? def.cost(ctx, target) : {};
41
+ }
42
+ function affordable(cost, ctx) {
43
+ for (const k of Object.keys(cost)) {
44
+ if ((ctx.run.resources[k] ?? 0) < cost[k])
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ function concurrencyExceeded(def, ctx) {
50
+ if (def.maxConcurrent === undefined)
51
+ return false;
52
+ let n = 0;
53
+ for (const a of ctx.run.actions)
54
+ if (a.actionId === def.id)
55
+ n++;
56
+ return n >= def.maxConcurrent;
57
+ }
58
+ function negate(cost) {
59
+ const out = {};
60
+ for (const k of Object.keys(cost))
61
+ out[k] = -cost[k];
62
+ return out;
63
+ }
64
+ /**
65
+ * Validate one invocation and, if valid, produce the transaction to commit.
66
+ * Rejection order mirrors ENG-48: unknown → ineligible → locked → concurrency →
67
+ * cost. Nothing here mutates state (ENG-58, rejection before commit).
68
+ */
69
+ export function evaluateAction(ctx, registry, actionId, target) {
70
+ const def = registry[actionId];
71
+ if (!def)
72
+ return { ok: false, reason: 'unknown' };
73
+ if (def.targetType !== target.kind)
74
+ return { ok: false, reason: 'ineligible' };
75
+ if (def.eligibility && !def.eligibility(ctx, target)) {
76
+ return { ok: false, reason: 'ineligible' };
77
+ }
78
+ const lockedBy = targetLockedBy(ctx.run.actions, target);
79
+ if (lockedBy)
80
+ return { ok: false, reason: 'locked', lockedBy };
81
+ if (concurrencyExceeded(def, ctx))
82
+ return { ok: false, reason: 'concurrency' };
83
+ const cost = costOf(def, ctx, target);
84
+ if (!affordable(cost, ctx))
85
+ return { ok: false, reason: 'cost' };
86
+ const costPatch = Object.keys(cost).length > 0 ? { resources: negate(cost) } : {};
87
+ const duration = def.duration ?? 0;
88
+ if (duration > 0) {
89
+ const inProgress = {
90
+ actionId,
91
+ target,
92
+ startedStep: ctx.run.step,
93
+ duration,
94
+ remaining: duration,
95
+ };
96
+ const patch = composePatches([
97
+ costPatch,
98
+ { actions: [...ctx.run.actions, inProgress] },
99
+ ]);
100
+ return { ok: true, transaction: { patch } };
101
+ }
102
+ const resolved = def.resolve(ctx, target);
103
+ const patch = composePatches([resolved.patch, costPatch]);
104
+ return {
105
+ ok: true,
106
+ transaction: resolved.effects && resolved.effects.length > 0
107
+ ? { patch, effects: resolved.effects }
108
+ : { patch },
109
+ };
110
+ }
111
+ /**
112
+ * Advance the durative queue by one step: decrement each remaining, resolve any
113
+ * that hit zero (and drop them), keep the rest (ENG-49). Pure — the engine folds
114
+ * the returned `actions` and `transactions` into the step's atomic commit.
115
+ */
116
+ export function tickActions(ctx, registry) {
117
+ const actions = [];
118
+ const transactions = [];
119
+ for (const a of ctx.run.actions) {
120
+ const remaining = a.remaining - 1;
121
+ if (remaining <= 0) {
122
+ const def = registry[a.actionId];
123
+ if (def)
124
+ transactions.push(def.resolve(ctx, a.target));
125
+ }
126
+ else {
127
+ actions.push({ ...a, remaining });
128
+ }
129
+ }
130
+ return { actions, transactions };
131
+ }
132
+ /**
133
+ * Cancel the first in-progress action matching `predicate`, refunding its cost
134
+ * iff the definition is refundable (ENG-50). Returns `null` when none match.
135
+ */
136
+ export function cancelAction(ctx, registry, predicate) {
137
+ const idx = ctx.run.actions.findIndex(predicate);
138
+ if (idx < 0)
139
+ return null;
140
+ const a = ctx.run.actions[idx];
141
+ const actions = ctx.run.actions.filter((_, i) => i !== idx);
142
+ const def = registry[a.actionId];
143
+ let patch = { actions };
144
+ if (def?.refundOnCancel && def.cost) {
145
+ patch = composePatches([patch, { resources: def.cost(ctx, a.target) }]);
146
+ }
147
+ return { patch };
148
+ }
149
+ /**
150
+ * Availability for a target (ENG-51): `locked` (with the locking action) takes
151
+ * precedence; otherwise the eligible/affordable/uncapped actions, or `none`.
152
+ */
153
+ export function queryActions(ctx, registry, target) {
154
+ const lockedBy = targetLockedBy(ctx.run.actions, target);
155
+ if (lockedBy)
156
+ return { kind: 'locked', by: lockedBy };
157
+ const actions = [];
158
+ for (const def of Object.values(registry)) {
159
+ if (def.targetType !== target.kind)
160
+ continue;
161
+ if (def.eligibility && !def.eligibility(ctx, target))
162
+ continue;
163
+ if (concurrencyExceeded(def, ctx))
164
+ continue;
165
+ if (!affordable(costOf(def, ctx, target), ctx))
166
+ continue;
167
+ actions.push(def.id);
168
+ }
169
+ return actions.length > 0 ? { kind: 'available', actions } : { kind: 'none' };
170
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Game-definition assembler (ENG-69, ENG-73).
3
+ *
4
+ * The kernel ({@link createEngine}) runs on a low-level {@link EngineConfig}
5
+ * (a concrete `initialRun`, producers, events, actions). A {@link GameDefinition}
6
+ * is the *authoring* surface: a profile plus content (scenes, tile palette,
7
+ * optional onStep/actions/events/modes). This module is the missing glue that
8
+ * compiles the authoring surface into a runnable config, so a host can mount a
9
+ * game from its declaration alone.
10
+ *
11
+ * It is **platform-agnostic** (no React, React Native, or DOM imports), so the
12
+ * same assembler powers the web host (storybook) and the native host (ENG-75).
13
+ */
14
+ import type { DefaultGameTypes, GameDefinition, GameTypes, ProfileState, RunState, StorageAdapter } from './types.js';
15
+ import { type Engine, type EngineConfig } from './engine.js';
16
+ import type { PersistenceKeys } from './persistence.js';
17
+ /** Tuning a host supplies when assembling a definition (all optional). */
18
+ export interface AssembleOptions<G extends GameTypes = DefaultGameTypes> {
19
+ /** Seed for scene generation + the run RNG (default 1; ENG-61). */
20
+ seed?: number;
21
+ /** Profile-scope state fed to {@link GameDefinition.initialRun} (ENG-5). */
22
+ profile?: ProfileState<G>;
23
+ /** Injectable persistence backend; no-op without one (ENG-64). */
24
+ storage?: StorageAdapter;
25
+ /** Per-scope storage keys (ENG-65). */
26
+ persistenceKeys?: PersistenceKeys;
27
+ }
28
+ /**
29
+ * Build the canonical {@link RunState} for a definition: every scene constructed
30
+ * from its spec (a generator scene draws from the shared seeded RNG, ENG-24), the
31
+ * initial mode stack, and the seeded resources/flags/vars. A game-supplied
32
+ * {@link GameDefinition.initialRun} hook shallow-overrides the result (ENG-5).
33
+ */
34
+ export declare function buildInitialRun<G extends GameTypes = DefaultGameTypes>(def: GameDefinition<G>, options?: AssembleOptions<G>): RunState<G>;
35
+ /**
36
+ * Compile a {@link GameDefinition} into a kernel {@link EngineConfig}. The single
37
+ * `onStep` hook becomes a producer (ENG-41); actions/events/autosave pass through;
38
+ * topology resolution uses the engine default (square4/square8/continuous).
39
+ */
40
+ export declare function buildEngineConfig<G extends GameTypes = DefaultGameTypes>(def: GameDefinition<G>, options?: AssembleOptions<G>): EngineConfig<G>;
41
+ /**
42
+ * Convenience: assemble a definition and create the running kernel in one call —
43
+ * the entry point a host uses to mount a game from its declaration (ENG-73).
44
+ */
45
+ export declare function createGame<G extends GameTypes = DefaultGameTypes>(def: GameDefinition<G>, options?: AssembleOptions<G>): Engine<G>;
46
+ //# sourceMappingURL=assemble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../src/assemble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,YAAY,EACZ,QAAQ,EAER,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,YAAY,EAAqB,MAAM,aAAa,CAAC;AAG9F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,0EAA0E;AAC1E,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,SAAS,GAAG,gBAAgB;IACrE,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC1B,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uCAAuC;IACvC,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,GAAG,gBAAgB,EACpE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,EACtB,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAC/B,QAAQ,CAAC,CAAC,CAAC,CAyBb;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,SAAS,GAAG,gBAAgB,EACtE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,EACtB,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAC/B,YAAY,CAAC,CAAC,CAAC,CAYjB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,GAAG,gBAAgB,EAC/D,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,EACtB,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAC/B,MAAM,CAAC,CAAC,CAAC,CAEX"}
@@ -0,0 +1,68 @@
1
+ import { createEngine } from './engine.js';
2
+ import { buildScene } from './scene.js';
3
+ import { createRng } from './rng.js';
4
+ const emptyProfile = () => ({
5
+ unlocks: {},
6
+ stats: {},
7
+ meta: {},
8
+ });
9
+ /**
10
+ * Build the canonical {@link RunState} for a definition: every scene constructed
11
+ * from its spec (a generator scene draws from the shared seeded RNG, ENG-24), the
12
+ * initial mode stack, and the seeded resources/flags/vars. A game-supplied
13
+ * {@link GameDefinition.initialRun} hook shallow-overrides the result (ENG-5).
14
+ */
15
+ export function buildInitialRun(def, options = {}) {
16
+ const seed = options.seed ?? 1;
17
+ const rng = createRng(seed);
18
+ const scenes = {};
19
+ for (const [id, spec] of Object.entries(def.scenes)) {
20
+ scenes[id] = buildScene(id, spec, rng);
21
+ }
22
+ const base = {
23
+ activeSceneId: def.initialSceneId,
24
+ scenes,
25
+ resources: { ...(def.initialResources ?? {}) },
26
+ flags: { ...(def.initialFlags ?? {}) },
27
+ vars: (def.initialVars ?? {}),
28
+ actions: [],
29
+ eventCooldowns: {},
30
+ modeStack: def.initialModes ? [...def.initialModes] : [],
31
+ step: 0,
32
+ rngSeed: seed,
33
+ };
34
+ if (!def.initialRun)
35
+ return base;
36
+ const profile = options.profile ?? emptyProfile();
37
+ return { ...base, ...def.initialRun(profile) };
38
+ }
39
+ /**
40
+ * Compile a {@link GameDefinition} into a kernel {@link EngineConfig}. The single
41
+ * `onStep` hook becomes a producer (ENG-41); actions/events/autosave pass through;
42
+ * topology resolution uses the engine default (square4/square8/continuous).
43
+ */
44
+ export function buildEngineConfig(def, options = {}) {
45
+ const producers = def.onStep ? [def.onStep] : [];
46
+ const config = {
47
+ initialRun: buildInitialRun(def, options),
48
+ producers,
49
+ };
50
+ if (def.events)
51
+ config.events = def.events;
52
+ if (def.actions)
53
+ config.actions = def.actions;
54
+ if (def.autosaveEvery !== undefined)
55
+ config.autosaveEvery = def.autosaveEvery;
56
+ if (options.storage)
57
+ config.storage = options.storage;
58
+ if (options.persistenceKeys)
59
+ config.persistenceKeys = options.persistenceKeys;
60
+ return config;
61
+ }
62
+ /**
63
+ * Convenience: assemble a definition and create the running kernel in one call —
64
+ * the entry point a host uses to mount a game from its declaration (ENG-73).
65
+ */
66
+ export function createGame(def, options = {}) {
67
+ return createEngine(buildEngineConfig(def, options));
68
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Asset-loading conventions shared by games built on the engine.
3
+ *
4
+ * Static assets (audio, images) need two different loading strategies because
5
+ * the engine runs on two very different runtimes:
6
+ *
7
+ * - Native (Expo / React Native): assets are bundled by Metro and referenced
8
+ * with a literal `require('../assets/foo.mp3')`, which resolves to an opaque
9
+ * module id (a number) at build time.
10
+ * - Web (Expo web served behind Replit's path-based proxy): assets live in the
11
+ * app's `public/` directory and are loaded by URL. The URL must include the
12
+ * app's path prefix (e.g. `/sim-game/audio/foo.mp3`) so the proxy routes it
13
+ * to the right artifact.
14
+ *
15
+ * These helpers centralise the runtime detection, base-path derivation, and the
16
+ * require()-guard so individual games don't each reinvent them. The actual
17
+ * `require(...)` calls MUST stay in the game package with string literals —
18
+ * Metro statically analyses requires and cannot follow a require() that lives in
19
+ * a shared library or takes a variable. See `tryRequireAsset` below.
20
+ */
21
+ import type { AssetRef, AudioConfig } from './types.js';
22
+ /**
23
+ * True when running in a web browser DOM. False under React Native (no
24
+ * `document`) and false in Node/Vitest, so server-side tooling and unit tests
25
+ * take the native code path.
26
+ */
27
+ export declare function isWebRuntime(): boolean;
28
+ /**
29
+ * On web, derive the app's path prefix from the current URL (the first
30
+ * non-empty path segment, e.g. `/sim-game`). Replit serves each artifact behind
31
+ * such a prefix, and `public/` assets must be requested through it. Returns an
32
+ * empty string off-web or when the app is served from the root.
33
+ */
34
+ export declare function webBasePath(): string;
35
+ /**
36
+ * Build a web URL for a file served from the app's `public/` directory,
37
+ * prefixed with the artifact base path so Replit's proxy routes it correctly.
38
+ * `relativePath` is relative to `public/` (e.g. `audio/theme.mp3`).
39
+ */
40
+ export declare function webPublicAsset(relativePath: string): AssetRef;
41
+ /**
42
+ * Guard a literal `require()` of a bundled asset so it can't crash environments
43
+ * where the bundler isn't present (Node, Vitest, web). Pass a thunk that does
44
+ * the require with a STRING LITERAL argument:
45
+ *
46
+ * tryRequireAsset(() => require('../assets/audio/theme.mp3'))
47
+ *
48
+ * Returns the resolved asset reference, or an empty string when the require
49
+ * fails (asset missing, or running outside Metro).
50
+ */
51
+ export declare function tryRequireAsset(load: () => unknown): AssetRef;
52
+ /**
53
+ * Pick the runtime-appropriate AudioConfig: the web map (public/ URLs) in a
54
+ * browser, the native map (bundled requires) everywhere else. Keeps the
55
+ * isWebRuntime() branch out of each game's audio setup.
56
+ */
57
+ export declare function buildAudioConfig(maps: {
58
+ web: AudioConfig;
59
+ native: AudioConfig;
60
+ }): AudioConfig;
61
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAIpC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAG7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,GAAG,QAAQ,CAQ7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG,WAAW,CAE7F"}
package/dist/assets.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * True when running in a web browser DOM. False under React Native (no
3
+ * `document`) and false in Node/Vitest, so server-side tooling and unit tests
4
+ * take the native code path.
5
+ */
6
+ export function isWebRuntime() {
7
+ return typeof document !== 'undefined';
8
+ }
9
+ /**
10
+ * On web, derive the app's path prefix from the current URL (the first
11
+ * non-empty path segment, e.g. `/sim-game`). Replit serves each artifact behind
12
+ * such a prefix, and `public/` assets must be requested through it. Returns an
13
+ * empty string off-web or when the app is served from the root.
14
+ */
15
+ export function webBasePath() {
16
+ if (!isWebRuntime() || typeof window === 'undefined')
17
+ return '';
18
+ const segment = window.location.pathname.split('/').find((s) => s.length > 0);
19
+ return segment ? `/${segment}` : '';
20
+ }
21
+ /**
22
+ * Build a web URL for a file served from the app's `public/` directory,
23
+ * prefixed with the artifact base path so Replit's proxy routes it correctly.
24
+ * `relativePath` is relative to `public/` (e.g. `audio/theme.mp3`).
25
+ */
26
+ export function webPublicAsset(relativePath) {
27
+ const clean = relativePath.replace(/^\/+/, '');
28
+ return `${webBasePath()}/${clean}`;
29
+ }
30
+ /**
31
+ * Guard a literal `require()` of a bundled asset so it can't crash environments
32
+ * where the bundler isn't present (Node, Vitest, web). Pass a thunk that does
33
+ * the require with a STRING LITERAL argument:
34
+ *
35
+ * tryRequireAsset(() => require('../assets/audio/theme.mp3'))
36
+ *
37
+ * Returns the resolved asset reference, or an empty string when the require
38
+ * fails (asset missing, or running outside Metro).
39
+ */
40
+ export function tryRequireAsset(load) {
41
+ try {
42
+ const ref = load();
43
+ if (typeof ref === 'number' || typeof ref === 'string')
44
+ return ref;
45
+ return '';
46
+ }
47
+ catch {
48
+ return '';
49
+ }
50
+ }
51
+ /**
52
+ * Pick the runtime-appropriate AudioConfig: the web map (public/ URLs) in a
53
+ * browser, the native map (bundled requires) everywhere else. Keeps the
54
+ * isWebRuntime() branch out of each game's audio setup.
55
+ */
56
+ export function buildAudioConfig(maps) {
57
+ return isWebRuntime() ? maps.web : maps.native;
58
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Audio abstraction for the engine.
3
+ *
4
+ * The engine is platform-agnostic: it does NOT depend on Expo or any concrete
5
+ * audio backend. The consuming app provides an implementation of `AudioPlayer`
6
+ * (e.g. an expo-av-backed player) and injects it where the engine is mounted.
7
+ * A no-op player is used by default so the engine runs headless (tests, SSR).
8
+ */
9
+ export interface AudioPlayer {
10
+ preload(): Promise<void>;
11
+ playMusic(key: string): Promise<void>;
12
+ stopMusic(): Promise<void>;
13
+ playSfx(key: string): Promise<void>;
14
+ setMusicVolume(vol: number): void;
15
+ setSfxVolume(vol: number): void;
16
+ unload(): Promise<void>;
17
+ }
18
+ /** A silent AudioPlayer — used when no concrete implementation is injected. */
19
+ export declare function createNoopAudioPlayer(): AudioPlayer;
20
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,IAAI,WAAW,CAUnD"}
package/dist/audio.js ADDED
@@ -0,0 +1,12 @@
1
+ /** A silent AudioPlayer — used when no concrete implementation is injected. */
2
+ export function createNoopAudioPlayer() {
3
+ return {
4
+ async preload() { },
5
+ async playMusic() { },
6
+ async stopMusic() { },
7
+ async playSfx() { },
8
+ setMusicVolume() { },
9
+ setSfxVolume() { },
10
+ async unload() { },
11
+ };
12
+ }