koota-kit 0.1.1
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 +28 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/SECURITY.md +25 -0
- package/dist/cjs/eventLog.cjs +81 -0
- package/dist/cjs/eventLog.d.cts +49 -0
- package/dist/cjs/eventLog.d.cts.map +1 -0
- package/dist/cjs/index.cjs +36 -0
- package/dist/cjs/index.d.cts +9 -0
- package/dist/cjs/index.d.cts.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/rng.cjs +168 -0
- package/dist/cjs/rng.d.cts +86 -0
- package/dist/cjs/rng.d.cts.map +1 -0
- package/dist/cjs/traits/index.cjs +54 -0
- package/dist/cjs/traits/index.d.cts +48 -0
- package/dist/cjs/traits/index.d.cts.map +1 -0
- package/dist/cjs/world.cjs +111 -0
- package/dist/cjs/world.d.cts +83 -0
- package/dist/cjs/world.d.cts.map +1 -0
- package/dist/esm/eventLog.d.ts +49 -0
- package/dist/esm/eventLog.d.ts.map +1 -0
- package/dist/esm/eventLog.js +78 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/rng.d.ts +86 -0
- package/dist/esm/rng.d.ts.map +1 -0
- package/dist/esm/rng.js +154 -0
- package/dist/esm/traits/index.d.ts +48 -0
- package/dist/esm/traits/index.d.ts.map +1 -0
- package/dist/esm/traits/index.js +51 -0
- package/dist/esm/world.d.ts +83 -0
- package/dist/esm/world.d.ts.map +1 -0
- package/dist/esm/world.js +101 -0
- package/docs/API.md +119 -0
- package/docs/ARCHITECTURE.md +57 -0
- package/docs/assets/favicon.svg +1 -0
- package/docs/assets/koota-kit-hero.webp +0 -0
- package/docs/changelog.md +13 -0
- package/docs/contributing.md +28 -0
- package/examples/basic.mjs +27 -0
- package/examples/commonjs.cjs +8 -0
- package/package.json +173 -0
package/dist/cjs/rng.cjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Dual-layer deterministic PRNG (seedrandom-backed).
|
|
3
|
+
//
|
|
4
|
+
// `gen` — worldgen / layouts / spawn locations / map composition / one-time
|
|
5
|
+
// seeded decisions. Replays a world reliably from the gen seed alone, even
|
|
6
|
+
// when play diverges. Snapshot it once at world creation and you can
|
|
7
|
+
// re-derive the same world forever.
|
|
8
|
+
//
|
|
9
|
+
// `events` — per-tick runtime decisions (weather rolls, event triggers, mood
|
|
10
|
+
// drift, combat dice). Diverges based on play history. Snapshot/restore
|
|
11
|
+
// independently so a worldgen replay doesn't carry play state with it.
|
|
12
|
+
//
|
|
13
|
+
// Why two streams: a single stream couples worldgen to play, so seeding a
|
|
14
|
+
// "same map, fresh playthrough" scenario requires you to either re-derive
|
|
15
|
+
// worldgen mid-restore (fragile) or snapshot the stream at the exact tick
|
|
16
|
+
// worldgen finished (brittle to system ordering). Two streams: snapshot gen
|
|
17
|
+
// once, never touch it again; snapshot events on demand. Each layer's
|
|
18
|
+
// determinism contract is independent.
|
|
19
|
+
//
|
|
20
|
+
// Forbidden in sim code: `Math.random()`. Use `nextFloat(rng.gen)` or
|
|
21
|
+
// `nextFloat(rng.events)` depending on the layer.
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createRng = createRng;
|
|
27
|
+
exports.nextU32 = nextU32;
|
|
28
|
+
exports.nextFloat = nextFloat;
|
|
29
|
+
exports.nextInt = nextInt;
|
|
30
|
+
exports.chance = chance;
|
|
31
|
+
exports.snapshotStream = snapshotStream;
|
|
32
|
+
exports.snapshotLayers = snapshotLayers;
|
|
33
|
+
exports.restoreStream = restoreStream;
|
|
34
|
+
exports.restoreLayers = restoreLayers;
|
|
35
|
+
const seedrandom_1 = __importDefault(require("seedrandom"));
|
|
36
|
+
function assertSeed(seed, name) {
|
|
37
|
+
if (typeof seed !== "string" && typeof seed !== "number") {
|
|
38
|
+
throw new TypeError(`createRng: seeds.${name} must be a string or finite number.`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof seed === "number" && !Number.isFinite(seed)) {
|
|
41
|
+
throw new RangeError(`createRng: seeds.${name} must be finite; received ${String(seed)}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function makeStream(seed) {
|
|
45
|
+
const prng = (0, seedrandom_1.default)(String(seed), { state: true });
|
|
46
|
+
return { prng };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create both RNG layers from their seeds. Called once at world creation
|
|
50
|
+
* (see `createSimWorld` in `./world`) — `gen` and `events` are independent
|
|
51
|
+
* seedrandom instances from this point on, so consuming one never perturbs
|
|
52
|
+
* the other's sequence.
|
|
53
|
+
*/
|
|
54
|
+
function createRng(seeds) {
|
|
55
|
+
if (typeof seeds !== "object" || seeds === null) {
|
|
56
|
+
throw new TypeError("createRng: seeds must be an object with gen and events values.");
|
|
57
|
+
}
|
|
58
|
+
assertSeed(seeds.gen, "gen");
|
|
59
|
+
assertSeed(seeds.events, "events");
|
|
60
|
+
return {
|
|
61
|
+
gen: makeStream(seeds.gen),
|
|
62
|
+
events: makeStream(seeds.events),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Draw the next raw unsigned 32-bit integer from `stream` (range
|
|
67
|
+
* `[0, 2**32)`). The lowest-level draw primitive — `nextFloat`, `nextInt`,
|
|
68
|
+
* and `chance` are all expressed in terms of `quick()`, not this function;
|
|
69
|
+
* reach for `nextU32` directly only when you need the full 32 bits of
|
|
70
|
+
* entropy (e.g. seeding another PRNG, hashing).
|
|
71
|
+
*/
|
|
72
|
+
function nextU32(stream) {
|
|
73
|
+
return stream.prng.int32() >>> 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Draw the next float from `stream` in the half-open range `[0, 1)`. The
|
|
77
|
+
* primitive every other draw helper in this module (`nextInt`, `chance`)
|
|
78
|
+
* is built on.
|
|
79
|
+
*/
|
|
80
|
+
function nextFloat(stream) {
|
|
81
|
+
return stream.prng.quick();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Draw a random integer from `stream` in the half-open range
|
|
85
|
+
* `[minInclusive, maxExclusive)`. Bounds must be safe integers, the range must
|
|
86
|
+
* be non-empty and no larger than `2**32`, and valid calls consume exactly one
|
|
87
|
+
* `nextFloat` draw. Invalid calls do not consume the stream.
|
|
88
|
+
*/
|
|
89
|
+
function nextInt(stream, minInclusive, maxExclusive) {
|
|
90
|
+
if (!Number.isSafeInteger(minInclusive) || !Number.isSafeInteger(maxExclusive)) {
|
|
91
|
+
throw new TypeError("nextInt: bounds must be safe integers.");
|
|
92
|
+
}
|
|
93
|
+
if (maxExclusive <= minInclusive) {
|
|
94
|
+
throw new RangeError(`nextInt: maxExclusive (${maxExclusive}) must be greater than minInclusive (${minInclusive}).`);
|
|
95
|
+
}
|
|
96
|
+
if (maxExclusive - minInclusive > 2 ** 32) {
|
|
97
|
+
throw new RangeError("nextInt: the requested range cannot exceed 2**32 values.");
|
|
98
|
+
}
|
|
99
|
+
const span = maxExclusive - minInclusive;
|
|
100
|
+
return minInclusive + Math.floor(nextFloat(stream) * span);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Roll a boolean weighted by finite probability `p` in `[0, 1]` (`0` never
|
|
104
|
+
* true, `1` always true). Valid calls consume exactly one `nextFloat` draw
|
|
105
|
+
* from `stream`, so calling `chance` where a system might otherwise skip a
|
|
106
|
+
* draw entirely will shift every later draw on that stream — keep draw counts
|
|
107
|
+
* deterministic across codepaths that must replay identically.
|
|
108
|
+
*/
|
|
109
|
+
function chance(stream, p) {
|
|
110
|
+
if (!Number.isFinite(p) || p < 0 || p > 1) {
|
|
111
|
+
throw new RangeError(`chance: probability must be a finite number in [0, 1]; received ${p}.`);
|
|
112
|
+
}
|
|
113
|
+
return nextFloat(stream) < p;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Capture `stream`'s internal seedrandom state as a plain, JSON-serializable
|
|
117
|
+
* object (deep-cloned, so later draws on `stream` cannot mutate the
|
|
118
|
+
* snapshot). Restoring it with `restoreStream` reproduces every subsequent
|
|
119
|
+
* draw byte-exact.
|
|
120
|
+
*/
|
|
121
|
+
function snapshotStream(stream) {
|
|
122
|
+
const state = stream.prng.state();
|
|
123
|
+
return { state: { i: state.i, j: state.j, S: [...state.S] } };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Snapshot both RNG layers at once. See `snapshotStream` — this just applies
|
|
127
|
+
* it to `gen` and `events` independently and returns both.
|
|
128
|
+
*/
|
|
129
|
+
function snapshotLayers(rng) {
|
|
130
|
+
return { gen: snapshotStream(rng.gen), events: snapshotStream(rng.events) };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Rebuild an `RngStream` from a snapshot taken by `snapshotStream`. The next
|
|
134
|
+
* draw on the returned stream reproduces the exact next draw the snapshotted
|
|
135
|
+
* stream would have produced — the seed string passed to `seedrandom` here
|
|
136
|
+
* is irrelevant because `{ state: snap.state }` fully overrides it. Malformed
|
|
137
|
+
* persisted state is rejected before seedrandom sees it.
|
|
138
|
+
*/
|
|
139
|
+
function restoreStream(snap) {
|
|
140
|
+
const state = snap?.state;
|
|
141
|
+
if (!state ||
|
|
142
|
+
!Number.isInteger(state.i) ||
|
|
143
|
+
state.i < 0 ||
|
|
144
|
+
state.i > 255 ||
|
|
145
|
+
!Number.isInteger(state.j) ||
|
|
146
|
+
state.j < 0 ||
|
|
147
|
+
state.j > 255 ||
|
|
148
|
+
!Array.isArray(state.S) ||
|
|
149
|
+
state.S.length !== 256 ||
|
|
150
|
+
state.S.some((value) => !Number.isInteger(value) || value < 0 || value > 255) ||
|
|
151
|
+
new Set(state.S).size !== 256) {
|
|
152
|
+
throw new TypeError("restoreStream: snapshot must contain a valid seedrandom ARC4 state.");
|
|
153
|
+
}
|
|
154
|
+
// Clone the permutation so the same immutable snapshot can safely seed more
|
|
155
|
+
// than one replay, even if seedrandom changes its internal copy behavior.
|
|
156
|
+
const prng = (0, seedrandom_1.default)("", { state: { i: state.i, j: state.j, S: [...state.S] } });
|
|
157
|
+
return { prng };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Rebuild both RNG layers from a snapshot taken by `snapshotLayers`. See
|
|
161
|
+
* `restoreStream` — applied independently to `gen` and `events`.
|
|
162
|
+
*/
|
|
163
|
+
function restoreLayers(snap) {
|
|
164
|
+
if (typeof snap !== "object" || snap === null) {
|
|
165
|
+
throw new TypeError("restoreLayers: snapshot must contain gen and events stream states.");
|
|
166
|
+
}
|
|
167
|
+
return { gen: restoreStream(snap.gen), events: restoreStream(snap.events) };
|
|
168
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import seedrandom from "seedrandom";
|
|
2
|
+
type State = seedrandom.State.Arc4;
|
|
3
|
+
type Stateful = seedrandom.StatefulPRNG<State>;
|
|
4
|
+
export type RngStream = {
|
|
5
|
+
prng: Stateful;
|
|
6
|
+
};
|
|
7
|
+
export type RngLayers = {
|
|
8
|
+
gen: RngStream;
|
|
9
|
+
events: RngStream;
|
|
10
|
+
};
|
|
11
|
+
export type RngSeeds = {
|
|
12
|
+
/** World-generation seed (immutable, defines the world's identity). */
|
|
13
|
+
readonly gen: string | number;
|
|
14
|
+
/** Per-playthrough events seed (changes per cycle for new playthroughs of the same world). */
|
|
15
|
+
readonly events: string | number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Create both RNG layers from their seeds. Called once at world creation
|
|
19
|
+
* (see `createSimWorld` in `./world`) — `gen` and `events` are independent
|
|
20
|
+
* seedrandom instances from this point on, so consuming one never perturbs
|
|
21
|
+
* the other's sequence.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRng(seeds: RngSeeds): RngLayers;
|
|
24
|
+
/**
|
|
25
|
+
* Draw the next raw unsigned 32-bit integer from `stream` (range
|
|
26
|
+
* `[0, 2**32)`). The lowest-level draw primitive — `nextFloat`, `nextInt`,
|
|
27
|
+
* and `chance` are all expressed in terms of `quick()`, not this function;
|
|
28
|
+
* reach for `nextU32` directly only when you need the full 32 bits of
|
|
29
|
+
* entropy (e.g. seeding another PRNG, hashing).
|
|
30
|
+
*/
|
|
31
|
+
export declare function nextU32(stream: RngStream): number;
|
|
32
|
+
/**
|
|
33
|
+
* Draw the next float from `stream` in the half-open range `[0, 1)`. The
|
|
34
|
+
* primitive every other draw helper in this module (`nextInt`, `chance`)
|
|
35
|
+
* is built on.
|
|
36
|
+
*/
|
|
37
|
+
export declare function nextFloat(stream: RngStream): number;
|
|
38
|
+
/**
|
|
39
|
+
* Draw a random integer from `stream` in the half-open range
|
|
40
|
+
* `[minInclusive, maxExclusive)`. Bounds must be safe integers, the range must
|
|
41
|
+
* be non-empty and no larger than `2**32`, and valid calls consume exactly one
|
|
42
|
+
* `nextFloat` draw. Invalid calls do not consume the stream.
|
|
43
|
+
*/
|
|
44
|
+
export declare function nextInt(stream: RngStream, minInclusive: number, maxExclusive: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* Roll a boolean weighted by finite probability `p` in `[0, 1]` (`0` never
|
|
47
|
+
* true, `1` always true). Valid calls consume exactly one `nextFloat` draw
|
|
48
|
+
* from `stream`, so calling `chance` where a system might otherwise skip a
|
|
49
|
+
* draw entirely will shift every later draw on that stream — keep draw counts
|
|
50
|
+
* deterministic across codepaths that must replay identically.
|
|
51
|
+
*/
|
|
52
|
+
export declare function chance(stream: RngStream, p: number): boolean;
|
|
53
|
+
export type RngStreamSnapshot = {
|
|
54
|
+
state: State;
|
|
55
|
+
};
|
|
56
|
+
export type RngLayersSnapshot = {
|
|
57
|
+
gen: RngStreamSnapshot;
|
|
58
|
+
events: RngStreamSnapshot;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Capture `stream`'s internal seedrandom state as a plain, JSON-serializable
|
|
62
|
+
* object (deep-cloned, so later draws on `stream` cannot mutate the
|
|
63
|
+
* snapshot). Restoring it with `restoreStream` reproduces every subsequent
|
|
64
|
+
* draw byte-exact.
|
|
65
|
+
*/
|
|
66
|
+
export declare function snapshotStream(stream: RngStream): RngStreamSnapshot;
|
|
67
|
+
/**
|
|
68
|
+
* Snapshot both RNG layers at once. See `snapshotStream` — this just applies
|
|
69
|
+
* it to `gen` and `events` independently and returns both.
|
|
70
|
+
*/
|
|
71
|
+
export declare function snapshotLayers(rng: RngLayers): RngLayersSnapshot;
|
|
72
|
+
/**
|
|
73
|
+
* Rebuild an `RngStream` from a snapshot taken by `snapshotStream`. The next
|
|
74
|
+
* draw on the returned stream reproduces the exact next draw the snapshotted
|
|
75
|
+
* stream would have produced — the seed string passed to `seedrandom` here
|
|
76
|
+
* is irrelevant because `{ state: snap.state }` fully overrides it. Malformed
|
|
77
|
+
* persisted state is rejected before seedrandom sees it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function restoreStream(snap: RngStreamSnapshot): RngStream;
|
|
80
|
+
/**
|
|
81
|
+
* Rebuild both RNG layers from a snapshot taken by `snapshotLayers`. See
|
|
82
|
+
* `restoreStream` — applied independently to `gen` and `events`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function restoreLayers(snap: RngLayersSnapshot): RngLayers;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=rng.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/rng.ts"],"names":[],"mappings":"AAqBA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,KAAK,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;AACnC,KAAK,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAUpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAc7F;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,iBAAiB,CAGnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,iBAAiB,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAqBhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,CAKhE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
|
|
4
|
+
*
|
|
5
|
+
* Packaging pattern (from medieval-hexagon-gameboard): trait declarations
|
|
6
|
+
* live in a leaf sub-package with ZERO sibling imports — this file imports
|
|
7
|
+
* only `koota` (the npm dep) and pure types, never runtime values from
|
|
8
|
+
* sibling modules like ./world or ./rng. That is what prevents the
|
|
9
|
+
* koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
|
|
10
|
+
* flat layout only dodges accidentally. Games adopting this package should
|
|
11
|
+
* follow the same rule: declare traits in their own leaf module, exported
|
|
12
|
+
* from exactly one location so trait reference-identity holds regardless of
|
|
13
|
+
* which subpath a consumer imports from.
|
|
14
|
+
*
|
|
15
|
+
* The AoS-aliasing footgun this module guards against (timber-town's
|
|
16
|
+
* documented convention): object-valued fields in a SoA schema must use the
|
|
17
|
+
* AoS-factory pattern (`() => ({...})`) so each entity gets its own object
|
|
18
|
+
* instance — koota's SoA pattern would otherwise alias state across
|
|
19
|
+
* entities (one shared object literal mutated by every entity that has the
|
|
20
|
+
* trait).
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.defineTrait = defineTrait;
|
|
26
|
+
const koota_1 = require("koota");
|
|
27
|
+
/**
|
|
28
|
+
* Thin wrapper over koota's `trait()` that enforces the AoS-factory
|
|
29
|
+
* convention for object-valued fields, at both type level (see SafeSchema)
|
|
30
|
+
* and runtime (throws with an explanation instead of silently aliasing).
|
|
31
|
+
*
|
|
32
|
+
* Accepted forms:
|
|
33
|
+
* defineTrait({ count: 0, label: "x" }) // SoA, primitives
|
|
34
|
+
* defineTrait({ pos: () => ({ x: 0, y: 0 }) }) // SoA, per-field factory
|
|
35
|
+
* defineTrait(() => ({ pos: { x: 0, y: 0 } })) // whole-trait AoS factory
|
|
36
|
+
*
|
|
37
|
+
* Rejected (the footgun):
|
|
38
|
+
* defineTrait({ pos: { x: 0, y: 0 } }) // shared literal → aliased
|
|
39
|
+
*/
|
|
40
|
+
function defineTrait(schema) {
|
|
41
|
+
if (typeof schema !== "function") {
|
|
42
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
43
|
+
if (value !== null && typeof value === "object") {
|
|
44
|
+
throw new TypeError(`defineTrait: field "${key}" is a bare object literal. koota's SoA layout ` +
|
|
45
|
+
`would share this single instance across every entity holding the trait ` +
|
|
46
|
+
`(state aliasing). Use an AoS factory instead: ${key}: () => ({...}).`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Pin Koota's normalization generic to the caller's schema. Passing the
|
|
51
|
+
// guard intersection through inference makes Koota 0.6.x normalize
|
|
52
|
+
// `S & SafeSchema<S>` and produces a narrower, incompatible Trait type.
|
|
53
|
+
return (0, koota_1.trait)(schema);
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `src/traits/` — trait-authoring conventions, kept in a LEAF sub-package.
|
|
3
|
+
*
|
|
4
|
+
* Packaging pattern (from medieval-hexagon-gameboard): trait declarations
|
|
5
|
+
* live in a leaf sub-package with ZERO sibling imports — this file imports
|
|
6
|
+
* only `koota` (the npm dep) and pure types, never runtime values from
|
|
7
|
+
* sibling modules like ./world or ./rng. That is what prevents the
|
|
8
|
+
* koota ↔ world-facade ↔ game-scenario top-level evaluation cycle that a
|
|
9
|
+
* flat layout only dodges accidentally. Games adopting this package should
|
|
10
|
+
* follow the same rule: declare traits in their own leaf module, exported
|
|
11
|
+
* from exactly one location so trait reference-identity holds regardless of
|
|
12
|
+
* which subpath a consumer imports from.
|
|
13
|
+
*
|
|
14
|
+
* The AoS-aliasing footgun this module guards against (timber-town's
|
|
15
|
+
* documented convention): object-valued fields in a SoA schema must use the
|
|
16
|
+
* AoS-factory pattern (`() => ({...})`) so each entity gets its own object
|
|
17
|
+
* instance — koota's SoA pattern would otherwise alias state across
|
|
18
|
+
* entities (one shared object literal mutated by every entity that has the
|
|
19
|
+
* trait).
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import type { Norm, Schema, Trait } from "koota";
|
|
24
|
+
/**
|
|
25
|
+
* Compile-time guard: any object-valued field in a SoA schema must be an
|
|
26
|
+
* AoS factory (`() => ({...})`), never a bare object literal. Fields that
|
|
27
|
+
* are functions, primitives, null, or undefined pass through unchanged;
|
|
28
|
+
* bare object/array literals map to `never`, producing a type error at the
|
|
29
|
+
* call site.
|
|
30
|
+
*/
|
|
31
|
+
export type SafeSchema<S> = S extends (...args: never[]) => unknown ? S : {
|
|
32
|
+
[K in keyof S]: S[K] extends (...args: never[]) => unknown ? S[K] : S[K] extends object ? never : S[K];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Thin wrapper over koota's `trait()` that enforces the AoS-factory
|
|
36
|
+
* convention for object-valued fields, at both type level (see SafeSchema)
|
|
37
|
+
* and runtime (throws with an explanation instead of silently aliasing).
|
|
38
|
+
*
|
|
39
|
+
* Accepted forms:
|
|
40
|
+
* defineTrait({ count: 0, label: "x" }) // SoA, primitives
|
|
41
|
+
* defineTrait({ pos: () => ({ x: 0, y: 0 }) }) // SoA, per-field factory
|
|
42
|
+
* defineTrait(() => ({ pos: { x: 0, y: 0 } })) // whole-trait AoS factory
|
|
43
|
+
*
|
|
44
|
+
* Rejected (the footgun):
|
|
45
|
+
* defineTrait({ pos: { x: 0, y: 0 } }) // shared literal → aliased
|
|
46
|
+
*/
|
|
47
|
+
export declare function defineTrait<S extends Schema>(schema: S & SafeSchema<S>): Trait<Norm<S>>;
|
|
48
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/traits/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAC/D,CAAC,GACD;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACtD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GACjB,KAAK,GACL,CAAC,CAAC,CAAC,CAAC;CACX,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAgBvF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// World facade. Wraps koota's createWorld + trait helper so the rest of a
|
|
3
|
+
// game's codebase imports from here and lifecycle/determinism conventions stay
|
|
4
|
+
// behind one boundary.
|
|
5
|
+
//
|
|
6
|
+
// Games declare their traits with `defineTrait` (see ./traits) and drive the
|
|
7
|
+
// sim through the WorldHandle: world creation, trait re-export, tick clock,
|
|
8
|
+
// dual-layer RNG plumbing.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.trait = exports.relation = exports.createActions = void 0;
|
|
11
|
+
exports.createSimWorld = createSimWorld;
|
|
12
|
+
exports.destroySimWorld = destroySimWorld;
|
|
13
|
+
exports.advanceClock = advanceClock;
|
|
14
|
+
exports.snapshotWorld = snapshotWorld;
|
|
15
|
+
exports.restoreWorldHeader = restoreWorldHeader;
|
|
16
|
+
const koota_1 = require("koota");
|
|
17
|
+
Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return koota_1.createActions; } });
|
|
18
|
+
Object.defineProperty(exports, "relation", { enumerable: true, get: function () { return koota_1.relation; } });
|
|
19
|
+
Object.defineProperty(exports, "trait", { enumerable: true, get: function () { return koota_1.trait; } });
|
|
20
|
+
const rng_js_1 = require("./rng.cjs");
|
|
21
|
+
/**
|
|
22
|
+
* Create a fresh `WorldHandle`: a new koota world, both RNG layers seeded
|
|
23
|
+
* from `seeds`, a zeroed clock, and an empty scratch map. This is the one
|
|
24
|
+
* entry point for standing up a sim — call it once per world (per game
|
|
25
|
+
* session, per test, per worldgen run) rather than constructing the koota
|
|
26
|
+
* world and RNG separately, so the facade's invariants (immutable `seeds`,
|
|
27
|
+
* clock starting at zero, scratch starting empty) always hold.
|
|
28
|
+
*/
|
|
29
|
+
function createSimWorld(seeds) {
|
|
30
|
+
// Validate before allocating a Koota world: invalid input must not consume a
|
|
31
|
+
// process-global world id or leave a world that the caller cannot destroy.
|
|
32
|
+
const rng = (0, rng_js_1.createRng)(seeds);
|
|
33
|
+
const immutableSeeds = Object.freeze({ gen: seeds.gen, events: seeds.events });
|
|
34
|
+
return {
|
|
35
|
+
world: (0, koota_1.createWorld)(),
|
|
36
|
+
rng,
|
|
37
|
+
seeds: immutableSeeds,
|
|
38
|
+
clock: { tickIndex: 0, simSeconds: 0 },
|
|
39
|
+
scratch: new Map(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Tear down a simulation handle. This clears facade-owned scratch state and
|
|
44
|
+
* releases Koota's process-global world id. The operation is idempotent, which
|
|
45
|
+
* makes it safe to call from cleanup paths that may run more than once.
|
|
46
|
+
*/
|
|
47
|
+
function destroySimWorld(handle) {
|
|
48
|
+
handle.scratch.clear();
|
|
49
|
+
if (handle.world.isInitialized) {
|
|
50
|
+
handle.world.destroy();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Advance `handle`'s clock by one fixed-timestep tick: increments
|
|
55
|
+
* `tickIndex` by 1 and accumulates `dt` seconds into `simSeconds`. Call once
|
|
56
|
+
* per sim tick, before running that tick's systems, so `clock` always
|
|
57
|
+
* reflects "how far the sim has progressed" rather than "how far it will
|
|
58
|
+
* have progressed after this tick's work."
|
|
59
|
+
*/
|
|
60
|
+
function advanceClock(handle, dt) {
|
|
61
|
+
if (!Number.isFinite(dt) || dt < 0) {
|
|
62
|
+
throw new RangeError(`advanceClock: dt must be a finite, non-negative number; received ${dt}.`);
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isSafeInteger(handle.clock.tickIndex) || handle.clock.tickIndex < 0) {
|
|
65
|
+
throw new RangeError("advanceClock: clock.tickIndex must be a non-negative safe integer.");
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isFinite(handle.clock.simSeconds) || handle.clock.simSeconds < 0) {
|
|
68
|
+
throw new RangeError("advanceClock: clock.simSeconds must be finite and non-negative.");
|
|
69
|
+
}
|
|
70
|
+
const tickIndex = handle.clock.tickIndex + 1;
|
|
71
|
+
const simSeconds = handle.clock.simSeconds + dt;
|
|
72
|
+
if (!Number.isSafeInteger(tickIndex) || !Number.isFinite(simSeconds)) {
|
|
73
|
+
throw new RangeError("advanceClock: advancing the clock would exceed its numeric range.");
|
|
74
|
+
}
|
|
75
|
+
handle.clock.tickIndex = tickIndex;
|
|
76
|
+
handle.clock.simSeconds = simSeconds;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Capture `handle`'s RNG state and clock into a plain, JSON-serializable
|
|
80
|
+
* `WorldSnapshot`. Pair with `restoreWorldHeader` for save/load — restoring
|
|
81
|
+
* a snapshot reproduces every subsequent RNG draw on both layers byte-exact
|
|
82
|
+
* and resumes the clock from the exact tick/second it was captured at.
|
|
83
|
+
*/
|
|
84
|
+
function snapshotWorld(handle) {
|
|
85
|
+
return {
|
|
86
|
+
rng: (0, rng_js_1.snapshotLayers)(handle.rng),
|
|
87
|
+
clock: { tickIndex: handle.clock.tickIndex, simSeconds: handle.clock.simSeconds },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Restore `handle`'s RNG layers and clock from a `WorldSnapshot` taken by
|
|
92
|
+
* `snapshotWorld`. Mutates `handle` in place (replaces `handle.rng` and
|
|
93
|
+
* `handle.clock` with freshly rebuilt values) — entity/component state is
|
|
94
|
+
* untouched, since the facade never captured it in the first place; restore
|
|
95
|
+
* that separately using whatever serialization your game chose.
|
|
96
|
+
*/
|
|
97
|
+
function restoreWorldHeader(handle, snap) {
|
|
98
|
+
const clock = snap?.clock;
|
|
99
|
+
if (!clock ||
|
|
100
|
+
!Number.isSafeInteger(clock.tickIndex) ||
|
|
101
|
+
clock.tickIndex < 0 ||
|
|
102
|
+
!Number.isFinite(clock.simSeconds) ||
|
|
103
|
+
clock.simSeconds < 0) {
|
|
104
|
+
throw new TypeError("restoreWorldHeader: snapshot.clock must contain a non-negative safe integer tickIndex and finite, non-negative simSeconds.");
|
|
105
|
+
}
|
|
106
|
+
// Build and validate every replacement before mutating the handle so a bad
|
|
107
|
+
// persisted snapshot cannot leave it half-restored.
|
|
108
|
+
const rng = (0, rng_js_1.restoreLayers)(snap.rng);
|
|
109
|
+
handle.rng = rng;
|
|
110
|
+
handle.clock = { tickIndex: clock.tickIndex, simSeconds: clock.simSeconds };
|
|
111
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { World } from "koota";
|
|
2
|
+
import { createActions, relation, trait } from "koota";
|
|
3
|
+
import type { RngLayers, RngLayersSnapshot, RngSeeds } from "./rng.cjs";
|
|
4
|
+
export type { Entity } from "koota";
|
|
5
|
+
export type { World };
|
|
6
|
+
export { createActions, relation, trait };
|
|
7
|
+
export type WorldHandle = {
|
|
8
|
+
readonly world: World;
|
|
9
|
+
rng: RngLayers;
|
|
10
|
+
/**
|
|
11
|
+
* The immutable seeds this world was created from. Stored so systems
|
|
12
|
+
* can derive deterministic per-entity seeds (e.g. noise keyed on an
|
|
13
|
+
* entity's stable grid coordinates) WITHOUT lazily consuming the
|
|
14
|
+
* rng.gen stream in query-iteration order — koota entity ids are not
|
|
15
|
+
* reset between worlds, so iteration-order rng draws are
|
|
16
|
+
* non-deterministic across two runs in one process. Derive from
|
|
17
|
+
* `seeds.gen`, never a lazy draw.
|
|
18
|
+
*/
|
|
19
|
+
readonly seeds: Readonly<RngSeeds>;
|
|
20
|
+
clock: {
|
|
21
|
+
tickIndex: number;
|
|
22
|
+
simSeconds: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Per-system scratch caches owned by the WorldHandle (so they lifecycle with
|
|
26
|
+
* the world, not at module scope). Systems read/write via well-known keys.
|
|
27
|
+
* Examples: per-region noise fns, pathfinding grids, behavior-tree instances.
|
|
28
|
+
*/
|
|
29
|
+
readonly scratch: Map<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Create a fresh `WorldHandle`: a new koota world, both RNG layers seeded
|
|
33
|
+
* from `seeds`, a zeroed clock, and an empty scratch map. This is the one
|
|
34
|
+
* entry point for standing up a sim — call it once per world (per game
|
|
35
|
+
* session, per test, per worldgen run) rather than constructing the koota
|
|
36
|
+
* world and RNG separately, so the facade's invariants (immutable `seeds`,
|
|
37
|
+
* clock starting at zero, scratch starting empty) always hold.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createSimWorld(seeds: RngSeeds): WorldHandle;
|
|
40
|
+
/**
|
|
41
|
+
* Tear down a simulation handle. This clears facade-owned scratch state and
|
|
42
|
+
* releases Koota's process-global world id. The operation is idempotent, which
|
|
43
|
+
* makes it safe to call from cleanup paths that may run more than once.
|
|
44
|
+
*/
|
|
45
|
+
export declare function destroySimWorld(handle: WorldHandle): void;
|
|
46
|
+
/**
|
|
47
|
+
* Advance `handle`'s clock by one fixed-timestep tick: increments
|
|
48
|
+
* `tickIndex` by 1 and accumulates `dt` seconds into `simSeconds`. Call once
|
|
49
|
+
* per sim tick, before running that tick's systems, so `clock` always
|
|
50
|
+
* reflects "how far the sim has progressed" rather than "how far it will
|
|
51
|
+
* have progressed after this tick's work."
|
|
52
|
+
*/
|
|
53
|
+
export declare function advanceClock(handle: WorldHandle, dt: number): void;
|
|
54
|
+
/**
|
|
55
|
+
* The serializable "header" of a world: both RNG layers' byte-exact state
|
|
56
|
+
* plus the clock. Deliberately excludes entity/component state — koota
|
|
57
|
+
* entities are the caller's data to serialize however their game needs
|
|
58
|
+
* (full ECS dump, delta log, etc.); this snapshot only covers what the
|
|
59
|
+
* facade itself owns.
|
|
60
|
+
*/
|
|
61
|
+
export type WorldSnapshot = {
|
|
62
|
+
rng: RngLayersSnapshot;
|
|
63
|
+
clock: {
|
|
64
|
+
tickIndex: number;
|
|
65
|
+
simSeconds: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Capture `handle`'s RNG state and clock into a plain, JSON-serializable
|
|
70
|
+
* `WorldSnapshot`. Pair with `restoreWorldHeader` for save/load — restoring
|
|
71
|
+
* a snapshot reproduces every subsequent RNG draw on both layers byte-exact
|
|
72
|
+
* and resumes the clock from the exact tick/second it was captured at.
|
|
73
|
+
*/
|
|
74
|
+
export declare function snapshotWorld(handle: WorldHandle): WorldSnapshot;
|
|
75
|
+
/**
|
|
76
|
+
* Restore `handle`'s RNG layers and clock from a `WorldSnapshot` taken by
|
|
77
|
+
* `snapshotWorld`. Mutates `handle` in place (replaces `handle.rng` and
|
|
78
|
+
* `handle.clock` with freshly rebuilt values) — entity/component state is
|
|
79
|
+
* untouched, since the facade never captured it in the first place; restore
|
|
80
|
+
* that separately using whatever serialization your game chose.
|
|
81
|
+
*/
|
|
82
|
+
export declare function restoreWorldHeader(handle: WorldHandle, snap: WorldSnapshot): void;
|
|
83
|
+
//# sourceMappingURL=world.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/world.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,aAAa,EAAe,QAAQ,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGvE,YAAY,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACpC,YAAY,EAAE,KAAK,EAAE,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAE1C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,GAAG,EAAE,SAAS,CAAC;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAY3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAKzD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAiBlE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,iBAAiB,CAAC;IACvB,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAKhE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAkBjF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WorldHandle } from "./world.js";
|
|
2
|
+
/**
|
|
3
|
+
* A published event log: append here, drain or peek from the other side.
|
|
4
|
+
*
|
|
5
|
+
* Returned as an object rather than loose functions so a system declares its log
|
|
6
|
+
* ONCE at module scope and the key is written in exactly one place. Two string
|
|
7
|
+
* literals for the same log is the typo that produces an event stream nobody
|
|
8
|
+
* receives, and no type checker can catch it.
|
|
9
|
+
*/
|
|
10
|
+
export type EventLog<T> = {
|
|
11
|
+
/** The scratch key, exposed for debugging and for snapshot tooling. */
|
|
12
|
+
readonly key: string;
|
|
13
|
+
/** Append an event. Called by the system that owns the fact. */
|
|
14
|
+
push: (handle: WorldHandle, event: T) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Take every event and EMPTY the log — for the single consuming system.
|
|
17
|
+
*
|
|
18
|
+
* If two systems both need the same events, that is a design question and not
|
|
19
|
+
* a reason to drain twice: publish two logs, or have the second peek. A log
|
|
20
|
+
* drained by two consumers delivers each event to whichever ran first.
|
|
21
|
+
*/
|
|
22
|
+
drain: (handle: WorldHandle) => T[];
|
|
23
|
+
/**
|
|
24
|
+
* Read every event WITHOUT emptying — for observers: harnesses, tests, HUDs.
|
|
25
|
+
*
|
|
26
|
+
* Returns a readonly copy, so a caller cannot mutate the live array and
|
|
27
|
+
* cannot hold a reference that changes underneath them.
|
|
28
|
+
*/
|
|
29
|
+
peek: (handle: WorldHandle) => readonly T[];
|
|
30
|
+
/** Drop everything without reading. For run teardown and test setup. */
|
|
31
|
+
clear: (handle: WorldHandle) => void;
|
|
32
|
+
/** How many events are pending, without allocating a copy. */
|
|
33
|
+
size: (handle: WorldHandle) => number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Declare an event log under `key`.
|
|
37
|
+
*
|
|
38
|
+
* The array is created lazily on first use and lives in `handle.scratch`, so it
|
|
39
|
+
* lifecycles with the world: a new run starts with empty logs and cannot inherit
|
|
40
|
+
* the last one's events. That is not a detail — a log at module scope survives
|
|
41
|
+
* the world that produced it, and stale events replay into a fresh run.
|
|
42
|
+
*
|
|
43
|
+
* Keys should be namespaced by system (`"combat:hits"`, `"voices:events"`) for
|
|
44
|
+
* the same reason trait names are: scratch is one flat map shared by every
|
|
45
|
+
* system in the world. Empty keys are rejected, as is a collision with an
|
|
46
|
+
* existing non-array scratch value.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineEventLog<T>(key: string): EventLog<T>;
|
|
49
|
+
//# sourceMappingURL=eventLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventLog.d.ts","sourceRoot":"","sources":["../../src/eventLog.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9C;;;;;;OAMG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,CAAC,EAAE,CAAC;IACpC;;;;;OAKG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC;IAC5C,wEAAwE;IACxE,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACrC,8DAA8D;IAC9D,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CA6C1D"}
|