incanto 0.36.2 → 0.38.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/incanto-feel.mjs +93 -0
- package/bin/incanto-playtest.mjs +136 -0
- package/dist/2d.d.ts +2 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +44 -3
- package/dist/3d.js +4 -32
- package/dist/{behavior-DibCwrW7.d.ts → behavior-B5PJ32eu.d.ts} +106 -6
- package/dist/{create-game-D-StzrTj.js → create-game-C4t5gVhc.js} +5 -5
- package/dist/{create-game-DnOu2aAa.js → create-game-DqY2lMBc.js} +783 -689
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-Cvb1BSca.js → duplicate-CRtihGmC.js} +1 -1
- package/dist/editor.js +21 -0
- package/dist/{environment-presets-fK9oyrrR.js → environment-presets-C0Axpfth.js} +75 -9
- package/dist/{gameplay-C0MUMSak.js → gameplay-CsJDUQh_.js} +2 -2
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +4 -50
- package/dist/index.js +5 -75
- package/dist/{loader-BqigZsfF.d.ts → loader-DSMdqasu.d.ts} +1 -1
- package/dist/{loader-DAZlgqeC.js → loader-r49nDwB4.js} +15 -0
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-B3QtdhvZ.d.ts → pathfinding-BrE5Uec6.d.ts} +1 -1
- package/dist/{physics-2d-B4v39hca.js → physics-2d-OfKb0PV_.js} +2 -2
- package/dist/{physics-3d-DFJVMYET.js → physics-3d-C8242nXr.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-DXwtIVqP.js → register-B4RRQbyq.js} +2 -2
- package/dist/{register-BHAwM4bK.js → register-Dm7nrd-E.js} +322 -34
- package/dist/{register-COGCNitr.js → register-DnB9m7Rl.js} +2 -2
- package/dist/replay-Cxrr-ApH.d.ts +86 -0
- package/dist/{audit-D7F3n3Nt.js → replay-DH0UOuZ-.js} +72 -2
- package/dist/test-CoDPWx-m.js +1404 -0
- package/dist/test.d.ts +113 -4
- package/dist/test.js +3 -3
- package/dist/vite.js +1 -1
- package/editor/assets/{agent8-DMvBVhe4.js → agent8-BtIlblPm.js} +1 -1
- package/editor/assets/{debug-BgAq55oC.js → debug-BvTmHXI7.js} +1 -1
- package/editor/assets/{index-Cy_QsKbh.js → index-BS01WWgM.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +3 -1
- package/schemas/scene.schema.json +318 -0
- package/skills/incanto-3d-character.md +35 -0
- package/skills/incanto-building-2d-games.md +6 -0
- package/skills/incanto-building-3d-games.md +6 -0
- package/skills/incanto-game-feel.md +76 -0
- package/skills/incanto-node-reference.md +51 -0
- package/skills/incanto-performance.md +212 -0
- package/skills/incanto-playtesting.md +143 -0
- package/skills/incanto-verifying-your-game.md +41 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/village.scene.json +3 -1
- package/templates-app/village-quest-3d/verify.ts +55 -5
- package/dist/errors-DGRtWlSx.d.ts +0 -39
- package/dist/test-X_xE6Yay.js +0 -642
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* incanto-feel — what your controls actually FEEL like, as numbers.
|
|
6
|
+
*
|
|
7
|
+
* bunx incanto-feel src/game.scene.json
|
|
8
|
+
*
|
|
9
|
+
* `capture()` and `framing()` are snapshots. Game feel lives on the time axis:
|
|
10
|
+
* how high the jump really goes, how many frames a turn costs, how wide the
|
|
11
|
+
* coyote window really is. This drives the player through scripted probes and
|
|
12
|
+
* reports the measurements.
|
|
13
|
+
*
|
|
14
|
+
* The windows are MEASURED by probing, not read off the schema — so a
|
|
15
|
+
* `coyoteSeconds` the controller never consults reports 0 ms. That was a real
|
|
16
|
+
* bug in this engine's own 3D controller, and the JSON looked correct
|
|
17
|
+
* throughout.
|
|
18
|
+
*
|
|
19
|
+
* It reports numbers and does not grade them: "good" jump feel is a genre
|
|
20
|
+
* decision, and a PASS/FAIL table against invented bands would be confidently
|
|
21
|
+
* wrong for most games.
|
|
22
|
+
*/
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = {};
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--behaviors') args.behaviors = argv[++i];
|
|
32
|
+
else if (a === '--move') args.move = argv[++i];
|
|
33
|
+
else if (a === '--jump') args.jump = argv[++i];
|
|
34
|
+
else if (a === '--json') args.json = true;
|
|
35
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
36
|
+
else if (a.startsWith('--')) {
|
|
37
|
+
console.error(`unknown flag: ${a}`);
|
|
38
|
+
args.help = true;
|
|
39
|
+
} else if (!args.scene) args.scene = a;
|
|
40
|
+
}
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = parseArgs(process.argv.slice(2));
|
|
45
|
+
if (args.help || !args.scene) {
|
|
46
|
+
console.error(`Usage: incanto-feel <scene.json> [options]
|
|
47
|
+
|
|
48
|
+
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
49
|
+
--move ACTION the vector action to move with (default: the first vector2)
|
|
50
|
+
--jump ACTION the button to jump with (default: "jump")
|
|
51
|
+
--json the report as JSON instead of prose`);
|
|
52
|
+
process.exit(args.help && args.scene !== undefined ? 0 : 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { feelReport, feelText } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
|
|
56
|
+
const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
57
|
+
|
|
58
|
+
const behaviors = {};
|
|
59
|
+
if (args.behaviors) {
|
|
60
|
+
let mod;
|
|
61
|
+
try {
|
|
62
|
+
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(`could not load --behaviors '${args.behaviors}' (${e?.message ?? e}).`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
68
|
+
if (typeof value === 'function' && value.prototype instanceof incanto.Behavior) {
|
|
69
|
+
behaviors[name] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const scenePath = resolve(args.scene);
|
|
75
|
+
const sceneDir = dirname(scenePath);
|
|
76
|
+
const sceneJson = JSON.parse(readFileSync(scenePath, 'utf-8'));
|
|
77
|
+
|
|
78
|
+
let report;
|
|
79
|
+
try {
|
|
80
|
+
report = await feelReport(sceneJson, {
|
|
81
|
+
behaviors,
|
|
82
|
+
stubMissingBehaviors: !args.behaviors,
|
|
83
|
+
...(args.move ? { moveAction: args.move } : {}),
|
|
84
|
+
...(args.jump ? { jumpAction: args.jump } : {}),
|
|
85
|
+
resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
|
|
86
|
+
});
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error(`feel failed: ${e?.message ?? e}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.stdout.write(args.json ? `${JSON.stringify(report, null, 2)}\n` : `${feelText(report)}\n`);
|
|
93
|
+
process.exit(report.player ? 0 : 1);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* incanto-playtest — the engine plays your game and tells you whether it works.
|
|
6
|
+
*
|
|
7
|
+
* bunx incanto-playtest src/game.scene.json --runs 20 --seconds 60
|
|
8
|
+
*
|
|
9
|
+
* `incanto-check` answers "will it load". This answers "is it a game": can it
|
|
10
|
+
* be finished, which destinations are unreachable, which wires never fire, and
|
|
11
|
+
* whether anything in it can hurt the player.
|
|
12
|
+
*
|
|
13
|
+
* Runs are seeded and headless — no renderer, no browser — so twenty minutes of
|
|
14
|
+
* simulated play costs a second or two. A run that did NOT win is written out
|
|
15
|
+
* as a replay you can watch:
|
|
16
|
+
*
|
|
17
|
+
* bunx incanto-play src/game.scene.json --commands .incanto/playtest/fell-seed7.txt
|
|
18
|
+
*
|
|
19
|
+
* Without --behaviors, unregistered scripts are stubbed: the structure plays,
|
|
20
|
+
* your game logic does not. Pass the file that exports your Behavior subclasses
|
|
21
|
+
* to test the real thing.
|
|
22
|
+
*
|
|
23
|
+
* Exit code 1 when no run reached a win, so CI can gate on it.
|
|
24
|
+
*/
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = { runs: 20, seconds: 60, seed: 1, out: '.incanto/playtest' };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === '--behaviors') args.behaviors = argv[++i];
|
|
34
|
+
else if (a === '--runs') args.runs = Number(argv[++i]);
|
|
35
|
+
else if (a === '--seconds') args.seconds = Number(argv[++i]);
|
|
36
|
+
else if (a === '--seed') args.seed = Number(argv[++i]);
|
|
37
|
+
else if (a === '--out') args.out = argv[++i];
|
|
38
|
+
else if (a === '--no-replays') args.noReplays = true;
|
|
39
|
+
else if (a === '--json') args.json = true;
|
|
40
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
41
|
+
else if (a.startsWith('--')) {
|
|
42
|
+
console.error(`unknown flag: ${a}`);
|
|
43
|
+
args.help = true;
|
|
44
|
+
} else if (!args.scene) args.scene = a;
|
|
45
|
+
}
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const args = parseArgs(process.argv.slice(2));
|
|
50
|
+
if (args.help || !args.scene) {
|
|
51
|
+
console.error(`Usage: incanto-playtest <scene.json> [options]
|
|
52
|
+
|
|
53
|
+
--runs N independent seeded runs (default 20)
|
|
54
|
+
--seconds N simulated seconds per run before calling it stuck (default 60)
|
|
55
|
+
--seed N first seed; run i uses seed+i (default 1)
|
|
56
|
+
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
57
|
+
--out DIR where failing replays go (default .incanto/playtest)
|
|
58
|
+
--no-replays report only, write nothing
|
|
59
|
+
--json the full report as JSON instead of prose
|
|
60
|
+
|
|
61
|
+
Exits 1 when no run reached a win.`);
|
|
62
|
+
process.exit(args.help && args.scene !== undefined ? 0 : 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { playtest, playtestText, failingReplays } = await import(
|
|
66
|
+
pathToFileURL(join(PKG, 'dist', 'test.js')).href
|
|
67
|
+
);
|
|
68
|
+
const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
69
|
+
|
|
70
|
+
const behaviors = {};
|
|
71
|
+
if (args.behaviors) {
|
|
72
|
+
let mod;
|
|
73
|
+
try {
|
|
74
|
+
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error(
|
|
77
|
+
`could not load --behaviors '${args.behaviors}' (${e?.message ?? e}). ` +
|
|
78
|
+
'Pass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.',
|
|
79
|
+
);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
83
|
+
if (typeof value === 'function' && value.prototype instanceof incanto.Behavior) {
|
|
84
|
+
behaviors[name] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(behaviors).length === 0) {
|
|
88
|
+
console.error(`warning: no Behavior subclasses exported from ${args.behaviors}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const scenePath = resolve(args.scene);
|
|
93
|
+
const sceneJson = JSON.parse(readFileSync(scenePath, 'utf-8'));
|
|
94
|
+
const sceneDir = dirname(scenePath);
|
|
95
|
+
|
|
96
|
+
let report;
|
|
97
|
+
try {
|
|
98
|
+
report = await playtest(sceneJson, {
|
|
99
|
+
runs: args.runs,
|
|
100
|
+
seconds: args.seconds,
|
|
101
|
+
seed: args.seed,
|
|
102
|
+
behaviors,
|
|
103
|
+
stubMissingBehaviors: !args.behaviors,
|
|
104
|
+
// sub-scenes resolve relative to the scene file, as the loader does
|
|
105
|
+
resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error(`playtest failed: ${e?.message ?? e}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (args.json) {
|
|
113
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
114
|
+
} else {
|
|
115
|
+
process.stdout.write(`${playtestText(report)}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A failing run is only useful if you can watch it. One file per outcome kind —
|
|
119
|
+
// twenty identical "stuck" replays teach nothing that the first one does not.
|
|
120
|
+
if (!args.noReplays && !args.json) {
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const written = [];
|
|
123
|
+
for (const { seed, outcome, replay } of failingReplays(report)) {
|
|
124
|
+
if (seen.has(outcome)) continue;
|
|
125
|
+
seen.add(outcome);
|
|
126
|
+
const file = join(resolve(args.out), `${outcome}-seed${seed}.json`);
|
|
127
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
128
|
+
writeFileSync(file, JSON.stringify(replay, null, 2));
|
|
129
|
+
written.push(file);
|
|
130
|
+
}
|
|
131
|
+
if (written.length > 0) {
|
|
132
|
+
process.stdout.write(`\n replays: ${written.join('\n ')}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
process.exit(report.runs.some((r) => r.outcome === 'won') ? 0 : 1);
|
package/dist/2d.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-
|
|
1
|
+
import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-B5PJ32eu.js";
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
|
|
3
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
+
import { t as LoadSceneOptions } from "./loader-DSMdqasu.js";
|
|
5
5
|
import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
|
|
6
6
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
7
7
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
package/dist/2d.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-
|
|
3
|
-
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-
|
|
4
|
-
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-
|
|
2
|
+
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-C4t5gVhc.js";
|
|
3
|
+
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DnB9m7Rl.js";
|
|
4
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-OfKb0PV_.js";
|
|
5
5
|
//#region src/2d/library-sprite.ts
|
|
6
6
|
/**
|
|
7
7
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
package/dist/3d.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-
|
|
1
|
+
import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-B5PJ32eu.js";
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
|
|
3
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
+
import { t as LoadSceneOptions } from "./loader-DSMdqasu.js";
|
|
5
5
|
import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
|
|
6
|
-
import { n as PathGrid, s as SpatialPose } from "./pathfinding-
|
|
6
|
+
import { n as PathGrid, s as SpatialPose } from "./pathfinding-BrE5Uec6.js";
|
|
7
7
|
import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
|
|
8
8
|
import { VRM } from "@pixiv/three-vrm";
|
|
9
9
|
import { Sky } from "three/examples/jsm/objects/Sky.js";
|
|
@@ -1058,6 +1058,12 @@ declare class CharacterController3D extends Node3D {
|
|
|
1058
1058
|
sprintMultiplier: number;
|
|
1059
1059
|
jumpVelocity: number;
|
|
1060
1060
|
sprintJumpMultiplier: number;
|
|
1061
|
+
coyoteSeconds: number;
|
|
1062
|
+
jumpBufferSeconds: number;
|
|
1063
|
+
jumpCutMultiplier: number;
|
|
1064
|
+
maxJumps: number;
|
|
1065
|
+
airControl: number;
|
|
1066
|
+
fallGravity: number;
|
|
1061
1067
|
floatHeight: number;
|
|
1062
1068
|
mouseLook: boolean;
|
|
1063
1069
|
zoomMin: number;
|
|
@@ -1093,6 +1099,11 @@ declare class CharacterController3D extends Node3D {
|
|
|
1093
1099
|
/** idle | walk | run | fastRun | airborne — drive animations from this. */
|
|
1094
1100
|
state: "idle" | "walk" | "run" | "fastRun" | "airborne";
|
|
1095
1101
|
grounded: boolean;
|
|
1102
|
+
/** @internal Feel timers — see the jump block for what each one buys. */
|
|
1103
|
+
private coyoteLeft;
|
|
1104
|
+
private bufferLeft;
|
|
1105
|
+
private jumpsUsed;
|
|
1106
|
+
private rising;
|
|
1096
1107
|
private get body();
|
|
1097
1108
|
override onEnterTree(): void;
|
|
1098
1109
|
override fixedUpdate(dt: number): void;
|
|
@@ -3114,6 +3125,19 @@ interface Renderer3DOptions {
|
|
|
3114
3125
|
/** Defaults to `window.devicePixelRatio` capped at 2. */
|
|
3115
3126
|
pixelRatio?: number;
|
|
3116
3127
|
/**
|
|
3128
|
+
* Starting point for the rendering knobs, from the player's quality tier.
|
|
3129
|
+
*
|
|
3130
|
+
* DEFAULTS, not overrides: a scene's own `environment.rendering` is a decision
|
|
3131
|
+
* about that scene and still wins, and an explicit `pixelRatio` option wins
|
|
3132
|
+
* over both. This only decides what a device that has said nothing gets —
|
|
3133
|
+
* which for `antialias` is the only chance there is, since it is a WebGL
|
|
3134
|
+
* context attribute and cannot change after the context exists.
|
|
3135
|
+
*/
|
|
3136
|
+
qualityDefaults?: {
|
|
3137
|
+
antialias: boolean;
|
|
3138
|
+
pixelRatio: number;
|
|
3139
|
+
};
|
|
3140
|
+
/**
|
|
3117
3141
|
* Keep the frame rate by rendering at fewer pixels when frames run slow, and
|
|
3118
3142
|
* hand them back when they recover (default ON). A retina display asks for
|
|
3119
3143
|
* 4× the pixels; shaded water is fragment-heavy; the honest answer is fewer
|
|
@@ -3211,6 +3235,8 @@ declare class Renderer3D {
|
|
|
3211
3235
|
/** The pixel ratio the scene ASKED for — the governor scales this, never replaces it. */
|
|
3212
3236
|
private readonly basePixelRatio;
|
|
3213
3237
|
private readonly adaptive;
|
|
3238
|
+
private renderScale;
|
|
3239
|
+
private adaptiveScale;
|
|
3214
3240
|
private lastFrameAt;
|
|
3215
3241
|
private readonly debugLines;
|
|
3216
3242
|
private readonly selectionLines;
|
|
@@ -3225,6 +3251,21 @@ declare class Renderer3D {
|
|
|
3225
3251
|
*/
|
|
3226
3252
|
private governResolution;
|
|
3227
3253
|
/**
|
|
3254
|
+
* The player's resolution multiplier, on top of whatever the scene and the
|
|
3255
|
+
* quality tier resolved to.
|
|
3256
|
+
*
|
|
3257
|
+
* The cheapest lever there is on a weak GPU: at 0.66 the fragment shaders run
|
|
3258
|
+
* over 44% of the pixels for a soft image and nothing else, where dropping a
|
|
3259
|
+
* quality tier costs the shadows outright. Live — a settings menu writes it
|
|
3260
|
+
* mid-game — and it multiplies with the adaptive governor rather than
|
|
3261
|
+
* fighting it, so a player who has already scaled down does not get scaled
|
|
3262
|
+
* down twice as hard.
|
|
3263
|
+
*/
|
|
3264
|
+
setRenderScale(scale: number): void;
|
|
3265
|
+
/** Judge frames against `fps` — see AdaptiveResolution.setTarget. */
|
|
3266
|
+
setFrameTarget(fps: number): void;
|
|
3267
|
+
private applyPixelRatio;
|
|
3268
|
+
/**
|
|
3228
3269
|
* Compile the scene's GPU programs off the critical path.
|
|
3229
3270
|
*
|
|
3230
3271
|
* The compile itself is unavoidable — a dressed outdoor scene is 40+ programs
|
package/dist/3d.js
CHANGED
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as
|
|
3
|
-
import {
|
|
4
|
-
import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DnOu2aAa.js";
|
|
1
|
+
import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-CsJDUQh_.js";
|
|
2
|
+
import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-C0Axpfth.js";
|
|
3
|
+
import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-DqY2lMBc.js";
|
|
5
4
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
6
|
-
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-
|
|
7
|
-
//#region src/3d/environment-runtime.ts
|
|
8
|
-
/**
|
|
9
|
-
* Live environment editing — the renderer re-applies `scene.environment`
|
|
10
|
-
* every frame (cheap when unchanged), so changing the LOOK at runtime is
|
|
11
|
-
* just writing that object. This helper does it safely:
|
|
12
|
-
*
|
|
13
|
-
* setEnvironment3D(engine, { sky: { elevationDeg: 4 }, exposure: 0.8 });
|
|
14
|
-
* setEnvironment3D(engine, { fog: null }); // null DELETES a key
|
|
15
|
-
*
|
|
16
|
-
* - one-level deep merge (patching `sky.elevationDeg` keeps the other sky keys)
|
|
17
|
-
* - hard-validates the merged result NOW (in your call stack), so a bad patch
|
|
18
|
-
* throws where you wrote it — never inside the render loop
|
|
19
|
-
*/
|
|
20
|
-
function setEnvironment3D(engine, patch) {
|
|
21
|
-
const scene = engine.scene;
|
|
22
|
-
if (!scene) throw new IncantoError("BAD_FORMAT", "setEnvironment3D: the engine has no scene.");
|
|
23
|
-
const merged = { ...scene.environment ?? {} };
|
|
24
|
-
for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
|
|
25
|
-
else if (typeof value === "object" && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) merged[key] = {
|
|
26
|
-
...merged[key],
|
|
27
|
-
...value
|
|
28
|
-
};
|
|
29
|
-
else merged[key] = value;
|
|
30
|
-
parseEnvironment3D(merged);
|
|
31
|
-
scene.environment = merged;
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
5
|
+
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-C8242nXr.js";
|
|
34
6
|
//#region src/3d/terrain-nav.ts
|
|
35
7
|
function buildTerrainNav(terrain, opts) {
|
|
36
8
|
const cellSize = opts?.cellSize ?? 2;
|
|
@@ -670,6 +670,17 @@ declare class InputMap {
|
|
|
670
670
|
* scripted gameplay tests and touch buttons drive the game by intent
|
|
671
671
|
* (`press('jump')`) instead of reverse-engineering keybinds.
|
|
672
672
|
*/
|
|
673
|
+
/**
|
|
674
|
+
* Every action this scene declared, with its kind.
|
|
675
|
+
*
|
|
676
|
+
* A scene states its own control vocabulary in `input{}`, which means a tool
|
|
677
|
+
* can drive a game it has never seen — the automated playtester's entire
|
|
678
|
+
* premise. Nothing else could ask: `actions` was private.
|
|
679
|
+
*/
|
|
680
|
+
declaredActions(): Array<{
|
|
681
|
+
name: string;
|
|
682
|
+
type: "button" | "vector2";
|
|
683
|
+
}>;
|
|
673
684
|
pressAction(action: string): void;
|
|
674
685
|
/** Release an injected button action (yields one justReleased frame). */
|
|
675
686
|
releaseAction(action: string): void;
|
|
@@ -1080,6 +1091,21 @@ interface SettingsValues {
|
|
|
1080
1091
|
* does not translate falls back to English, silently and on purpose.
|
|
1081
1092
|
*/
|
|
1082
1093
|
language: string;
|
|
1094
|
+
/**
|
|
1095
|
+
* Has a HUMAN chosen the quality tier?
|
|
1096
|
+
*
|
|
1097
|
+
* Without this, a first boot cannot tell "the default happens to be high" from
|
|
1098
|
+
* "the player picked high", so device auto-detection would either never run or
|
|
1099
|
+
* would overwrite a deliberate choice on every launch.
|
|
1100
|
+
*/
|
|
1101
|
+
qualityChosen: boolean;
|
|
1102
|
+
/** Frames per second to draw at most. 0 = uncapped (draw every rAF). */
|
|
1103
|
+
maxFps: number;
|
|
1104
|
+
/**
|
|
1105
|
+
* Extra multiplier on the render resolution, on top of the tier's pixel ratio.
|
|
1106
|
+
* 1 = as the tier says. Below 1 trades sharpness for frame rate.
|
|
1107
|
+
*/
|
|
1108
|
+
renderScale: number;
|
|
1083
1109
|
}
|
|
1084
1110
|
/** What a device probe can see without a GPU context. */
|
|
1085
1111
|
interface DeviceHints {
|
|
@@ -1111,6 +1137,43 @@ declare class Settings {
|
|
|
1111
1137
|
/** Back to defaults, on disk too. */
|
|
1112
1138
|
reset(): void;
|
|
1113
1139
|
/**
|
|
1140
|
+
* Choose a starting tier from the device, ONCE.
|
|
1141
|
+
*
|
|
1142
|
+
* Only on a first visit: after someone has picked a tier, re-detecting on
|
|
1143
|
+
* every launch would silently undo their choice — which is the behaviour every
|
|
1144
|
+
* player experiences as "the settings do not save".
|
|
1145
|
+
*/
|
|
1146
|
+
autoQuality(hints?: DeviceHints): QualityTier;
|
|
1147
|
+
/** Record that the tier came from a person, not from the device probe. */
|
|
1148
|
+
chooseQuality(tier: QualityTier): void;
|
|
1149
|
+
/**
|
|
1150
|
+
* Apply the saved quality tier to a live scene, and keep applying it.
|
|
1151
|
+
*
|
|
1152
|
+
* The same shape as `bindAudio` and `bindLocale`. Until 0.38 this did not
|
|
1153
|
+
* exist: `settings.quality` was written to disk and read by NOBODY, and
|
|
1154
|
+
* `qualityEnvironment` — the function that turns a tier into something the
|
|
1155
|
+
* renderer understands — emitted patches the environment parser rejects, for
|
|
1156
|
+
* all three tiers. Both were public, documented and never once called.
|
|
1157
|
+
*/
|
|
1158
|
+
bindQuality(apply: (patch: Record<string, JsonValue>) => void): () => void;
|
|
1159
|
+
/**
|
|
1160
|
+
* Apply the saved frame cap, and again whenever it changes.
|
|
1161
|
+
*
|
|
1162
|
+
* Separate from `bindQuality` because it is not a tier: a player caps the
|
|
1163
|
+
* frame rate to keep a phone cool or a battery alive, on the highest visual
|
|
1164
|
+
* settings the device can hold, and those two choices do not belong on one
|
|
1165
|
+
* slider. 0 means uncapped. Returns the unsubscribe.
|
|
1166
|
+
*/
|
|
1167
|
+
bindFrameCap(apply: (fps: number) => void): () => void;
|
|
1168
|
+
/**
|
|
1169
|
+
* Apply the saved resolution multiplier, and again whenever it changes.
|
|
1170
|
+
*
|
|
1171
|
+
* The lever that buys the most frames per unit of ugliness on a weak GPU —
|
|
1172
|
+
* shading 44% of the pixels at 0.66 costs a soft image and nothing else,
|
|
1173
|
+
* while dropping a quality tier costs shadows entirely.
|
|
1174
|
+
*/
|
|
1175
|
+
bindRenderScale(apply: (scale: number) => void): () => void;
|
|
1176
|
+
/**
|
|
1114
1177
|
* Apply the saved language, and keep it in step both ways.
|
|
1115
1178
|
*
|
|
1116
1179
|
* The same shape as `bindAudio`: a game that writes
|
|
@@ -1128,13 +1191,22 @@ declare class Settings {
|
|
|
1128
1191
|
bindAudio(audio: AudioBuses): () => void;
|
|
1129
1192
|
}
|
|
1130
1193
|
/**
|
|
1131
|
-
* What a quality tier MEANS, as
|
|
1194
|
+
* What a quality tier MEANS, as a LIVE environment patch.
|
|
1195
|
+
*
|
|
1196
|
+
* Only the levers that actually cost: shadows, post, bloom and clouds.
|
|
1197
|
+
* Deliberately an `environment` patch, because `setEnvironment3D(engine, patch)`
|
|
1198
|
+
* already applies one live and validated — this is not a second rendering
|
|
1199
|
+
* pipeline, it is three presets for the one that exists.
|
|
1200
|
+
*
|
|
1201
|
+
* The `off` spelling matters and got this wrong for three releases: every tier
|
|
1202
|
+
* emitted `{ enabled: false }`, which the environment parser rejects
|
|
1203
|
+
* (`environment.bloom has unknown key 'enabled'`). Nothing called this function,
|
|
1204
|
+
* so nothing found out. In a live patch `null` DELETES a key — that is how a
|
|
1205
|
+
* stage is turned off — and `shadows` alone takes a real `false`.
|
|
1132
1206
|
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* `
|
|
1136
|
-
* this is not a second rendering pipeline, it is three presets for the one that
|
|
1137
|
-
* exists.
|
|
1207
|
+
* Antialiasing and pixel ratio are NOT here: `antialias` is a WebGL context
|
|
1208
|
+
* attribute and cannot change without rebuilding the renderer. See
|
|
1209
|
+
* `qualityRendering`.
|
|
1138
1210
|
*/
|
|
1139
1211
|
declare function qualityEnvironment(tier: QualityTier): Record<string, JsonValue>;
|
|
1140
1212
|
//#endregion
|
|
@@ -1184,6 +1256,8 @@ interface EngineOptions {
|
|
|
1184
1256
|
scheduler?: Scheduler;
|
|
1185
1257
|
/** Seed for `engine.rng` — set it to make a run reproducible (default: random). */
|
|
1186
1258
|
seed?: number;
|
|
1259
|
+
/** Frame cap in fps; 0 (default) runs at whatever the display offers. */
|
|
1260
|
+
maxFps?: number;
|
|
1187
1261
|
}
|
|
1188
1262
|
/**
|
|
1189
1263
|
* The game loop: drives a Scene's tree with fixed-timestep `fixedUpdate`
|
|
@@ -1295,6 +1369,32 @@ declare class Engine {
|
|
|
1295
1369
|
setScene(scene: Scene): void;
|
|
1296
1370
|
start(): void;
|
|
1297
1371
|
/**
|
|
1372
|
+
* The frame cap: fps, or 0 for "as fast as the display goes" (the default).
|
|
1373
|
+
*
|
|
1374
|
+
* Live — a settings menu writes it mid-game. It gates the whole frame, not
|
|
1375
|
+
* just the render: an uncapped loop on a 144 Hz phone burns battery running
|
|
1376
|
+
* game logic nobody sees.
|
|
1377
|
+
*
|
|
1378
|
+
* `tick()` is deliberately NOT gated. A headless harness driving its own
|
|
1379
|
+
* clock asked for that frame; the cap is about the real-time loop.
|
|
1380
|
+
*/
|
|
1381
|
+
maxFps: number;
|
|
1382
|
+
private nextFrameAt;
|
|
1383
|
+
private lastOfferedAt;
|
|
1384
|
+
private displayPeriodMs;
|
|
1385
|
+
/**
|
|
1386
|
+
* Should this display frame run?
|
|
1387
|
+
*
|
|
1388
|
+
* The obvious `if (now - last < interval) return` is wrong in a way that only
|
|
1389
|
+
* shows up between the round numbers: a 45 fps cap on a 60 Hz display misses
|
|
1390
|
+
* every other deadline by a fraction of a millisecond, waits a whole display
|
|
1391
|
+
* frame more, and delivers 30 — asking for 45 makes the game slower than not
|
|
1392
|
+
* capping at all. The fix is to allow a frame that is within half a display
|
|
1393
|
+
* period of its deadline, and to advance the deadline by the interval rather
|
|
1394
|
+
* than from `now`, so the error does not accumulate.
|
|
1395
|
+
*/
|
|
1396
|
+
private frameIsDue;
|
|
1397
|
+
/**
|
|
1298
1398
|
* A frame threw somewhere outside a single node (a renderer, physics, a
|
|
1299
1399
|
* signal handler). Report the first one and count the rest: a broken frame
|
|
1300
1400
|
* repeats sixty times a second, and sixty identical traces a second is how
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-
|
|
3
|
-
import { h as Engine, m as AudioPlayer } from "./register-
|
|
2
|
+
import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
|
|
3
|
+
import { h as Engine, m as AudioPlayer } from "./register-Dm7nrd-E.js";
|
|
4
4
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
5
5
|
import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
|
|
6
6
|
import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-CCtAMDLB.js";
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-CsJDUQh_.js";
|
|
8
|
+
import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DnB9m7Rl.js";
|
|
9
9
|
import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
|
|
10
|
-
import { n as enablePhysics2D } from "./physics-2d-
|
|
10
|
+
import { n as enablePhysics2D } from "./physics-2d-OfKb0PV_.js";
|
|
11
11
|
import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
|
|
12
12
|
//#region src/2d/assets.ts
|
|
13
13
|
/**
|