@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/actions.d.ts +84 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +170 -0
- package/dist/assemble.d.ts +46 -0
- package/dist/assemble.d.ts.map +1 -0
- package/dist/assemble.js +68 -0
- package/dist/assets.d.ts +61 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +58 -0
- package/dist/audio.d.ts +20 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +12 -0
- package/dist/camera.d.ts +54 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +51 -0
- package/dist/clock.d.ts +41 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +26 -0
- package/dist/coords.d.ts +16 -0
- package/dist/coords.d.ts.map +1 -0
- package/dist/coords.js +17 -0
- package/dist/engine.d.ts +89 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +140 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +41 -0
- package/dist/helpers.d.ts +41 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/modes.d.ts +44 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +69 -0
- package/dist/movement.d.ts +77 -0
- package/dist/movement.d.ts.map +1 -0
- package/dist/movement.js +135 -0
- package/dist/patches.d.ts +29 -0
- package/dist/patches.d.ts.map +1 -0
- package/dist/patches.js +163 -0
- package/dist/persistence.d.ts +31 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +32 -0
- package/dist/profiles.d.ts +37 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +52 -0
- package/dist/resolver.d.ts +62 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +105 -0
- package/dist/rng.d.ts +7 -0
- package/dist/rng.d.ts.map +1 -0
- package/dist/rng.js +20 -0
- package/dist/scene.d.ts +18 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +58 -0
- package/dist/scheduler.d.ts +63 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +77 -0
- package/dist/stubs.d.ts +54 -0
- package/dist/stubs.d.ts.map +1 -0
- package/dist/stubs.js +8 -0
- package/dist/testing.d.ts +65 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +67 -0
- package/dist/tools/assetManifest.d.ts +12 -0
- package/dist/tools/assetManifest.d.ts.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/validator.d.ts +21 -0
- package/dist/tools/validator.d.ts.map +1 -0
- package/dist/tools/validator.js +12 -0
- package/dist/tools/worldBuilder.d.ts +22 -0
- package/dist/tools/worldBuilder.d.ts.map +1 -0
- package/dist/topology.d.ts +36 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/topology.js +132 -0
- package/dist/transitions.d.ts +27 -0
- package/dist/transitions.d.ts.map +1 -0
- package/dist/transitions.js +31 -0
- package/dist/types.d.ts +487 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/web/GameHost.d.ts +110 -0
- package/dist/web/GameHost.d.ts.map +1 -0
- package/dist/web/GameHost.js +472 -0
- package/dist/web/index.d.ts +14 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +11 -0
- package/dist/web/sfx.d.ts +41 -0
- package/dist/web/sfx.d.ts.map +1 -0
- package/dist/web/sfx.js +108 -0
- package/dist/world.d.ts +13 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/zone.d.ts +6 -0
- package/dist/zone.d.ts.map +1 -0
- 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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"}
|
package/dist/actions.js
ADDED
|
@@ -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"}
|
package/dist/assemble.js
ADDED
|
@@ -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
|
+
}
|
package/dist/assets.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/audio.d.ts
ADDED
|
@@ -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
|
+
}
|