@arcane-engine/runtime 0.1.0 → 0.2.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/package.json +4 -2
- package/src/agent/protocol.ts +35 -1
- package/src/agent/types.ts +98 -13
- package/src/particles/emitter.test.ts +323 -0
- package/src/particles/emitter.ts +409 -0
- package/src/particles/index.ts +25 -0
- package/src/particles/types.ts +236 -0
- package/src/pathfinding/astar.ts +27 -0
- package/src/pathfinding/types.ts +39 -0
- package/src/physics/aabb.ts +55 -8
- package/src/rendering/animation.ts +73 -0
- package/src/rendering/audio.ts +29 -9
- package/src/rendering/camera.ts +28 -4
- package/src/rendering/input.ts +45 -9
- package/src/rendering/lighting.ts +29 -3
- package/src/rendering/loop.ts +16 -3
- package/src/rendering/sprites.ts +24 -1
- package/src/rendering/text.ts +52 -6
- package/src/rendering/texture.ts +22 -4
- package/src/rendering/tilemap.ts +36 -4
- package/src/rendering/types.ts +37 -19
- package/src/rendering/validate.ts +48 -3
- package/src/state/error.ts +21 -2
- package/src/state/observe.ts +40 -9
- package/src/state/prng.ts +88 -10
- package/src/state/query.ts +115 -15
- package/src/state/store.ts +42 -11
- package/src/state/transaction.ts +116 -12
- package/src/state/types.ts +31 -5
- package/src/systems/system.ts +77 -5
- package/src/systems/types.ts +52 -6
- package/src/testing/harness.ts +103 -5
- package/src/testing/mock-renderer.test.ts +16 -20
- package/src/tweening/chain.test.ts +191 -0
- package/src/tweening/chain.ts +103 -0
- package/src/tweening/easing.test.ts +134 -0
- package/src/tweening/easing.ts +288 -0
- package/src/tweening/helpers.test.ts +185 -0
- package/src/tweening/helpers.ts +166 -0
- package/src/tweening/index.ts +76 -0
- package/src/tweening/tween.test.ts +322 -0
- package/src/tweening/tween.ts +296 -0
- package/src/tweening/types.ts +134 -0
- package/src/ui/colors.ts +129 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/primitives.ts +44 -5
- package/src/ui/types.ts +41 -2
package/src/state/query.ts
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import type { Vec2 } from "./types.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A predicate function for filtering state queries.
|
|
5
|
+
* Returns true if the item matches the filter criteria.
|
|
6
|
+
* Build predicates using combinators: {@link lt}, {@link gt}, {@link eq}, {@link oneOf}, etc.
|
|
7
|
+
*/
|
|
4
8
|
export type Predicate<T> = (item: T) => boolean;
|
|
5
9
|
|
|
6
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Query state at a dot-separated path, with optional filtering.
|
|
12
|
+
* Pure function — does not modify the state.
|
|
13
|
+
*
|
|
14
|
+
* If the value at the path is an array, returns matching elements.
|
|
15
|
+
* If the value is a single value, wraps it in an array.
|
|
16
|
+
* If the path doesn't exist, returns an empty array.
|
|
17
|
+
*
|
|
18
|
+
* The filter can be a predicate function, or an object where each key-value pair
|
|
19
|
+
* must match (values can be predicates or literal values).
|
|
20
|
+
* Supports `*` wildcards in paths to query across array elements.
|
|
21
|
+
*
|
|
22
|
+
* @param state - The state to query.
|
|
23
|
+
* @param path - Dot-separated path (e.g., "enemies", "player.inventory"). Use `*` for wildcards.
|
|
24
|
+
* @param filter - Optional predicate function or property-matching object.
|
|
25
|
+
* @returns Readonly array of matching results.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const alive = query(state, "enemies", { alive: true });
|
|
29
|
+
* const nearby = query(state, "enemies", within({ x: 5, y: 5 }, 3));
|
|
30
|
+
* const names = query(state, "enemies.*.name");
|
|
31
|
+
*/
|
|
7
32
|
export function query<S, R = unknown>(
|
|
8
33
|
state: S,
|
|
9
34
|
path: string,
|
|
@@ -36,12 +61,30 @@ export function query<S, R = unknown>(
|
|
|
36
61
|
}) as readonly R[];
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Get a single value at a dot-separated path. Pure function.
|
|
66
|
+
* Returns undefined if the path doesn't exist.
|
|
67
|
+
*
|
|
68
|
+
* @param state - The state to read from.
|
|
69
|
+
* @param path - Dot-separated path (e.g., "player.hp", "config.difficulty").
|
|
70
|
+
* @returns The value at the path, or undefined if not found.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const hp = get(state, "player.hp"); // number | undefined
|
|
74
|
+
*/
|
|
40
75
|
export function get<S, R = unknown>(state: S, path: string): R | undefined {
|
|
41
76
|
return getByPath(state, path) as R | undefined;
|
|
42
77
|
}
|
|
43
78
|
|
|
44
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Check if a value exists at a path, optionally testing it with a predicate.
|
|
81
|
+
* Pure function. Returns false if the path doesn't exist or if the predicate fails.
|
|
82
|
+
*
|
|
83
|
+
* @param state - The state to check.
|
|
84
|
+
* @param path - Dot-separated path (e.g., "player.weapon").
|
|
85
|
+
* @param predicate - Optional predicate to test the value against.
|
|
86
|
+
* @returns True if the value exists (and passes the predicate, if provided).
|
|
87
|
+
*/
|
|
45
88
|
export function has<S>(
|
|
46
89
|
state: S,
|
|
47
90
|
path: string,
|
|
@@ -55,42 +98,84 @@ export function has<S>(
|
|
|
55
98
|
|
|
56
99
|
// --- Filter combinators ---
|
|
57
100
|
|
|
58
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Create a predicate that tests if a number is less than the given value.
|
|
103
|
+
*
|
|
104
|
+
* @param value - The threshold to compare against.
|
|
105
|
+
* @returns A predicate returning true if item < value.
|
|
106
|
+
*/
|
|
59
107
|
export function lt(value: number): Predicate<number> {
|
|
60
108
|
return (item: number) => item < value;
|
|
61
109
|
}
|
|
62
110
|
|
|
63
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Create a predicate that tests if a number is greater than the given value.
|
|
113
|
+
*
|
|
114
|
+
* @param value - The threshold to compare against.
|
|
115
|
+
* @returns A predicate returning true if item > value.
|
|
116
|
+
*/
|
|
64
117
|
export function gt(value: number): Predicate<number> {
|
|
65
118
|
return (item: number) => item > value;
|
|
66
119
|
}
|
|
67
120
|
|
|
68
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Create a predicate that tests if a number is less than or equal to the given value.
|
|
123
|
+
*
|
|
124
|
+
* @param value - The threshold to compare against.
|
|
125
|
+
* @returns A predicate returning true if item <= value.
|
|
126
|
+
*/
|
|
69
127
|
export function lte(value: number): Predicate<number> {
|
|
70
128
|
return (item: number) => item <= value;
|
|
71
129
|
}
|
|
72
130
|
|
|
73
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Create a predicate that tests if a number is greater than or equal to the given value.
|
|
133
|
+
*
|
|
134
|
+
* @param value - The threshold to compare against.
|
|
135
|
+
* @returns A predicate returning true if item >= value.
|
|
136
|
+
*/
|
|
74
137
|
export function gte(value: number): Predicate<number> {
|
|
75
138
|
return (item: number) => item >= value;
|
|
76
139
|
}
|
|
77
140
|
|
|
78
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Create a predicate that tests for strict equality (===) with the given value.
|
|
143
|
+
*
|
|
144
|
+
* @param value - The value to compare against.
|
|
145
|
+
* @returns A predicate returning true if item === value.
|
|
146
|
+
*/
|
|
79
147
|
export function eq<T>(value: T): Predicate<T> {
|
|
80
148
|
return (item: T) => item === value;
|
|
81
149
|
}
|
|
82
150
|
|
|
83
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Create a predicate that tests for strict inequality (!==) with the given value.
|
|
153
|
+
*
|
|
154
|
+
* @param value - The value to compare against.
|
|
155
|
+
* @returns A predicate returning true if item !== value.
|
|
156
|
+
*/
|
|
84
157
|
export function neq<T>(value: T): Predicate<T> {
|
|
85
158
|
return (item: T) => item !== value;
|
|
86
159
|
}
|
|
87
160
|
|
|
88
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Create a predicate that tests if a value is one of the given options (using Array.includes).
|
|
163
|
+
*
|
|
164
|
+
* @param values - The allowed values to match against.
|
|
165
|
+
* @returns A predicate returning true if item is in the values list.
|
|
166
|
+
*/
|
|
89
167
|
export function oneOf<T>(...values: T[]): Predicate<T> {
|
|
90
168
|
return (item: T) => values.includes(item);
|
|
91
169
|
}
|
|
92
170
|
|
|
93
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Create a predicate that tests if a Vec2 position is within a circular radius
|
|
173
|
+
* of a center point. Uses squared distance for efficiency (no sqrt).
|
|
174
|
+
*
|
|
175
|
+
* @param center - Center point of the circle.
|
|
176
|
+
* @param radius - Radius of the circle. Must be >= 0.
|
|
177
|
+
* @returns A predicate returning true if the position is within the circle (inclusive).
|
|
178
|
+
*/
|
|
94
179
|
export function within(center: Vec2, radius: number): Predicate<Vec2> {
|
|
95
180
|
return (pos: Vec2) => {
|
|
96
181
|
const dx = pos.x - center.x;
|
|
@@ -99,17 +184,32 @@ export function within(center: Vec2, radius: number): Predicate<Vec2> {
|
|
|
99
184
|
};
|
|
100
185
|
}
|
|
101
186
|
|
|
102
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Combine multiple predicates with logical AND. All predicates must pass.
|
|
189
|
+
*
|
|
190
|
+
* @param predicates - Predicates to combine.
|
|
191
|
+
* @returns A predicate returning true only if every predicate passes.
|
|
192
|
+
*/
|
|
103
193
|
export function allOf<T>(...predicates: Predicate<T>[]): Predicate<T> {
|
|
104
194
|
return (item: T) => predicates.every((p) => p(item));
|
|
105
195
|
}
|
|
106
196
|
|
|
107
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* Combine multiple predicates with logical OR. At least one predicate must pass.
|
|
199
|
+
*
|
|
200
|
+
* @param predicates - Predicates to combine.
|
|
201
|
+
* @returns A predicate returning true if any predicate passes.
|
|
202
|
+
*/
|
|
108
203
|
export function anyOf<T>(...predicates: Predicate<T>[]): Predicate<T> {
|
|
109
204
|
return (item: T) => predicates.some((p) => p(item));
|
|
110
205
|
}
|
|
111
206
|
|
|
112
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Negate a predicate. Returns the logical NOT of the original predicate.
|
|
209
|
+
*
|
|
210
|
+
* @param predicate - The predicate to negate.
|
|
211
|
+
* @returns A predicate returning true when the original returns false, and vice versa.
|
|
212
|
+
*/
|
|
113
213
|
export function not<T>(predicate: Predicate<T>): Predicate<T> {
|
|
114
214
|
return (item: T) => !predicate(item);
|
|
115
215
|
}
|
package/src/state/store.ts
CHANGED
|
@@ -6,47 +6,78 @@ import { query, get, has } from "./query.ts";
|
|
|
6
6
|
import type { PathPattern, ObserverCallback, Unsubscribe } from "./observe.ts";
|
|
7
7
|
import { createObserverRegistry } from "./observe.ts";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The game store: central coordination point for state management.
|
|
11
|
+
* Ties together state, transactions, queries, and observers.
|
|
12
|
+
* Created via {@link createStore}.
|
|
13
|
+
*
|
|
14
|
+
* - `getState()` - Returns the current state as a deep readonly snapshot.
|
|
15
|
+
* - `dispatch(mutations)` - Apply mutations atomically, update state, notify observers.
|
|
16
|
+
* - `observe(pattern, callback)` - Subscribe to state changes matching a path pattern.
|
|
17
|
+
* - `query(path, filter?)` - Query arrays or values in current state.
|
|
18
|
+
* - `get(path)` - Get a single value from current state.
|
|
19
|
+
* - `has(path, predicate?)` - Check existence in current state.
|
|
20
|
+
* - `replaceState(state)` - Replace the entire state (for deserialization / time travel).
|
|
21
|
+
* - `getHistory()` - Get the transaction history for recording/replay.
|
|
22
|
+
*/
|
|
10
23
|
export type GameStore<S> = Readonly<{
|
|
11
|
-
/**
|
|
24
|
+
/** Returns the current state as a deep readonly snapshot. */
|
|
12
25
|
getState: () => DeepReadonly<S>;
|
|
13
26
|
|
|
14
|
-
/** Apply mutations
|
|
27
|
+
/** Apply mutations atomically. Updates state and notifies observers on success. */
|
|
15
28
|
dispatch: (mutations: readonly Mutation<S>[]) => TransactionResult<S>;
|
|
16
29
|
|
|
17
|
-
/** Subscribe to state changes
|
|
30
|
+
/** Subscribe to state changes matching a path pattern. Returns an unsubscribe function. */
|
|
18
31
|
observe: <T = unknown>(
|
|
19
32
|
pattern: PathPattern,
|
|
20
33
|
callback: ObserverCallback<T>,
|
|
21
34
|
) => Unsubscribe;
|
|
22
35
|
|
|
23
|
-
/** Query
|
|
36
|
+
/** Query arrays or values at a path, with optional filtering. */
|
|
24
37
|
query: <R = unknown>(
|
|
25
38
|
path: string,
|
|
26
39
|
filter?: Predicate<R> | Record<string, unknown>,
|
|
27
40
|
) => readonly R[];
|
|
28
41
|
|
|
29
|
-
/** Get a value from current state */
|
|
42
|
+
/** Get a single value from current state by path. Returns undefined if not found. */
|
|
30
43
|
get: <R = unknown>(path: string) => R | undefined;
|
|
31
44
|
|
|
32
|
-
/** Check
|
|
45
|
+
/** Check if a value exists at a path, optionally testing with a predicate. */
|
|
33
46
|
has: (path: string, predicate?: Predicate<unknown>) => boolean;
|
|
34
47
|
|
|
35
|
-
/** Replace the entire state (for deserialization / time travel) */
|
|
48
|
+
/** Replace the entire state (for deserialization / time travel). Does not trigger observers. */
|
|
36
49
|
replaceState: (state: S) => void;
|
|
37
50
|
|
|
38
|
-
/** Get the transaction history
|
|
51
|
+
/** Get the transaction history as an ordered list of TransactionRecords. */
|
|
39
52
|
getHistory: () => readonly TransactionRecord<S>[];
|
|
40
53
|
}>;
|
|
41
54
|
|
|
42
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* A recorded transaction for replay and debugging.
|
|
57
|
+
* Stored in the store's history, accessible via getHistory().
|
|
58
|
+
*
|
|
59
|
+
* - `timestamp` - When the transaction was applied (Date.now() milliseconds).
|
|
60
|
+
* - `mutations` - The mutations that were applied in this transaction.
|
|
61
|
+
* - `diff` - The computed diff of changes from this transaction.
|
|
62
|
+
*/
|
|
43
63
|
export type TransactionRecord<S> = Readonly<{
|
|
44
64
|
timestamp: number;
|
|
45
65
|
mutations: readonly Mutation<S>[];
|
|
46
66
|
diff: Diff;
|
|
47
67
|
}>;
|
|
48
68
|
|
|
49
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Create a new game store with initial state, transactions, and observers.
|
|
71
|
+
* The store is the central coordination point for game state management.
|
|
72
|
+
*
|
|
73
|
+
* @param initialState - The initial state object. Becomes the starting state for all queries.
|
|
74
|
+
* @returns A GameStore with getState, dispatch, observe, query, get, has, replaceState, and getHistory.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const store = createStore({ player: { x: 0, y: 0, hp: 100 }, enemies: [] });
|
|
78
|
+
* store.dispatch([set("player.x", 10)]);
|
|
79
|
+
* console.log(store.getState().player.x); // 10
|
|
80
|
+
*/
|
|
50
81
|
export function createStore<S>(initialState: S): GameStore<S> {
|
|
51
82
|
let state: S = initialState;
|
|
52
83
|
const observers = createObserverRegistry<S>();
|
package/src/state/transaction.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ArcaneError } from "./error.ts";
|
|
2
2
|
import { createError } from "./error.ts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A mutation: a named, describable, applicable state change.
|
|
6
|
+
* Created by mutation primitives ({@link set}, {@link update}, {@link push}, etc.)
|
|
7
|
+
* and applied atomically via {@link transaction}.
|
|
8
|
+
*
|
|
9
|
+
* - `type` - Mutation kind: "set", "update", "push", or "remove".
|
|
10
|
+
* - `path` - Dot-separated path to the target value (e.g., "player.hp").
|
|
11
|
+
* - `description` - Human-readable description of what this mutation does.
|
|
12
|
+
* - `apply` - Pure function that takes state and returns new state with the mutation applied.
|
|
13
|
+
*/
|
|
5
14
|
export type Mutation<S> = Readonly<{
|
|
6
15
|
type: string;
|
|
7
16
|
path: string;
|
|
@@ -11,7 +20,21 @@ export type Mutation<S> = Readonly<{
|
|
|
11
20
|
|
|
12
21
|
// --- Core mutation primitives ---
|
|
13
22
|
|
|
14
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Create a mutation that sets a value at a dot-separated path.
|
|
25
|
+
* Pure function — returns a Mutation object, does not modify state directly.
|
|
26
|
+
* Apply via {@link transaction} or {@link GameStore.dispatch}.
|
|
27
|
+
*
|
|
28
|
+
* @param path - Dot-separated path (e.g., "player.hp", "enemies.0.alive").
|
|
29
|
+
* @param value - The value to set at the path.
|
|
30
|
+
* @returns A Mutation that can be applied in a transaction.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const result = transaction(state, [
|
|
34
|
+
* set("player.hp", 80),
|
|
35
|
+
* set("player.position.x", 10),
|
|
36
|
+
* ]);
|
|
37
|
+
*/
|
|
15
38
|
export function set<S>(path: string, value: unknown): Mutation<S> {
|
|
16
39
|
return {
|
|
17
40
|
type: "set",
|
|
@@ -21,7 +44,15 @@ export function set<S>(path: string, value: unknown): Mutation<S> {
|
|
|
21
44
|
};
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Create a mutation that updates a value at a path using a transform function.
|
|
49
|
+
* The function receives the current value and returns the new value.
|
|
50
|
+
* Pure function — returns a Mutation object, does not modify state directly.
|
|
51
|
+
*
|
|
52
|
+
* @param path - Dot-separated path to the value (e.g., "player.hp").
|
|
53
|
+
* @param fn - Transform function: receives the current value, returns the new value.
|
|
54
|
+
* @returns A Mutation that can be applied in a transaction.
|
|
55
|
+
*/
|
|
25
56
|
export function update<S>(
|
|
26
57
|
path: string,
|
|
27
58
|
fn: (current: unknown) => unknown,
|
|
@@ -37,7 +68,14 @@ export function update<S>(
|
|
|
37
68
|
};
|
|
38
69
|
}
|
|
39
70
|
|
|
40
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Create a mutation that pushes an item onto an array at a path.
|
|
73
|
+
* Throws during application if the value at the path is not an array.
|
|
74
|
+
*
|
|
75
|
+
* @param path - Dot-separated path to the array (e.g., "enemies", "player.inventory").
|
|
76
|
+
* @param item - The item to append to the array.
|
|
77
|
+
* @returns A Mutation that can be applied in a transaction.
|
|
78
|
+
*/
|
|
41
79
|
export function push<S>(path: string, item: unknown): Mutation<S> {
|
|
42
80
|
return {
|
|
43
81
|
type: "push",
|
|
@@ -53,7 +91,14 @@ export function push<S>(path: string, item: unknown): Mutation<S> {
|
|
|
53
91
|
};
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Create a mutation that removes items from an array at a path where the predicate returns true.
|
|
96
|
+
* Throws during application if the value at the path is not an array.
|
|
97
|
+
*
|
|
98
|
+
* @param path - Dot-separated path to the array.
|
|
99
|
+
* @param predicate - Function that returns true for items to remove.
|
|
100
|
+
* @returns A Mutation that can be applied in a transaction.
|
|
101
|
+
*/
|
|
57
102
|
export function removeWhere<S>(
|
|
58
103
|
path: string,
|
|
59
104
|
predicate: (item: unknown) => boolean,
|
|
@@ -76,7 +121,15 @@ export function removeWhere<S>(
|
|
|
76
121
|
};
|
|
77
122
|
}
|
|
78
123
|
|
|
79
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Create a mutation that removes a key from an object.
|
|
126
|
+
* The last segment of the path is the key to remove; the preceding segments
|
|
127
|
+
* identify the parent object. Throws during application if the parent is not an object.
|
|
128
|
+
*
|
|
129
|
+
* @param path - Dot-separated path where the last segment is the key to remove
|
|
130
|
+
* (e.g., "player.buffs.shield" removes "shield" from player.buffs).
|
|
131
|
+
* @returns A Mutation that can be applied in a transaction.
|
|
132
|
+
*/
|
|
80
133
|
export function removeKey<S>(path: string): Mutation<S> {
|
|
81
134
|
const segments = path.split(".");
|
|
82
135
|
const key = segments.pop()!;
|
|
@@ -101,28 +154,54 @@ export function removeKey<S>(path: string): Mutation<S> {
|
|
|
101
154
|
|
|
102
155
|
// --- Diff ---
|
|
103
156
|
|
|
104
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* A single change entry in a diff, representing one value that changed.
|
|
159
|
+
*
|
|
160
|
+
* - `path` - Dot-separated path to the changed value (e.g., "player.hp").
|
|
161
|
+
* - `from` - The previous value (undefined if the key was added).
|
|
162
|
+
* - `to` - The new value (undefined if the key was removed).
|
|
163
|
+
*/
|
|
105
164
|
export type DiffEntry = Readonly<{
|
|
106
165
|
path: string;
|
|
107
166
|
from: unknown;
|
|
108
167
|
to: unknown;
|
|
109
168
|
}>;
|
|
110
169
|
|
|
111
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* All changes from a transaction, as a list of individual DiffEntry items.
|
|
172
|
+
* Empty entries array means no changes occurred.
|
|
173
|
+
*
|
|
174
|
+
* - `entries` - Ordered list of individual value changes.
|
|
175
|
+
*/
|
|
112
176
|
export type Diff = Readonly<{
|
|
113
177
|
entries: readonly DiffEntry[];
|
|
114
178
|
}>;
|
|
115
179
|
|
|
116
180
|
// --- Transaction result ---
|
|
117
181
|
|
|
118
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* An effect triggered by a state change, for observer/event routing.
|
|
184
|
+
* Reserved for future use — currently transactions return an empty effects array.
|
|
185
|
+
*
|
|
186
|
+
* - `type` - Effect type identifier (e.g., "damage", "levelUp").
|
|
187
|
+
* - `source` - Identifier of the mutation or system that produced this effect.
|
|
188
|
+
* - `data` - Arbitrary payload data for the effect.
|
|
189
|
+
*/
|
|
119
190
|
export type Effect = Readonly<{
|
|
120
191
|
type: string;
|
|
121
192
|
source: string;
|
|
122
193
|
data: Readonly<Record<string, unknown>>;
|
|
123
194
|
}>;
|
|
124
195
|
|
|
125
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Result of executing a transaction. Check `valid` before using the new state.
|
|
198
|
+
*
|
|
199
|
+
* - `state` - The resulting state. Equals the original state if the transaction failed.
|
|
200
|
+
* - `diff` - Changes that occurred. Empty if the transaction failed.
|
|
201
|
+
* - `effects` - Side effects produced (reserved for future use).
|
|
202
|
+
* - `valid` - Whether the transaction succeeded. If false, state is unchanged.
|
|
203
|
+
* - `error` - Structured error if `valid` is false. Undefined on success.
|
|
204
|
+
*/
|
|
126
205
|
export type TransactionResult<S> = Readonly<{
|
|
127
206
|
state: S;
|
|
128
207
|
diff: Diff;
|
|
@@ -131,7 +210,24 @@ export type TransactionResult<S> = Readonly<{
|
|
|
131
210
|
error?: ArcaneError;
|
|
132
211
|
}>;
|
|
133
212
|
|
|
134
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Apply mutations atomically to state. All succeed or all roll back.
|
|
215
|
+
* Pure function — returns a new state without modifying the original.
|
|
216
|
+
* If any mutation throws, the entire transaction fails and the original state is returned.
|
|
217
|
+
*
|
|
218
|
+
* @param state - The current state to apply mutations to.
|
|
219
|
+
* @param mutations - Ordered list of mutations to apply. Created via {@link set}, {@link update}, etc.
|
|
220
|
+
* @returns A TransactionResult with the new state, diff, and validity flag.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* const result = transaction(state, [
|
|
224
|
+
* set("player.hp", 80),
|
|
225
|
+
* update("player.xp", (xp: any) => xp + 50),
|
|
226
|
+
* ]);
|
|
227
|
+
* if (result.valid) {
|
|
228
|
+
* // Use result.state
|
|
229
|
+
* }
|
|
230
|
+
*/
|
|
135
231
|
export function transaction<S>(
|
|
136
232
|
state: S,
|
|
137
233
|
mutations: readonly Mutation<S>[],
|
|
@@ -166,7 +262,15 @@ export function transaction<S>(
|
|
|
166
262
|
}
|
|
167
263
|
}
|
|
168
264
|
|
|
169
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* Compute the diff between two state trees by recursively comparing all values.
|
|
267
|
+
* Pure function — does not modify either state tree.
|
|
268
|
+
* Used internally by {@link transaction}, but can also be called directly.
|
|
269
|
+
*
|
|
270
|
+
* @param before - The state before changes.
|
|
271
|
+
* @param after - The state after changes.
|
|
272
|
+
* @returns A Diff containing all individual value changes.
|
|
273
|
+
*/
|
|
170
274
|
export function computeDiff<S>(before: S, after: S): Diff {
|
|
171
275
|
const entries: DiffEntry[] = [];
|
|
172
276
|
diffRecursive(before, after, "", entries);
|
package/src/state/types.ts
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Branded string type for entity identification.
|
|
3
|
+
* Uses TypeScript's structural branding to prevent plain strings from being
|
|
4
|
+
* used where an EntityId is expected. Create via {@link entityId} or {@link generateId}.
|
|
5
|
+
*/
|
|
2
6
|
export type EntityId = string & { readonly __entityId: true };
|
|
3
7
|
|
|
4
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Create an EntityId from a known string value.
|
|
10
|
+
* Use this for deterministic IDs (e.g., "player", "enemy_1").
|
|
11
|
+
* For random unique IDs, use {@link generateId} instead.
|
|
12
|
+
*
|
|
13
|
+
* @param id - The string to brand as an EntityId.
|
|
14
|
+
* @returns A branded EntityId.
|
|
15
|
+
*/
|
|
5
16
|
export function entityId(id: string): EntityId {
|
|
6
17
|
return id as EntityId;
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Generate a unique EntityId using crypto.randomUUID().
|
|
22
|
+
* Each call produces a new UUID v4 string branded as EntityId.
|
|
23
|
+
* Use {@link entityId} instead when you need a deterministic, human-readable ID.
|
|
24
|
+
*
|
|
25
|
+
* @returns A new unique EntityId.
|
|
26
|
+
*/
|
|
10
27
|
export function generateId(): EntityId {
|
|
11
28
|
return crypto.randomUUID() as EntityId;
|
|
12
29
|
}
|
|
13
30
|
|
|
14
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Immutable 2D vector. Used for positions, velocities, and directions.
|
|
33
|
+
*
|
|
34
|
+
* - `x` - Horizontal component (positive = right).
|
|
35
|
+
* - `y` - Vertical component (positive = down in screen coordinates).
|
|
36
|
+
*/
|
|
15
37
|
export type Vec2 = Readonly<{ x: number; y: number }>;
|
|
16
38
|
|
|
17
39
|
type Primitive = string | number | boolean | null | undefined;
|
|
18
40
|
|
|
19
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Deep recursive readonly utility type. Enforces immutability at the type level
|
|
43
|
+
* by recursively wrapping all properties, arrays, Maps, and Sets as readonly.
|
|
44
|
+
* Applied to state returned by {@link GameStore.getState} to prevent accidental mutation.
|
|
45
|
+
*/
|
|
20
46
|
export type DeepReadonly<T> = T extends Primitive
|
|
21
47
|
? T
|
|
22
48
|
: T extends (infer U)[]
|
package/src/systems/system.ts
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { Condition, Action, Rule, SystemDef, RuleResult, ExtendOptions } from "./types.ts";
|
|
2
2
|
import { createError } from "../state/error.ts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Create a system definition from a name and list of rules.
|
|
6
|
+
*
|
|
7
|
+
* A system is a named collection of rules that together define a game mechanic.
|
|
8
|
+
* Use {@link rule} to build rules, then combine them into a system.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam S - The game state type.
|
|
11
|
+
* @param name - System name (e.g., "combat", "inventory").
|
|
12
|
+
* @param rules - Ordered array of rules belonging to this system.
|
|
13
|
+
* @returns An immutable {@link SystemDef}.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const combat = system("combat", [
|
|
18
|
+
* rule<GameState>("attack")
|
|
19
|
+
* .when((s, args) => s.player.hp > 0)
|
|
20
|
+
* .then((s, args) => ({ ...s, enemy: { ...s.enemy, hp: s.enemy.hp - 10 } })),
|
|
21
|
+
* ]);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
5
24
|
export function system<S>(name: string, rules: readonly Rule<S>[]): SystemDef<S> {
|
|
6
25
|
return { name, rules };
|
|
7
26
|
}
|
|
@@ -21,7 +40,24 @@ type RuleBuilderBase<S> = {
|
|
|
21
40
|
};
|
|
22
41
|
};
|
|
23
42
|
|
|
24
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Fluent builder for creating named rules.
|
|
45
|
+
*
|
|
46
|
+
* Chain `.when()` to add conditions and `.then()` to add actions.
|
|
47
|
+
* Use `.replaces()` to mark this rule as a replacement for an existing rule
|
|
48
|
+
* when used with {@link extend}.
|
|
49
|
+
*
|
|
50
|
+
* @typeParam S - The game state type.
|
|
51
|
+
* @param name - Unique rule name within the system.
|
|
52
|
+
* @returns A fluent builder with `.when()`, `.then()`, and `.replaces()` methods.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const attackRule = rule<GameState>("attack")
|
|
57
|
+
* .when((s) => s.player.hp > 0, (s) => s.enemy.hp > 0)
|
|
58
|
+
* .then((s, args) => ({ ...s, enemy: { ...s.enemy, hp: s.enemy.hp - 10 } }));
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
25
61
|
export function rule<S>(name: string): RuleBuilderBase<S> {
|
|
26
62
|
let replacesName: string | undefined;
|
|
27
63
|
|
|
@@ -59,7 +95,20 @@ export function rule<S>(name: string): RuleBuilderBase<S> {
|
|
|
59
95
|
};
|
|
60
96
|
}
|
|
61
97
|
|
|
62
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Find a rule by name in a system, check its conditions, and execute its actions.
|
|
100
|
+
*
|
|
101
|
+
* If the rule is not found, returns `{ ok: false }` with an UNKNOWN_RULE error.
|
|
102
|
+
* If any condition fails, returns `{ ok: false }` with a CONDITION_FAILED error.
|
|
103
|
+
* Otherwise, chains all actions and returns `{ ok: true }` with the new state.
|
|
104
|
+
*
|
|
105
|
+
* @typeParam S - The game state type.
|
|
106
|
+
* @param sys - The system to search for the rule.
|
|
107
|
+
* @param ruleName - Name of the rule to apply.
|
|
108
|
+
* @param state - Current game state.
|
|
109
|
+
* @param args - Optional arguments passed to conditions and actions.
|
|
110
|
+
* @returns A {@link RuleResult} with the outcome and resulting state.
|
|
111
|
+
*/
|
|
63
112
|
export function applyRule<S>(
|
|
64
113
|
sys: SystemDef<S>,
|
|
65
114
|
ruleName: string,
|
|
@@ -102,7 +151,16 @@ export function applyRule<S>(
|
|
|
102
151
|
return { ok: true, state: current, ruleName };
|
|
103
152
|
}
|
|
104
153
|
|
|
105
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Return names of rules whose conditions are all satisfied for the given state.
|
|
156
|
+
* Useful for presenting valid actions to a player or AI agent.
|
|
157
|
+
*
|
|
158
|
+
* @typeParam S - The game state type.
|
|
159
|
+
* @param sys - The system to query.
|
|
160
|
+
* @param state - Current game state to test conditions against.
|
|
161
|
+
* @param args - Optional arguments passed to condition functions.
|
|
162
|
+
* @returns Array of rule names that can currently be applied.
|
|
163
|
+
*/
|
|
106
164
|
export function getApplicableRules<S>(
|
|
107
165
|
sys: SystemDef<S>,
|
|
108
166
|
state: S,
|
|
@@ -113,7 +171,21 @@ export function getApplicableRules<S>(
|
|
|
113
171
|
.map((r) => r.name);
|
|
114
172
|
}
|
|
115
173
|
|
|
116
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Create a new system by extending an existing one.
|
|
176
|
+
*
|
|
177
|
+
* Supports three operations:
|
|
178
|
+
* 1. **Replace** — new rules with `replaces` set swap out existing rules by name.
|
|
179
|
+
* 2. **Add** — new rules without `replaces` are appended to the end.
|
|
180
|
+
* 3. **Remove** — rules named in `options.remove` are excluded.
|
|
181
|
+
*
|
|
182
|
+
* The base system is not modified; a new {@link SystemDef} is returned.
|
|
183
|
+
*
|
|
184
|
+
* @typeParam S - The game state type.
|
|
185
|
+
* @param base - The system to extend.
|
|
186
|
+
* @param options - Rules to add/replace and rule names to remove.
|
|
187
|
+
* @returns A new system with the modifications applied.
|
|
188
|
+
*/
|
|
117
189
|
export function extend<S>(base: SystemDef<S>, options: ExtendOptions<S>): SystemDef<S> {
|
|
118
190
|
const removeSet = new Set(options.remove ?? []);
|
|
119
191
|
const newRules = options.rules ?? [];
|