@aura3d/scripting 1.0.6
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/README.md +16 -0
- package/dist/AdaptiveDifficultyFixtures.d.ts +58 -0
- package/dist/AdaptiveDifficultyFixtures.d.ts.map +1 -0
- package/dist/AdaptiveDifficultyFixtures.js +228 -0
- package/dist/AdaptiveDifficultyFixtures.js.map +1 -0
- package/dist/AnalyticsPrivacyFixtures.d.ts +63 -0
- package/dist/AnalyticsPrivacyFixtures.d.ts.map +1 -0
- package/dist/AnalyticsPrivacyFixtures.js +120 -0
- package/dist/AnalyticsPrivacyFixtures.js.map +1 -0
- package/dist/Behavior.d.ts +10 -0
- package/dist/Behavior.d.ts.map +1 -0
- package/dist/Behavior.js +2 -0
- package/dist/Behavior.js.map +1 -0
- package/dist/BehaviorHost.d.ts +17 -0
- package/dist/BehaviorHost.d.ts.map +1 -0
- package/dist/BehaviorHost.js +39 -0
- package/dist/BehaviorHost.js.map +1 -0
- package/dist/BehaviorRegistry.d.ts +8 -0
- package/dist/BehaviorRegistry.d.ts.map +1 -0
- package/dist/BehaviorRegistry.js +17 -0
- package/dist/BehaviorRegistry.js.map +1 -0
- package/dist/BehaviorSystem.d.ts +26 -0
- package/dist/BehaviorSystem.d.ts.map +1 -0
- package/dist/BehaviorSystem.js +76 -0
- package/dist/BehaviorSystem.js.map +1 -0
- package/dist/BehaviorTree.d.ts +75 -0
- package/dist/BehaviorTree.d.ts.map +1 -0
- package/dist/BehaviorTree.js +204 -0
- package/dist/BehaviorTree.js.map +1 -0
- package/dist/CloudServiceFixtures.d.ts +81 -0
- package/dist/CloudServiceFixtures.d.ts.map +1 -0
- package/dist/CloudServiceFixtures.js +144 -0
- package/dist/CloudServiceFixtures.js.map +1 -0
- package/dist/CulturalBehaviorFixtures.d.ts +75 -0
- package/dist/CulturalBehaviorFixtures.d.ts.map +1 -0
- package/dist/CulturalBehaviorFixtures.js +180 -0
- package/dist/CulturalBehaviorFixtures.js.map +1 -0
- package/dist/DecisionTree.d.ts +42 -0
- package/dist/DecisionTree.d.ts.map +1 -0
- package/dist/DecisionTree.js +125 -0
- package/dist/DecisionTree.js.map +1 -0
- package/dist/GOAP.d.ts +51 -0
- package/dist/GOAP.d.ts.map +1 -0
- package/dist/GOAP.js +105 -0
- package/dist/GOAP.js.map +1 -0
- package/dist/HTN.d.ts +58 -0
- package/dist/HTN.d.ts.map +1 -0
- package/dist/HTN.js +115 -0
- package/dist/HTN.js.map +1 -0
- package/dist/LearningAgentFixtures.d.ts +60 -0
- package/dist/LearningAgentFixtures.d.ts.map +1 -0
- package/dist/LearningAgentFixtures.js +122 -0
- package/dist/LearningAgentFixtures.js.map +1 -0
- package/dist/NetworkReplicationFixtures.d.ts +82 -0
- package/dist/NetworkReplicationFixtures.d.ts.map +1 -0
- package/dist/NetworkReplicationFixtures.js +330 -0
- package/dist/NetworkReplicationFixtures.js.map +1 -0
- package/dist/Perception.d.ts +58 -0
- package/dist/Perception.d.ts.map +1 -0
- package/dist/Perception.js +111 -0
- package/dist/Perception.js.map +1 -0
- package/dist/PlayerBehaviorTelemetryFixtures.d.ts +74 -0
- package/dist/PlayerBehaviorTelemetryFixtures.d.ts.map +1 -0
- package/dist/PlayerBehaviorTelemetryFixtures.js +189 -0
- package/dist/PlayerBehaviorTelemetryFixtures.js.map +1 -0
- package/dist/ProceduralContentAdaptationFixtures.d.ts +52 -0
- package/dist/ProceduralContentAdaptationFixtures.d.ts.map +1 -0
- package/dist/ProceduralContentAdaptationFixtures.js +149 -0
- package/dist/ProceduralContentAdaptationFixtures.js.map +1 -0
- package/dist/ScriptContext.d.ts +18 -0
- package/dist/ScriptContext.d.ts.map +1 -0
- package/dist/ScriptContext.js +21 -0
- package/dist/ScriptContext.js.map +1 -0
- package/dist/StateMachine.d.ts +52 -0
- package/dist/StateMachine.d.ts.map +1 -0
- package/dist/StateMachine.js +117 -0
- package/dist/StateMachine.js.map +1 -0
- package/dist/UtilityAI.d.ts +55 -0
- package/dist/UtilityAI.d.ts.map +1 -0
- package/dist/UtilityAI.js +113 -0
- package/dist/UtilityAI.js.map +1 -0
- package/dist/VisualGraph.d.ts +20 -0
- package/dist/VisualGraph.d.ts.map +1 -0
- package/dist/VisualGraph.js +191 -0
- package/dist/VisualGraph.js.map +1 -0
- package/dist/VisualGraphContext.d.ts +123 -0
- package/dist/VisualGraphContext.d.ts.map +1 -0
- package/dist/VisualGraphContext.js +2 -0
- package/dist/VisualGraphContext.js.map +1 -0
- package/dist/VisualGraphExecutor.d.ts +16 -0
- package/dist/VisualGraphExecutor.d.ts.map +1 -0
- package/dist/VisualGraphExecutor.js +571 -0
- package/dist/VisualGraphExecutor.js.map +1 -0
- package/dist/VisualNode.d.ts +17 -0
- package/dist/VisualNode.d.ts.map +1 -0
- package/dist/VisualNode.js +24 -0
- package/dist/VisualNode.js.map +1 -0
- package/dist/VisualNodeCatalog.d.ts +14 -0
- package/dist/VisualNodeCatalog.d.ts.map +1 -0
- package/dist/VisualNodeCatalog.js +142 -0
- package/dist/VisualNodeCatalog.js.map +1 -0
- package/dist/WeaponSystem.d.ts +228 -0
- package/dist/WeaponSystem.d.ts.map +1 -0
- package/dist/WeaponSystem.js +530 -0
- package/dist/WeaponSystem.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +13 -0
- package/src/AdaptiveDifficultyFixtures.ts +324 -0
- package/src/AnalyticsPrivacyFixtures.ts +182 -0
- package/src/Behavior.ts +11 -0
- package/src/BehaviorHost.ts +51 -0
- package/src/BehaviorRegistry.ts +22 -0
- package/src/BehaviorSystem.ts +103 -0
- package/src/BehaviorTree.ts +234 -0
- package/src/CloudServiceFixtures.ts +225 -0
- package/src/CulturalBehaviorFixtures.ts +262 -0
- package/src/DecisionTree.ts +162 -0
- package/src/GOAP.ts +150 -0
- package/src/HTN.ts +178 -0
- package/src/LearningAgentFixtures.ts +184 -0
- package/src/NetworkReplicationFixtures.ts +440 -0
- package/src/Perception.ts +163 -0
- package/src/PlayerBehaviorTelemetryFixtures.ts +257 -0
- package/src/ProceduralContentAdaptationFixtures.ts +205 -0
- package/src/ScriptContext.ts +34 -0
- package/src/StateMachine.ts +138 -0
- package/src/UtilityAI.ts +158 -0
- package/src/VisualGraph.ts +236 -0
- package/src/VisualGraphContext.ts +138 -0
- package/src/VisualGraphExecutor.ts +663 -0
- package/src/VisualNode.ts +44 -0
- package/src/VisualNodeCatalog.ts +195 -0
- package/src/WeaponSystem.ts +793 -0
- package/src/index.ts +175 -0
- package/tests/scripting.test.ts +229 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { validateGraph, type VisualEdge, type VisualGraph } from "./VisualGraph";
|
|
2
|
+
import {
|
|
3
|
+
type VisualAnimationControllerState,
|
|
4
|
+
type VisualAnimationEvent,
|
|
5
|
+
type VisualCollisionEvent,
|
|
6
|
+
type VisualCombatEvent,
|
|
7
|
+
type VisualGraphDiagnostic,
|
|
8
|
+
type VisualGraphExecutionContext,
|
|
9
|
+
type VisualGraphSideEffect,
|
|
10
|
+
type VisualInputSet,
|
|
11
|
+
type VisualStateCollection,
|
|
12
|
+
type VisualVector3
|
|
13
|
+
} from "./VisualGraphContext";
|
|
14
|
+
import { getVisualNodeDefinition } from "./VisualNodeCatalog";
|
|
15
|
+
import { type VisualNode } from "./VisualNode";
|
|
16
|
+
|
|
17
|
+
const visualNodeOutputsBrand: unique symbol = Symbol("VisualNodeOutputs");
|
|
18
|
+
|
|
19
|
+
export interface VisualExecutionResult {
|
|
20
|
+
readonly values: ReadonlyMap<string, unknown>;
|
|
21
|
+
readonly executionOrder: readonly string[];
|
|
22
|
+
readonly nodeKinds: readonly string[];
|
|
23
|
+
readonly sideEffects: readonly VisualGraphSideEffect[];
|
|
24
|
+
readonly diagnostics: readonly VisualGraphDiagnostic[];
|
|
25
|
+
readonly blockedClaims: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class VisualGraphExecutor {
|
|
29
|
+
private readonly defaultContext: VisualGraphExecutionContext;
|
|
30
|
+
|
|
31
|
+
constructor(context: VisualGraphExecutionContext = {}) {
|
|
32
|
+
this.defaultContext = context;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
execute(graph: VisualGraph, context: VisualGraphExecutionContext = this.defaultContext): VisualExecutionResult {
|
|
36
|
+
const errors = validateGraph(graph, { context });
|
|
37
|
+
if (errors.length > 0) {
|
|
38
|
+
throw new Error(errors.join("; "));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const values = new Map<string, unknown>();
|
|
42
|
+
const executionOrder: string[] = [];
|
|
43
|
+
const sideEffects: VisualGraphSideEffect[] = [];
|
|
44
|
+
const diagnostics: VisualGraphDiagnostic[] = [];
|
|
45
|
+
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
46
|
+
const visiting = new Set<string>();
|
|
47
|
+
const visited = new Set<string>();
|
|
48
|
+
|
|
49
|
+
const executeNode = (nodeId: string): void => {
|
|
50
|
+
if (visited.has(nodeId)) return;
|
|
51
|
+
if (visiting.has(nodeId)) {
|
|
52
|
+
throw new Error(`Visual graph cycle detected at node: ${nodeId}`);
|
|
53
|
+
}
|
|
54
|
+
const node = nodes.get(nodeId);
|
|
55
|
+
if (!node) {
|
|
56
|
+
throw new Error(`Missing visual node: ${nodeId}`);
|
|
57
|
+
}
|
|
58
|
+
visiting.add(nodeId);
|
|
59
|
+
for (const edge of graph.edges.filter((candidate) => candidate.toNode === node.id)) {
|
|
60
|
+
executeNode(edge.fromNode);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setNodeOutput(values, node, executeVisualNode(node, graph.edges.filter((edge) => edge.toNode === node.id), values, context, sideEffects, diagnostics));
|
|
64
|
+
|
|
65
|
+
visiting.delete(nodeId);
|
|
66
|
+
visited.add(nodeId);
|
|
67
|
+
executionOrder.push(nodeId);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const node of graph.nodes) {
|
|
71
|
+
executeNode(node.id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
values,
|
|
76
|
+
executionOrder,
|
|
77
|
+
nodeKinds: [...new Set(graph.nodes.map((node) => node.kind))].sort(),
|
|
78
|
+
sideEffects,
|
|
79
|
+
diagnostics,
|
|
80
|
+
blockedClaims: [
|
|
81
|
+
"Unity Visual Scripting parity",
|
|
82
|
+
"Unreal Blueprint parity",
|
|
83
|
+
"async latent action graph parity",
|
|
84
|
+
"editor-authored visual scripting parity",
|
|
85
|
+
"live engine installGraph bridge parity"
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function executeVisualNode(
|
|
92
|
+
node: VisualNode,
|
|
93
|
+
inputEdges: readonly VisualEdge[],
|
|
94
|
+
values: Map<string, unknown>,
|
|
95
|
+
context: VisualGraphExecutionContext,
|
|
96
|
+
sideEffects: VisualGraphSideEffect[],
|
|
97
|
+
diagnostics: VisualGraphDiagnostic[]
|
|
98
|
+
): unknown {
|
|
99
|
+
const definition = getVisualNodeDefinition(node.kind);
|
|
100
|
+
if (!definition) throw new Error(`Unsupported visual node kind: ${node.kind}`);
|
|
101
|
+
const input = (port: string): unknown => getInputValue(node, port, inputEdges, values);
|
|
102
|
+
const number = (port: string): number => finiteNumber(input(port));
|
|
103
|
+
const boolean = (port: string): boolean => Boolean(input(port));
|
|
104
|
+
const string = (port: string): string => String(input(port) ?? "");
|
|
105
|
+
const optionalString = (port: string): string | undefined => {
|
|
106
|
+
const value = input(port);
|
|
107
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
108
|
+
};
|
|
109
|
+
const object = (port: string): unknown => stableClone(input(port) ?? {});
|
|
110
|
+
|
|
111
|
+
switch (node.kind) {
|
|
112
|
+
case "const":
|
|
113
|
+
return node.data?.value;
|
|
114
|
+
case "log":
|
|
115
|
+
return input("in");
|
|
116
|
+
case "add":
|
|
117
|
+
return inputEdges.length > 1
|
|
118
|
+
? inputEdges.reduce((total, edge) => total + finiteNumber(readEdgeValue(edge, values)), 0)
|
|
119
|
+
: number("in");
|
|
120
|
+
case "subtract":
|
|
121
|
+
return number("a") - number("b");
|
|
122
|
+
case "multiply":
|
|
123
|
+
return number("a") * number("b");
|
|
124
|
+
case "divide": {
|
|
125
|
+
const divisor = number("b");
|
|
126
|
+
if (divisor === 0) throw new Error(`Visual node ${node.id} divide by zero.`);
|
|
127
|
+
return number("a") / divisor;
|
|
128
|
+
}
|
|
129
|
+
case "power":
|
|
130
|
+
return Math.pow(number("base"), number("exponent"));
|
|
131
|
+
case "sqrt": {
|
|
132
|
+
const value = number("value");
|
|
133
|
+
if (value < 0) throw new Error(`Visual node ${node.id} cannot calculate square root of a negative number.`);
|
|
134
|
+
return Math.sqrt(value);
|
|
135
|
+
}
|
|
136
|
+
case "abs":
|
|
137
|
+
return Math.abs(number("value"));
|
|
138
|
+
case "sin":
|
|
139
|
+
return Math.sin(number("angle"));
|
|
140
|
+
case "cos":
|
|
141
|
+
return Math.cos(number("angle"));
|
|
142
|
+
case "tan":
|
|
143
|
+
return Math.tan(number("angle"));
|
|
144
|
+
case "min":
|
|
145
|
+
return Math.min(number("a"), number("b"));
|
|
146
|
+
case "max":
|
|
147
|
+
return Math.max(number("a"), number("b"));
|
|
148
|
+
case "clamp":
|
|
149
|
+
return Math.max(number("min"), Math.min(number("max"), number("value")));
|
|
150
|
+
case "lerp":
|
|
151
|
+
return number("a") + (number("b") - number("a")) * number("t");
|
|
152
|
+
case "inverseLerp": {
|
|
153
|
+
const a = number("a");
|
|
154
|
+
const b = number("b");
|
|
155
|
+
if (a === b) throw new Error(`Visual node ${node.id} inverseLerp requires different a and b values.`);
|
|
156
|
+
return (number("value") - a) / (b - a);
|
|
157
|
+
}
|
|
158
|
+
case "and":
|
|
159
|
+
return boolean("a") && boolean("b");
|
|
160
|
+
case "or":
|
|
161
|
+
return boolean("a") || boolean("b");
|
|
162
|
+
case "not":
|
|
163
|
+
return !boolean("value");
|
|
164
|
+
case "xor":
|
|
165
|
+
return boolean("a") !== boolean("b");
|
|
166
|
+
case "equal":
|
|
167
|
+
return input("a") === input("b");
|
|
168
|
+
case "notEqual":
|
|
169
|
+
return input("a") !== input("b");
|
|
170
|
+
case "greater":
|
|
171
|
+
return number("a") > number("b");
|
|
172
|
+
case "less":
|
|
173
|
+
return number("a") < number("b");
|
|
174
|
+
case "greaterEqual":
|
|
175
|
+
return number("a") >= number("b");
|
|
176
|
+
case "lessEqual":
|
|
177
|
+
return number("a") <= number("b");
|
|
178
|
+
case "isNull": {
|
|
179
|
+
const value = input("value");
|
|
180
|
+
return value === null || value === undefined;
|
|
181
|
+
}
|
|
182
|
+
case "isValid": {
|
|
183
|
+
const value = input("value");
|
|
184
|
+
return value !== null && value !== undefined;
|
|
185
|
+
}
|
|
186
|
+
case "select":
|
|
187
|
+
return boolean("condition") ? input("ifTrue") : input("ifFalse");
|
|
188
|
+
case "branch": {
|
|
189
|
+
const selected = boolean("condition") ? "true" : "false";
|
|
190
|
+
return outputs(selected, {
|
|
191
|
+
true: selected === "true",
|
|
192
|
+
false: selected === "false",
|
|
193
|
+
selected
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
case "switch": {
|
|
197
|
+
const caseCount = Math.max(1, Math.min(3, Math.floor(number("caseCount"))));
|
|
198
|
+
const selectedIndex = Math.floor(number("value"));
|
|
199
|
+
const selected = selectedIndex >= 0 && selectedIndex < caseCount ? `case${selectedIndex}` : "default";
|
|
200
|
+
return outputs(selected, {
|
|
201
|
+
case0: selected === "case0",
|
|
202
|
+
case1: selected === "case1",
|
|
203
|
+
case2: selected === "case2",
|
|
204
|
+
default: selected === "default",
|
|
205
|
+
selected
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
case "sequence": {
|
|
209
|
+
const count = Math.max(1, Math.min(3, Math.floor(number("count"))));
|
|
210
|
+
const orderedOutputs = Array.from({ length: count }, (_, index) => `out${index}`);
|
|
211
|
+
return outputs(orderedOutputs, {
|
|
212
|
+
out0: count > 0,
|
|
213
|
+
out1: count > 1,
|
|
214
|
+
out2: count > 2,
|
|
215
|
+
outputs: orderedOutputs
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
case "forRange": {
|
|
219
|
+
const startIndex = Math.floor(number("startIndex"));
|
|
220
|
+
const endIndex = Math.floor(number("endIndex"));
|
|
221
|
+
const iterations = Math.max(0, Math.min(256, endIndex - startIndex));
|
|
222
|
+
const indices = Array.from({ length: iterations }, (_, index) => startIndex + index);
|
|
223
|
+
return outputs(indices, {
|
|
224
|
+
body: iterations > 0,
|
|
225
|
+
completed: true,
|
|
226
|
+
index: indices[0] ?? startIndex,
|
|
227
|
+
indices
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
case "gate": {
|
|
231
|
+
const isOpen = !boolean("startClosed");
|
|
232
|
+
return outputs(isOpen ? "out" : undefined, {
|
|
233
|
+
out: isOpen,
|
|
234
|
+
isOpen
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case "onStart": {
|
|
239
|
+
const active = (context.frame ?? 0) === 0 && (context.time ?? 0) === 0;
|
|
240
|
+
return outputs(active, { out: active, active });
|
|
241
|
+
}
|
|
242
|
+
case "onFrame": {
|
|
243
|
+
const frame = { dt: context.dt ?? 0, time: context.time ?? 0, frame: context.frame ?? 0 };
|
|
244
|
+
return outputs(frame, { out: true, dt: frame.dt, time: frame.time, frame: frame.frame, context: frame });
|
|
245
|
+
}
|
|
246
|
+
case "getNode": {
|
|
247
|
+
const nodeId = string("nodeId");
|
|
248
|
+
const runtimeNode = collectionGet(context.runtimeNodes, nodeId);
|
|
249
|
+
if (!runtimeNode) {
|
|
250
|
+
diagnostics.push({ level: "warning", code: "runtime.nodeMissing", message: `Runtime node not found: ${nodeId}`, nodeId: node.id });
|
|
251
|
+
}
|
|
252
|
+
return outputs(stableClone(runtimeNode ?? null), {
|
|
253
|
+
node: stableClone(runtimeNode ?? null),
|
|
254
|
+
nodeId,
|
|
255
|
+
exists: runtimeNode !== undefined,
|
|
256
|
+
position: normalizeVector(runtimeNode?.position)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
case "setPosition":
|
|
260
|
+
return command("runtime.setPosition", node, context, sideEffects, string("nodeId"), {
|
|
261
|
+
nodeId: string("nodeId"),
|
|
262
|
+
position: normalizeVector(input("position"))
|
|
263
|
+
});
|
|
264
|
+
case "translate": {
|
|
265
|
+
const nodeId = string("nodeId");
|
|
266
|
+
const current = normalizeVector(collectionGet(context.runtimeNodes, nodeId)?.position);
|
|
267
|
+
const delta = normalizeVector(input("delta"));
|
|
268
|
+
return command("runtime.translate", node, context, sideEffects, nodeId, {
|
|
269
|
+
nodeId,
|
|
270
|
+
from: current,
|
|
271
|
+
delta,
|
|
272
|
+
position: addVectors(current, delta)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
case "rotate":
|
|
276
|
+
return command("runtime.rotate", node, context, sideEffects, string("nodeId"), {
|
|
277
|
+
nodeId: string("nodeId"),
|
|
278
|
+
rotation: normalizeVector(input("rotation"))
|
|
279
|
+
});
|
|
280
|
+
case "setVisible":
|
|
281
|
+
return command("runtime.setVisible", node, context, sideEffects, string("nodeId"), {
|
|
282
|
+
nodeId: string("nodeId"),
|
|
283
|
+
visible: boolean("visible")
|
|
284
|
+
});
|
|
285
|
+
case "setMaterial":
|
|
286
|
+
return command("runtime.setMaterial", node, context, sideEffects, string("nodeId"), {
|
|
287
|
+
nodeId: string("nodeId"),
|
|
288
|
+
material: object("material")
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
case "pressed":
|
|
292
|
+
return inputFlag(context.input?.pressed, string("action"));
|
|
293
|
+
case "held":
|
|
294
|
+
return inputFlag(context.input?.held, string("action"));
|
|
295
|
+
case "released":
|
|
296
|
+
return inputFlag(context.input?.released, string("action"));
|
|
297
|
+
case "axis":
|
|
298
|
+
return finiteNumber(context.input?.axes?.[string("axis")]);
|
|
299
|
+
case "buffered":
|
|
300
|
+
return inputFlag(context.input?.buffered, string("action"));
|
|
301
|
+
case "combo":
|
|
302
|
+
return inputFlag(context.input?.combos, string("combo"));
|
|
303
|
+
|
|
304
|
+
case "playClip":
|
|
305
|
+
return command("animation.playClip", node, context, sideEffects, string("controllerId"), {
|
|
306
|
+
controllerId: string("controllerId"),
|
|
307
|
+
clip: string("clip"),
|
|
308
|
+
loop: boolean("loop")
|
|
309
|
+
});
|
|
310
|
+
case "restartClip":
|
|
311
|
+
return command("animation.restartClip", node, context, sideEffects, string("controllerId"), {
|
|
312
|
+
controllerId: string("controllerId"),
|
|
313
|
+
clip: string("clip"),
|
|
314
|
+
restart: true
|
|
315
|
+
});
|
|
316
|
+
case "crossFade":
|
|
317
|
+
return command("animation.crossFade", node, context, sideEffects, string("controllerId"), {
|
|
318
|
+
controllerId: string("controllerId"),
|
|
319
|
+
clip: string("clip"),
|
|
320
|
+
duration: number("duration"),
|
|
321
|
+
restart: boolean("restart"),
|
|
322
|
+
layer: optionalString("layer")
|
|
323
|
+
});
|
|
324
|
+
case "setLayerWeight":
|
|
325
|
+
return command("animation.setLayerWeight", node, context, sideEffects, string("controllerId"), {
|
|
326
|
+
controllerId: string("controllerId"),
|
|
327
|
+
layer: string("layer"),
|
|
328
|
+
weight: number("weight")
|
|
329
|
+
});
|
|
330
|
+
case "onAnimationEvent": {
|
|
331
|
+
const event = findAnimationEvent(context, string("controllerId"), string("eventType"), optionalString("clip"));
|
|
332
|
+
return outputs(stableClone(event ?? null), {
|
|
333
|
+
out: event !== undefined,
|
|
334
|
+
fired: event !== undefined,
|
|
335
|
+
event: stableClone(event ?? null)
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
case "setMorphTarget": {
|
|
339
|
+
const morphTarget = string("morphTarget");
|
|
340
|
+
const weight = number("weight");
|
|
341
|
+
return command("animation.setMorphTarget", node, context, sideEffects, string("controllerId"), {
|
|
342
|
+
controllerId: string("controllerId"),
|
|
343
|
+
morphTarget,
|
|
344
|
+
weight,
|
|
345
|
+
weights: { [morphTarget]: weight }
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
case "setMorphTargets":
|
|
349
|
+
return command("animation.setMorphTargets", node, context, sideEffects, string("controllerId"), {
|
|
350
|
+
controllerId: string("controllerId"),
|
|
351
|
+
weights: object("weights")
|
|
352
|
+
});
|
|
353
|
+
case "getClipTime": {
|
|
354
|
+
const controller = collectionGet(context.animationControllers, string("controllerId"));
|
|
355
|
+
return outputs(controller?.clipTime ?? 0, {
|
|
356
|
+
out: controller?.clipTime ?? 0,
|
|
357
|
+
clip: controller?.currentClip ?? ""
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case "setVelocity":
|
|
362
|
+
return command("physics.setVelocity", node, context, sideEffects, string("bodyId"), {
|
|
363
|
+
bodyId: string("bodyId"),
|
|
364
|
+
velocity: normalizeVector(input("velocity"))
|
|
365
|
+
});
|
|
366
|
+
case "jump":
|
|
367
|
+
return command("physics.jump", node, context, sideEffects, string("bodyId"), {
|
|
368
|
+
bodyId: string("bodyId"),
|
|
369
|
+
impulse: number("impulse")
|
|
370
|
+
});
|
|
371
|
+
case "dash":
|
|
372
|
+
return command("physics.dash", node, context, sideEffects, string("bodyId"), {
|
|
373
|
+
bodyId: string("bodyId"),
|
|
374
|
+
direction: normalizeVector(input("direction")),
|
|
375
|
+
speed: number("speed")
|
|
376
|
+
});
|
|
377
|
+
case "onCollisionEnter": {
|
|
378
|
+
const event = findCollisionEvent(context.collisionEvents, "enter", string("bodyId"), optionalString("otherBodyId"));
|
|
379
|
+
return outputs(stableClone(event ?? null), { out: event !== undefined, collided: event !== undefined, event: stableClone(event ?? null) });
|
|
380
|
+
}
|
|
381
|
+
case "onCollisionExit": {
|
|
382
|
+
const event = findCollisionEvent(context.collisionEvents, "exit", string("bodyId"), optionalString("otherBodyId"));
|
|
383
|
+
return outputs(stableClone(event ?? null), { out: event !== undefined, collided: event !== undefined, event: stableClone(event ?? null) });
|
|
384
|
+
}
|
|
385
|
+
case "raycast": {
|
|
386
|
+
const hit = collectionGet(context.raycastHits, string("queryId"));
|
|
387
|
+
return outputs(stableClone(hit ?? null), { hit: hit?.hit === true, result: stableClone(hit ?? null) });
|
|
388
|
+
}
|
|
389
|
+
case "overlap": {
|
|
390
|
+
const overlap = collectionGet(context.overlaps, string("queryId"));
|
|
391
|
+
return outputs(stableClone(overlap ?? null), { hit: (overlap?.bodyIds.length ?? 0) > 0, result: stableClone(overlap ?? null) });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case "openHitbox":
|
|
395
|
+
return command("combat.openHitbox", node, context, sideEffects, string("hitboxId"), {
|
|
396
|
+
hitboxId: string("hitboxId"),
|
|
397
|
+
ownerId: string("ownerId"),
|
|
398
|
+
damage: number("damage"),
|
|
399
|
+
payload: object("payload")
|
|
400
|
+
});
|
|
401
|
+
case "closeHitbox":
|
|
402
|
+
return command("combat.closeHitbox", node, context, sideEffects, string("hitboxId"), {
|
|
403
|
+
hitboxId: string("hitboxId")
|
|
404
|
+
});
|
|
405
|
+
case "setHurtbox":
|
|
406
|
+
return command("combat.setHurtbox", node, context, sideEffects, string("hurtboxId"), {
|
|
407
|
+
hurtboxId: string("hurtboxId"),
|
|
408
|
+
ownerId: string("ownerId"),
|
|
409
|
+
payload: object("payload")
|
|
410
|
+
});
|
|
411
|
+
case "onHit": {
|
|
412
|
+
const event = findCombatHit(context.combatEvents, optionalString("actorId"), optionalString("hitboxId"));
|
|
413
|
+
return outputs(stableClone(event ?? null), { out: event !== undefined, hit: event !== undefined, event: stableClone(event ?? null) });
|
|
414
|
+
}
|
|
415
|
+
case "applyDamage":
|
|
416
|
+
return command("combat.applyDamage", node, context, sideEffects, string("targetId"), {
|
|
417
|
+
targetId: string("targetId"),
|
|
418
|
+
amount: number("amount"),
|
|
419
|
+
sourceId: optionalString("sourceId")
|
|
420
|
+
});
|
|
421
|
+
case "applyKnockback":
|
|
422
|
+
return command("combat.applyKnockback", node, context, sideEffects, string("targetId"), {
|
|
423
|
+
targetId: string("targetId"),
|
|
424
|
+
velocity: normalizeVector(input("velocity")),
|
|
425
|
+
sourceId: optionalString("sourceId")
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
case "follow":
|
|
429
|
+
return command("camera.follow", node, context, sideEffects, string("targetId"), {
|
|
430
|
+
targetId: string("targetId"),
|
|
431
|
+
stiffness: number("stiffness")
|
|
432
|
+
});
|
|
433
|
+
case "frameTargets":
|
|
434
|
+
return command("camera.frameTargets", node, context, sideEffects, undefined, {
|
|
435
|
+
targetIds: object("targetIds"),
|
|
436
|
+
padding: number("padding")
|
|
437
|
+
});
|
|
438
|
+
case "shake":
|
|
439
|
+
return command("camera.shake", node, context, sideEffects, context.camera?.id, {
|
|
440
|
+
intensity: number("intensity"),
|
|
441
|
+
duration: number("duration")
|
|
442
|
+
});
|
|
443
|
+
case "cutTo":
|
|
444
|
+
return command("camera.cutTo", node, context, sideEffects, context.camera?.id, {
|
|
445
|
+
position: normalizeVector(input("position")),
|
|
446
|
+
target: stableClone(input("target") ?? null)
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
case "captureSnapshot": {
|
|
450
|
+
const snapshot = cleanObject({
|
|
451
|
+
kind: "evidence.snapshot",
|
|
452
|
+
label: optionalString("label") ?? node.id,
|
|
453
|
+
frame: context.frame,
|
|
454
|
+
time: context.time,
|
|
455
|
+
dt: context.dt,
|
|
456
|
+
runtimeNodes: context.runtimeNodes,
|
|
457
|
+
animationControllers: context.animationControllers,
|
|
458
|
+
physicsBodies: context.physicsBodies,
|
|
459
|
+
camera: context.camera,
|
|
460
|
+
evidence: context.evidence
|
|
461
|
+
});
|
|
462
|
+
pushSideEffect(sideEffects, "evidence.captureSnapshot", node, context, optionalString("label"), snapshot);
|
|
463
|
+
return outputs(snapshot, { out: true, snapshot });
|
|
464
|
+
}
|
|
465
|
+
case "markProof":
|
|
466
|
+
return command("evidence.markProof", node, context, sideEffects, string("proofId"), {
|
|
467
|
+
proofId: string("proofId"),
|
|
468
|
+
details: object("details")
|
|
469
|
+
});
|
|
470
|
+
case "assertState": {
|
|
471
|
+
const operator = optionalString("operator") ?? "equals";
|
|
472
|
+
const actual = input("actual");
|
|
473
|
+
const expected = input("expected");
|
|
474
|
+
const passed = evaluateAssertion(actual, expected, operator);
|
|
475
|
+
const assertion = cleanObject({
|
|
476
|
+
kind: "evidence.assertState",
|
|
477
|
+
operator,
|
|
478
|
+
actual,
|
|
479
|
+
expected,
|
|
480
|
+
passed
|
|
481
|
+
});
|
|
482
|
+
pushSideEffect(sideEffects, "evidence.assertState", node, context, undefined, assertion);
|
|
483
|
+
return outputs(assertion, { out: passed, passed, assertion });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
throw new Error(`Unsupported visual node kind: ${node.kind}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getInputValue(node: VisualNode, portId: string, inputEdges: readonly VisualEdge[], values: Map<string, unknown>): unknown {
|
|
491
|
+
const edge = inputEdges.find((candidate) => candidate.toPort === portId);
|
|
492
|
+
if (edge) return readEdgeValue(edge, values);
|
|
493
|
+
const dataValue = node.data?.[portId];
|
|
494
|
+
if (dataValue !== undefined) return dataValue;
|
|
495
|
+
return node.ports.find((port) => port.id === portId)?.defaultValue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function readEdgeValue(edge: VisualEdge, values: Map<string, unknown>): unknown {
|
|
499
|
+
return values.get(`${edge.fromNode}.${edge.fromPort}`) ?? values.get(edge.fromNode);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function setNodeOutput(values: Map<string, unknown>, node: VisualNode, value: unknown): void {
|
|
503
|
+
const resolved = isOutputMap(value) ? value : outputs(value);
|
|
504
|
+
values.set(node.id, resolved.value);
|
|
505
|
+
for (const port of node.ports.filter((entry) => entry.direction === "output")) {
|
|
506
|
+
values.set(`${node.id}.${port.id}`, resolved.byPort?.[port.id] ?? resolved.value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function outputs(value: unknown, byPort?: Readonly<Record<string, unknown>>): VisualNodeOutputs {
|
|
511
|
+
return { [visualNodeOutputsBrand]: true, value, byPort };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isOutputMap(value: unknown): value is VisualNodeOutputs {
|
|
515
|
+
return typeof value === "object" && value !== null && (value as VisualNodeOutputs)[visualNodeOutputsBrand] === true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
interface VisualNodeOutputs {
|
|
519
|
+
readonly [visualNodeOutputsBrand]: true;
|
|
520
|
+
readonly value: unknown;
|
|
521
|
+
readonly byPort?: Readonly<Record<string, unknown>>;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function finiteNumber(value: unknown): number {
|
|
525
|
+
const numeric = Number(value ?? 0);
|
|
526
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function inputFlag(source: VisualInputSet | undefined, key: string): boolean {
|
|
530
|
+
if (!source || key.length === 0) return false;
|
|
531
|
+
return Array.isArray(source) ? source.includes(key) : (source as Readonly<Record<string, boolean>>)[key] === true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeVector(value: unknown, fallback: Required<Record<"x" | "y" | "z", number>> = { x: 0, y: 0, z: 0 }): Required<Record<"x" | "y" | "z", number>> {
|
|
535
|
+
if (Array.isArray(value)) {
|
|
536
|
+
return { x: finiteNumber(value[0] ?? fallback.x), y: finiteNumber(value[1] ?? fallback.y), z: finiteNumber(value[2] ?? fallback.z) };
|
|
537
|
+
}
|
|
538
|
+
if (typeof value === "object" && value !== null) {
|
|
539
|
+
const vector = value as VisualVector3 & Record<string, unknown>;
|
|
540
|
+
return {
|
|
541
|
+
x: finiteNumber(vector.x ?? fallback.x),
|
|
542
|
+
y: finiteNumber(vector.y ?? fallback.y),
|
|
543
|
+
z: finiteNumber(vector.z ?? fallback.z)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return { ...fallback };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function addVectors(a: Required<Record<"x" | "y" | "z", number>>, b: Required<Record<"x" | "y" | "z", number>>): Required<Record<"x" | "y" | "z", number>> {
|
|
550
|
+
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function command(
|
|
554
|
+
kind: string,
|
|
555
|
+
node: VisualNode,
|
|
556
|
+
context: VisualGraphExecutionContext,
|
|
557
|
+
sideEffects: VisualGraphSideEffect[],
|
|
558
|
+
target: string | undefined,
|
|
559
|
+
payload: Readonly<Record<string, unknown>>
|
|
560
|
+
): VisualNodeOutputs {
|
|
561
|
+
const commandPayload = cleanObject({ kind, target, ...payload });
|
|
562
|
+
pushSideEffect(sideEffects, kind, node, context, target, commandPayload);
|
|
563
|
+
return outputs(commandPayload, { out: true, command: commandPayload });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function pushSideEffect(
|
|
567
|
+
sideEffects: VisualGraphSideEffect[],
|
|
568
|
+
kind: string,
|
|
569
|
+
node: VisualNode,
|
|
570
|
+
context: VisualGraphExecutionContext,
|
|
571
|
+
target: string | undefined,
|
|
572
|
+
payload: unknown
|
|
573
|
+
): void {
|
|
574
|
+
sideEffects.push(cleanObject({
|
|
575
|
+
kind,
|
|
576
|
+
nodeId: node.id,
|
|
577
|
+
target,
|
|
578
|
+
frame: context.frame,
|
|
579
|
+
time: context.time,
|
|
580
|
+
payload
|
|
581
|
+
}) as VisualGraphSideEffect);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function collectionGet<T extends { readonly id: string }>(collection: VisualStateCollection<T> | undefined, id: string): T | undefined {
|
|
585
|
+
if (!collection || id.length === 0) return undefined;
|
|
586
|
+
return Array.isArray(collection) ? collection.find((entry) => entry.id === id) : (collection as Readonly<Record<string, T>>)[id];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function findAnimationEvent(
|
|
590
|
+
context: VisualGraphExecutionContext,
|
|
591
|
+
controllerId: string,
|
|
592
|
+
eventType: string,
|
|
593
|
+
clip: string | undefined
|
|
594
|
+
): VisualAnimationEvent | undefined {
|
|
595
|
+
const controller = collectionGet<VisualAnimationControllerState>(context.animationControllers, controllerId);
|
|
596
|
+
const events = [...(context.animationEvents ?? []), ...(controller?.events ?? [])];
|
|
597
|
+
return events.find((event) =>
|
|
598
|
+
event.type === eventType &&
|
|
599
|
+
(event.controllerId === undefined || event.controllerId === controllerId) &&
|
|
600
|
+
(clip === undefined || event.clip === undefined || event.clip === clip)
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function findCollisionEvent(
|
|
605
|
+
events: readonly VisualCollisionEvent[] | undefined,
|
|
606
|
+
type: VisualCollisionEvent["type"],
|
|
607
|
+
bodyId: string,
|
|
608
|
+
otherBodyId: string | undefined
|
|
609
|
+
): VisualCollisionEvent | undefined {
|
|
610
|
+
return events?.find((event) =>
|
|
611
|
+
event.type === type &&
|
|
612
|
+
(event.bodyId === bodyId || event.otherBodyId === bodyId) &&
|
|
613
|
+
(otherBodyId === undefined || event.bodyId === otherBodyId || event.otherBodyId === otherBodyId)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function findCombatHit(events: readonly VisualCombatEvent[] | undefined, actorId: string | undefined, hitboxId: string | undefined): VisualCombatEvent | undefined {
|
|
618
|
+
return events?.find((event) =>
|
|
619
|
+
event.type === "hit" &&
|
|
620
|
+
(actorId === undefined || event.actorId === actorId || event.targetId === actorId) &&
|
|
621
|
+
(hitboxId === undefined || event.hitboxId === hitboxId)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function evaluateAssertion(actual: unknown, expected: unknown, operator: string): boolean {
|
|
626
|
+
switch (operator) {
|
|
627
|
+
case "equals":
|
|
628
|
+
return stableStringify(actual) === stableStringify(expected);
|
|
629
|
+
case "notEquals":
|
|
630
|
+
return stableStringify(actual) !== stableStringify(expected);
|
|
631
|
+
case "truthy":
|
|
632
|
+
return Boolean(actual);
|
|
633
|
+
case "falsy":
|
|
634
|
+
return !actual;
|
|
635
|
+
default:
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function stableClone<T>(value: T): T {
|
|
641
|
+
return cleanObject(value) as T;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function cleanObject(value: unknown): unknown {
|
|
645
|
+
if (Array.isArray(value)) {
|
|
646
|
+
return value.map((entry) => cleanObject(entry));
|
|
647
|
+
}
|
|
648
|
+
if (typeof value === "object" && value !== null) {
|
|
649
|
+
const output: Record<string, unknown> = {};
|
|
650
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
651
|
+
const entry = (value as Record<string, unknown>)[key];
|
|
652
|
+
if (entry !== undefined) {
|
|
653
|
+
output[key] = cleanObject(entry);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return output;
|
|
657
|
+
}
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function stableStringify(value: unknown): string {
|
|
662
|
+
return JSON.stringify(cleanObject(value));
|
|
663
|
+
}
|