incanto 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1205 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.js +0 -10958
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { L as handlerShadowWarnings, R as sceneDimension, V as HudWidgetBase, Z as nodeRefWarnings } from "./save-slots-Bvuh2p_r.js";
|
|
2
|
+
import { N as translationKey } from "./register-DuVdzxbq.js";
|
|
3
|
+
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
+
import { u as getNodeType } from "./touch-BnCyPA0G.js";
|
|
4
5
|
//#region src/core/audit.ts
|
|
6
|
+
/**
|
|
7
|
+
* Behaviours whose ENTIRE observable effect is the signals they emit.
|
|
8
|
+
*
|
|
9
|
+
* Membership is the claim, so it is written down rather than guessed: a
|
|
10
|
+
* behaviour belongs here only if attaching it and wiring none of its signals
|
|
11
|
+
* leaves the game bit-identical. `Clickable` qualifies — it picks, it hovers,
|
|
12
|
+
* and it tells you; it changes nothing itself. `Pickup` does not (it frees
|
|
13
|
+
* itself), nor does `Health` (it holds the number), nor `Interactable` (it
|
|
14
|
+
* gates on range and shows a prompt).
|
|
15
|
+
*/
|
|
16
|
+
const SIGNAL_ONLY = new Map([["Clickable", [
|
|
17
|
+
"clicked",
|
|
18
|
+
"hovered",
|
|
19
|
+
"unhovered"
|
|
20
|
+
]]]);
|
|
5
21
|
const ANIMATED_TYPES = new Set([
|
|
6
22
|
"Particles2D",
|
|
7
23
|
"Particles3D",
|
|
@@ -48,11 +64,14 @@ function isHudWidget(type) {
|
|
|
48
64
|
}
|
|
49
65
|
return false;
|
|
50
66
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
67
|
+
/** The node types that auto-find a terrain when their `terrain` path is empty. */
|
|
68
|
+
const DRAPERS = new Set([
|
|
69
|
+
"Foliage3D",
|
|
70
|
+
"Flowers3D",
|
|
71
|
+
"Tree3D",
|
|
72
|
+
"River3D"
|
|
55
73
|
]);
|
|
74
|
+
const LIGHT_TYPES = new Set(["DirectionalLight3D", "OmniLight3D"]);
|
|
56
75
|
/**
|
|
57
76
|
* Nodes whose gameplay answer comes from a CPU twin built out of `size`.
|
|
58
77
|
*
|
|
@@ -62,6 +81,7 @@ const LIGHT_TYPES = new Set([
|
|
|
62
81
|
*/
|
|
63
82
|
const WORLD_QUERY_TYPES = new Set([
|
|
64
83
|
"Terrain3D",
|
|
84
|
+
"TileMap2D",
|
|
65
85
|
"Water3D",
|
|
66
86
|
"River3D"
|
|
67
87
|
]);
|
|
@@ -74,11 +94,21 @@ const WORLD_QUERY_TYPES = new Set([
|
|
|
74
94
|
* A JSON walk cannot ask a behavior whether it serializes, so this is the list
|
|
75
95
|
* of the ones that do. Your own behavior with a `serialize()` is caught at
|
|
76
96
|
* runtime instead, by `captureState`.
|
|
97
|
+
*
|
|
98
|
+
* It is a CLOSED list beside an OPEN one — every engine `Behavior` overriding
|
|
99
|
+
* `serialize()` — and it drifted: `Currency` (a game's money) and `SavePoint`
|
|
100
|
+
* (how long the run has been going) both saved, and both audited clean on a
|
|
101
|
+
* node with no uid. `saveable-scripts.test.ts` reads the sources and fails when
|
|
102
|
+
* the two lists disagree again.
|
|
77
103
|
*/
|
|
78
104
|
const SAVEABLE_SCRIPTS = new Set([
|
|
105
|
+
"Collector",
|
|
106
|
+
"Currency",
|
|
107
|
+
"DayNight",
|
|
79
108
|
"Health",
|
|
109
|
+
"SavePoint",
|
|
80
110
|
"ScoreKeeper",
|
|
81
|
-
"
|
|
111
|
+
"WaveSpawner"
|
|
82
112
|
]);
|
|
83
113
|
/** Does the scene header light anything by itself (sky, hdri, ambient, preset)? */
|
|
84
114
|
function isLit(environment) {
|
|
@@ -123,7 +153,7 @@ function instancedScenes(corpus, resolveRef) {
|
|
|
123
153
|
if (typeof ref === "string" && ref) instanced.add(resolveRef(from, ref));
|
|
124
154
|
for (const child of node.children ?? []) walk(child, from);
|
|
125
155
|
};
|
|
126
|
-
for (const { path, json } of corpus) walk(json
|
|
156
|
+
for (const { path, json } of corpus) walk(json?.root, path);
|
|
127
157
|
return instanced;
|
|
128
158
|
}
|
|
129
159
|
function auditScene(scene, opts = {}) {
|
|
@@ -135,10 +165,15 @@ function auditScene(scene, opts = {}) {
|
|
|
135
165
|
let lights = 0;
|
|
136
166
|
/** Nodes that drive the current camera themselves — see the warning below. */
|
|
137
167
|
const cameraDrivers = [];
|
|
168
|
+
const terrains = [];
|
|
169
|
+
const autoDrapers = [];
|
|
138
170
|
const followedCameras = [];
|
|
139
171
|
const needUid = [];
|
|
140
172
|
/** Every `DamageOnContact.targetGroup` in the scene, with the node that set it. */
|
|
141
173
|
const damageGroups = [];
|
|
174
|
+
/** Nodes whose script produces NOTHING but signals — see `SIGNAL_ONLY`. */
|
|
175
|
+
const silentSignals = [];
|
|
176
|
+
const groupRefs = [];
|
|
142
177
|
const walk = (node, path, insideHud, staticRoot, transformed) => {
|
|
143
178
|
const type = node.type ?? "";
|
|
144
179
|
const props = node.props ?? {};
|
|
@@ -155,8 +190,14 @@ function auditScene(scene, opts = {}) {
|
|
|
155
190
|
if (script === "FollowCamera") followedCameras.push(path);
|
|
156
191
|
}
|
|
157
192
|
if (type === "CharacterController3D") cameraDrivers.push(path);
|
|
193
|
+
if (type === "Terrain3D") terrains.push(path);
|
|
194
|
+
if (DRAPERS.has(type) && (props.terrain ?? "") === "") autoDrapers.push(path);
|
|
158
195
|
if (LIGHT_TYPES.has(type)) lights += 1;
|
|
159
196
|
if (BODY_TYPES.has(type) && transformed) warnings.push(`${path}: ${type} sits under '${transformed.path}', whose ${transformed.what} physics cannot compose — the collider will not line up with what is drawn. Keep body ancestors untransformed or translation-only, or put the body outside them.`);
|
|
197
|
+
if (script && SIGNAL_ONLY.has(script)) silentSignals.push({
|
|
198
|
+
path,
|
|
199
|
+
script
|
|
200
|
+
});
|
|
160
201
|
if (script === "DamageOnContact") {
|
|
161
202
|
const group = (node.script?.props)?.targetGroup;
|
|
162
203
|
if (typeof group === "string" && group !== "") damageGroups.push({
|
|
@@ -164,6 +205,15 @@ function auditScene(scene, opts = {}) {
|
|
|
164
205
|
group
|
|
165
206
|
});
|
|
166
207
|
}
|
|
208
|
+
for (const [name, prop] of [["Pickup", "collectorGroup"], ["Collector", "group"]]) {
|
|
209
|
+
if (script !== name) continue;
|
|
210
|
+
const group = (node.script?.props)?.[prop];
|
|
211
|
+
if (typeof group === "string" && group !== "") groupRefs.push({
|
|
212
|
+
path,
|
|
213
|
+
group,
|
|
214
|
+
what: `${name}.${prop}`
|
|
215
|
+
});
|
|
216
|
+
}
|
|
167
217
|
if (WORLD_QUERY_TYPES.has(type) && transformed) warnings.push(`${path}: ${type} sits under '${transformed.path}', whose ${transformed.what} it cannot compose — the ground (or water) you can see would not be the ground you can stand on. Keep its ancestors untransformed or translation-only.`);
|
|
168
218
|
if (BODY_TYPES.has(type)) {
|
|
169
219
|
const collider = props.collider;
|
|
@@ -185,22 +235,52 @@ function auditScene(scene, opts = {}) {
|
|
|
185
235
|
what: transformOf(root.props ?? {})
|
|
186
236
|
} : null);
|
|
187
237
|
if (needUid.length > 0) warnings.push(`these carry state a save keeps and have no uid to key it under, so the save comes back EMPTY and the load reports no problem: ${needUid.join(", ")}. Give each one a "uid" from newUid().`);
|
|
238
|
+
if (terrains.length > 1 && autoDrapers.length > 0) warnings.push(`${autoDrapers.join(", ")}: this scene has ${terrains.length} Terrain3Ds (${terrains.join(", ")}) and these do not say which one they grow on — with no \`terrain\` path they take the FIRST one in the tree, so the rest drape to the wrong height with nothing reported. Name it: "terrain": "%Name".`);
|
|
239
|
+
if (cameraDrivers.length > 1) warnings.push(`${cameraDrivers.join(", ")}: ${cameraDrivers.length} CharacterController3Ds drive the SAME camera (each one finds the first \`current\` camera in the tree), so the last of them wins every frame and the others have no view. For local co-op give each player a panel with \`createSplitScreen\` (one engine each); otherwise only one node may carry a controller.`);
|
|
188
240
|
if (cameraDrivers.length > 0 && followedCameras.length > 0) warnings.push(`${followedCameras.join(", ")}: a FollowCamera writes this camera's position every frame, and so does the CharacterController3D at ${cameraDrivers.join(", ")} — the controller owns the camera in every view it has. Drop one of them: use the controller's own \`view\`/\`camDistance\`, or take the controller off the camera duty.`);
|
|
189
241
|
if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
|
|
190
242
|
if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
|
|
191
243
|
const isFragment = opts.instanced === true || scene.fragment === true;
|
|
192
|
-
|
|
193
|
-
if (
|
|
244
|
+
const is3d = sceneDimension(scene) === "3d";
|
|
245
|
+
if (is3d && !isFragment && cameras === 0) warnings.push("this 3D scene has no camera — the screen renders from a default view, which is almost never the shot you want. Add a Camera3D with \"current\": true, or set \"fragment\": true in the scene header if this file is a prefab another scene drops in.");
|
|
246
|
+
if (is3d && !isFragment && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" } — or set \"fragment\": true if another scene lights this one.");
|
|
247
|
+
if (silentSignals.length > 0) {
|
|
248
|
+
const heard = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const conn of scene.connections ?? []) if (typeof conn.from === "string") heard.add(conn.from);
|
|
250
|
+
for (const { path, script } of silentSignals) {
|
|
251
|
+
const relative = path.slice(path.indexOf("/") + 1);
|
|
252
|
+
if (heard.has(relative) || heard.has(path) || heard.has(`/root/${relative}`)) continue;
|
|
253
|
+
const signals = SIGNAL_ONLY.get(script) ?? [];
|
|
254
|
+
warnings.push(`${path}: ${script} produces nothing but ${signals.map((x) => `'${x}'`).join("/")}, and no connection listens to any of them — so it can never do anything. Wire one, or listen in code with \`node.on('${signals[0]}', …)\`.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
194
257
|
if (damageGroups.length > 0) {
|
|
195
258
|
const hurtable = hurtableGroups(root);
|
|
196
259
|
for (const { path, group } of damageGroups) {
|
|
197
260
|
if (hurtable.has(group)) continue;
|
|
198
|
-
warnings.push(`${path}: DamageOnContact targets group '${group}', and nothing in this scene both carries a Health and is in that group — so this hazard can never damage anything. Groups that CAN be hurt here: [${[...hurtable].join(", ") || "none"}].`);
|
|
261
|
+
warnings.push(isFragment ? `${path}: DamageOnContact targets group '${group}', and no Health in THIS file is in that group — fine if the target lives in the host scene, a typo otherwise. Groups hurtable here: [${[...hurtable].join(", ") || "none"}].` : `${path}: DamageOnContact targets group '${group}', and nothing in this scene both carries a Health and is in that group — so this hazard can never damage anything. Groups that CAN be hurt here: [${[...hurtable].join(", ") || "none"}].`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const { filter } of scene.connections ?? []) {
|
|
265
|
+
const group = filter?.group;
|
|
266
|
+
if (typeof group === "string" && group !== "") groupRefs.push({
|
|
267
|
+
path: "connections",
|
|
268
|
+
group,
|
|
269
|
+
what: "filter.group"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (groupRefs.length > 0) {
|
|
273
|
+
const present = groupsInScene(root);
|
|
274
|
+
for (const { path, group, what } of groupRefs) {
|
|
275
|
+
if (present.has(group)) continue;
|
|
276
|
+
warnings.push(`${path}: ${what} names group '${group}', and no node in this scene is in it — so this never fires. Groups that exist here: [${[...present].join(", ") || "none"}].`);
|
|
199
277
|
}
|
|
200
278
|
}
|
|
201
|
-
for (const warning of nodeRefWarnings(scene)) warnings.push(warning);
|
|
279
|
+
for (const warning of nodeRefWarnings(scene, { fragment: isFragment })) warnings.push(warning);
|
|
202
280
|
for (const warning of handlerShadowWarnings(scene)) warnings.push(warning);
|
|
203
281
|
for (const warning of auditStrings(scene, opts.declaredElsewhere)) warnings.push(warning);
|
|
282
|
+
for (const warning of auditInputBindings(scene)) warnings.push(warning);
|
|
283
|
+
for (const warning of auditKeyHints(scene, root)) warnings.push(warning);
|
|
204
284
|
return warnings;
|
|
205
285
|
}
|
|
206
286
|
/**
|
|
@@ -216,6 +296,16 @@ function auditScene(scene, opts = {}) {
|
|
|
216
296
|
* end), but the shape generalises: a hazard whose `targetGroup` nothing can
|
|
217
297
|
* satisfy is wired, validated, and disarmed, and every check comes back green.
|
|
218
298
|
*/
|
|
299
|
+
/** Every group any node in the scene is in — the set a group REFERENCE can hit. */
|
|
300
|
+
function groupsInScene(root) {
|
|
301
|
+
const out = /* @__PURE__ */ new Set();
|
|
302
|
+
const walk = (node) => {
|
|
303
|
+
for (const g of node.groups ?? []) out.add(g);
|
|
304
|
+
for (const child of node.children ?? []) walk(child);
|
|
305
|
+
};
|
|
306
|
+
walk(root);
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
219
309
|
function hurtableGroups(root) {
|
|
220
310
|
const out = /* @__PURE__ */ new Set();
|
|
221
311
|
const hasHealth = (node) => node.script?.name === "Health" || (node.children ?? []).some(hasHealth);
|
|
@@ -291,6 +381,125 @@ function auditStrings(scene, elsewhere) {
|
|
|
291
381
|
}
|
|
292
382
|
return out;
|
|
293
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* One physical key, two actions.
|
|
386
|
+
*
|
|
387
|
+
* Found in the shipped `tps-3d` starter: `reload` and `restart` were both
|
|
388
|
+
* `KeyR`. Pressing R reloaded, because `restart` was never read by anything —
|
|
389
|
+
* the collision and the dead binding hid each other, and neither is visible to
|
|
390
|
+
* a loader that validates each action's own shape.
|
|
391
|
+
*
|
|
392
|
+
* A warning rather than an error: two actions on one key are legal and
|
|
393
|
+
* sometimes deliberate (an `interact` that also advances a dialogue), which is
|
|
394
|
+
* why the message says what will happen rather than refusing to load.
|
|
395
|
+
*/
|
|
396
|
+
function auditInputBindings(scene) {
|
|
397
|
+
const input = scene.input;
|
|
398
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return [];
|
|
399
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
400
|
+
const add = (code, action) => {
|
|
401
|
+
if (typeof code !== "string" || code === "") return;
|
|
402
|
+
const list = byKey.get(code) ?? [];
|
|
403
|
+
if (!list.includes(action)) list.push(action);
|
|
404
|
+
byKey.set(code, list);
|
|
405
|
+
};
|
|
406
|
+
for (const [action, decl] of Object.entries(input)) {
|
|
407
|
+
if (!decl || typeof decl !== "object" || Array.isArray(decl)) continue;
|
|
408
|
+
const keys = decl.keys;
|
|
409
|
+
if (Array.isArray(keys)) for (const code of keys) add(code, action);
|
|
410
|
+
else if (keys && typeof keys === "object") {
|
|
411
|
+
for (const codes of Object.values(keys)) if (Array.isArray(codes)) for (const code of codes) add(code, action);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const out = [];
|
|
415
|
+
for (const [code, actions] of byKey) {
|
|
416
|
+
if (actions.length < 2) continue;
|
|
417
|
+
out.push(`input: '${code}' is bound to ${actions.map((a) => `'${a}'`).join(" and ")} — both fire on the same press, so whichever one your code reads first wins and the other looks broken. Give one of them its own key.`);
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* How a HUD line spells a key, and how `input{}` spells the same key.
|
|
423
|
+
*
|
|
424
|
+
* Only the ones a player would ever type in a hint. A miss here is silence, not
|
|
425
|
+
* a false alarm: the check needs BOTH ends to be recognisable before it says
|
|
426
|
+
* anything.
|
|
427
|
+
*/
|
|
428
|
+
const KEY_WORDS = {
|
|
429
|
+
esc: ["Escape"],
|
|
430
|
+
escape: ["Escape"],
|
|
431
|
+
enter: ["Enter", "NumpadEnter"],
|
|
432
|
+
return: ["Enter"],
|
|
433
|
+
space: ["Space"],
|
|
434
|
+
tab: ["Tab"],
|
|
435
|
+
shift: ["ShiftLeft", "ShiftRight"],
|
|
436
|
+
ctrl: ["ControlLeft", "ControlRight"],
|
|
437
|
+
alt: ["AltLeft", "AltRight"]
|
|
438
|
+
};
|
|
439
|
+
/** Every key code an action binds, however the declaration is shaped. */
|
|
440
|
+
function keysOfAction(decl) {
|
|
441
|
+
if (!decl || typeof decl !== "object" || Array.isArray(decl)) return [];
|
|
442
|
+
const keys = decl.keys;
|
|
443
|
+
if (Array.isArray(keys)) return keys.filter((k) => typeof k === "string");
|
|
444
|
+
if (keys && typeof keys === "object") {
|
|
445
|
+
const out = [];
|
|
446
|
+
for (const codes of Object.values(keys)) if (Array.isArray(codes)) {
|
|
447
|
+
for (const c of codes) if (typeof c === "string") out.push(c);
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* A HUD line that tells the player a key the input map does not bind.
|
|
455
|
+
*
|
|
456
|
+
* `tps-3d`'s pause menu read `Esc to resume · R to restart` for exactly three
|
|
457
|
+
* hours after `restart` was moved off `KeyR` (which `reload` already owned) —
|
|
458
|
+
* the binding moved and the sentence about it did not, which is the whole
|
|
459
|
+
* failure mode of any fact written down twice.
|
|
460
|
+
*
|
|
461
|
+
* Deliberately narrow. It fires only when a hint names BOTH a key this table
|
|
462
|
+
* knows and a word that is a DECLARED ACTION in the same scene, so "Esc to
|
|
463
|
+
* resume" says nothing (there is no `resume` action) and prose about a "T shape"
|
|
464
|
+
* says nothing. What is left is the case where the scene itself contains both
|
|
465
|
+
* halves and they disagree.
|
|
466
|
+
*/
|
|
467
|
+
function auditKeyHints(scene, root) {
|
|
468
|
+
const input = scene.input;
|
|
469
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return [];
|
|
470
|
+
const actions = input;
|
|
471
|
+
const out = [];
|
|
472
|
+
const seen = /* @__PURE__ */ new Set();
|
|
473
|
+
const check = (path, text) => {
|
|
474
|
+
for (const m of text.matchAll(/\b([A-Za-z]{1,6})\s+to\s+([a-z]+)\b/g)) {
|
|
475
|
+
const word = (m[1] ?? "").toLowerCase();
|
|
476
|
+
const action = m[2] ?? "";
|
|
477
|
+
if (!(action in actions)) continue;
|
|
478
|
+
const wanted = KEY_WORDS[word] ?? (word.length === 1 ? [`Key${word.toUpperCase()}`] : null);
|
|
479
|
+
if (!wanted) continue;
|
|
480
|
+
const bound = keysOfAction(actions[action]);
|
|
481
|
+
if (bound.length === 0 || wanted.some((k) => bound.includes(k))) continue;
|
|
482
|
+
const line = `${path}: says "${m[0]}", and '${action}' is bound to [${bound.join(", ")}]`;
|
|
483
|
+
if (seen.has(line)) continue;
|
|
484
|
+
seen.add(line);
|
|
485
|
+
out.push(`${line} — the HUD is telling the player a key that does something else. Rebind the action, or fix the line.`);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const walk = (node, path) => {
|
|
489
|
+
const props = node.props ?? {};
|
|
490
|
+
for (const key of [
|
|
491
|
+
"text",
|
|
492
|
+
"format",
|
|
493
|
+
"label"
|
|
494
|
+
]) {
|
|
495
|
+
const value = props[key];
|
|
496
|
+
if (typeof value === "string" && value !== "") check(`${path}.${key}`, value);
|
|
497
|
+
}
|
|
498
|
+
for (const child of node.children ?? []) walk(child, `${path}/${child.name ?? "?"}`);
|
|
499
|
+
};
|
|
500
|
+
walk(root, `/${root.name ?? "?"}`);
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
294
503
|
//#endregion
|
|
295
504
|
//#region src/core/replay.ts
|
|
296
505
|
/**
|
|
@@ -377,7 +586,8 @@ function replay(engine, recording, opts) {
|
|
|
377
586
|
}
|
|
378
587
|
const at = recording.ticks[i];
|
|
379
588
|
if (at === null) engine.step();
|
|
380
|
-
else engine.tick(at);
|
|
589
|
+
else if (typeof at === "number" && Number.isFinite(at)) engine.tick(at);
|
|
590
|
+
else throw new IncantoError("BAD_FORMAT", `Replay tick ${i} is ${JSON.stringify(at)}, not a timestamp. A recording holds a finite millisecond value per frame, or null for a frame driven by step().`);
|
|
381
591
|
opts?.onTick?.(i);
|
|
382
592
|
}
|
|
383
593
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as Engine } from "./behavior-
|
|
2
|
-
import { s as JsonObject } from "./
|
|
1
|
+
import { b as Engine } from "./behavior-Dcz0fr1S.js";
|
|
2
|
+
import { s as JsonObject } from "./rng-Bb-IutXB.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/audit.d.ts
|
|
5
5
|
/** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
|
|
@@ -59,7 +59,7 @@ declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
|
|
|
59
59
|
* Incanto fails loudly with machine-readable codes and messages that list valid
|
|
60
60
|
* alternatives — AI agents self-correct on hard failures, not warnings.
|
|
61
61
|
*/
|
|
62
|
-
type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WEBGL_UNAVAILABLE";
|
|
62
|
+
type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NETWORK_UNAVAILABLE" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WEBGL_UNAVAILABLE";
|
|
63
63
|
/**
|
|
64
64
|
* Structured context mirrored from the prose message — tools and agents parse
|
|
65
65
|
* THESE instead of regexing the message (which is for humans and may reword).
|
|
@@ -115,6 +115,23 @@ interface ReplayJson {
|
|
|
115
115
|
*/
|
|
116
116
|
ticks: Array<number | null>;
|
|
117
117
|
events: ReplayEvent[];
|
|
118
|
+
/**
|
|
119
|
+
* WHICH GAME this is a recording of — optional, and stamped by the tools.
|
|
120
|
+
*
|
|
121
|
+
* `.incanto/playtest` is keyed only by `(outcome, seed)` and never cleaned, so
|
|
122
|
+
* the second playtest in a project either left a stale replay of a scene that
|
|
123
|
+
* no longer exists beside the current one, or silently overwrote another
|
|
124
|
+
* scene's. Nothing downstream could tell: `incanto-play --commands` accepted
|
|
125
|
+
* any recording against any scene and reported success.
|
|
126
|
+
*
|
|
127
|
+
* Absent on an older recording, which still replays — this identifies, it
|
|
128
|
+
* does not gate.
|
|
129
|
+
*/
|
|
130
|
+
scene?: {
|
|
131
|
+
/** The scene file this was recorded against, as the tool was given it. */path?: string; /** The scene's own `name`, which survives the file being moved. */
|
|
132
|
+
name?: string; /** The engine that recorded it, so a replay across a format change reads odd. */
|
|
133
|
+
engine?: string;
|
|
134
|
+
};
|
|
118
135
|
}
|
|
119
136
|
interface Recorder {
|
|
120
137
|
/** Detach the taps and return the recording. */
|
|
@@ -14,26 +14,6 @@ declare function jsonEquals(a: JsonValue, b: JsonValue): boolean;
|
|
|
14
14
|
*/
|
|
15
15
|
declare function jsonClone<T>(value: T): T;
|
|
16
16
|
//#endregion
|
|
17
|
-
//#region src/core/rng.d.ts
|
|
18
|
-
/**
|
|
19
|
-
* Deterministic seeded PRNG (mulberry32). Game logic should draw randomness
|
|
20
|
-
* from `engine.rng` (or `this.rng` in a Behavior) instead of `Math.random()` —
|
|
21
|
-
* a seeded engine then replays identically, which is what makes scripted
|
|
22
|
-
* headless verification and multiplayer-adjacent determinism possible.
|
|
23
|
-
*/
|
|
24
|
-
declare class Rng {
|
|
25
|
-
private state;
|
|
26
|
-
constructor(seed: number);
|
|
27
|
-
/** Next float in [0, 1). */
|
|
28
|
-
next(): number;
|
|
29
|
-
/** Float in [min, max). */
|
|
30
|
-
range(min: number, max: number): number;
|
|
31
|
-
/** Integer in [min, max] — inclusive of BOTH bounds. */
|
|
32
|
-
int(min: number, max: number): number;
|
|
33
|
-
/** A uniformly random element. Empty arrays are a hard error. */
|
|
34
|
-
pick<T>(items: readonly T[]): T;
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
17
|
//#region src/core/scene/schema.d.ts
|
|
38
18
|
/** Current scene file format version. Defaults are frozen per format version. */
|
|
39
19
|
declare const SCENE_FORMAT: 1;
|
|
@@ -132,4 +112,41 @@ interface ConnectionJson {
|
|
|
132
112
|
};
|
|
133
113
|
}
|
|
134
114
|
//#endregion
|
|
135
|
-
|
|
115
|
+
//#region src/core/rng.d.ts
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic seeded PRNG (mulberry32). Game logic should draw randomness
|
|
118
|
+
* from `engine.rng` (or `this.rng` in a Behavior) instead of `Math.random()` —
|
|
119
|
+
* a seeded engine then replays identically, which is what makes scripted
|
|
120
|
+
* headless verification and multiplayer-adjacent determinism possible.
|
|
121
|
+
*/
|
|
122
|
+
declare class Rng {
|
|
123
|
+
private state;
|
|
124
|
+
/**
|
|
125
|
+
* Where this stream has got to.
|
|
126
|
+
*
|
|
127
|
+
* A save that cannot carry this replays the seeded sequence from the top, so
|
|
128
|
+
* every "random" event after a load is the one the run opened with. Measured:
|
|
129
|
+
*
|
|
130
|
+
* ```
|
|
131
|
+
* continue WITHOUT save/load: rolls 10-13 = 0.2967 0.0860 0.7333 0.0400
|
|
132
|
+
* continue AFTER save/load : rolls 10-13 = 0.5628 0.3253 0.4690 0.8367
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* and 0.5628 is not merely different — it is the FIRST roll a fresh run of
|
|
136
|
+
* that game makes. The state was private, so a game could not fix it either:
|
|
137
|
+
* `serialize()` had nothing to carry.
|
|
138
|
+
*/
|
|
139
|
+
get position(): number;
|
|
140
|
+
set position(value: number);
|
|
141
|
+
constructor(seed: number);
|
|
142
|
+
/** Next float in [0, 1). */
|
|
143
|
+
next(): number;
|
|
144
|
+
/** Float in [min, max). */
|
|
145
|
+
range(min: number, max: number): number;
|
|
146
|
+
/** Integer in [min, max] — inclusive of BOTH bounds. */
|
|
147
|
+
int(min: number, max: number): number;
|
|
148
|
+
/** A uniformly random element. Empty arrays are a hard error. */
|
|
149
|
+
pick<T>(items: readonly T[]): T;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { SceneJson as a, JsonValue as c, jsonKind as d, SCENE_FORMAT as i, jsonClone as l, ConnectionJson as n, JsonKind as o, NodeJson as r, JsonObject as s, Rng as t, jsonEquals as u };
|
|
@@ -8,6 +8,28 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
|
8
8
|
*/
|
|
9
9
|
var Rng = class {
|
|
10
10
|
state;
|
|
11
|
+
/**
|
|
12
|
+
* Where this stream has got to.
|
|
13
|
+
*
|
|
14
|
+
* A save that cannot carry this replays the seeded sequence from the top, so
|
|
15
|
+
* every "random" event after a load is the one the run opened with. Measured:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* continue WITHOUT save/load: rolls 10-13 = 0.2967 0.0860 0.7333 0.0400
|
|
19
|
+
* continue AFTER save/load : rolls 10-13 = 0.5628 0.3253 0.4690 0.8367
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* and 0.5628 is not merely different — it is the FIRST roll a fresh run of
|
|
23
|
+
* that game makes. The state was private, so a game could not fix it either:
|
|
24
|
+
* `serialize()` had nothing to carry.
|
|
25
|
+
*/
|
|
26
|
+
get position() {
|
|
27
|
+
return this.state >>> 0;
|
|
28
|
+
}
|
|
29
|
+
set position(value) {
|
|
30
|
+
const next = value >>> 0;
|
|
31
|
+
this.state = next === 0 ? 2654435769 : next;
|
|
32
|
+
}
|
|
11
33
|
constructor(seed) {
|
|
12
34
|
this.state = seed >>> 0;
|
|
13
35
|
if (this.state === 0) this.state = 2654435769;
|