incanto 0.7.0 → 0.8.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-check.mjs +6 -4
- package/dist/2d.d.ts +93 -3
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +45 -4
- package/dist/3d.js +4 -4
- package/dist/{audio-player-VSjk_vG8.d.ts → audio-player-Dyjg5k92.d.ts} +1 -1
- package/dist/audit-C6rMyict.js +58 -0
- package/dist/{behavior-e3kAmPGC.d.ts → behavior-Bod1AyJS.d.ts} +211 -174
- package/dist/{create-game-CEFoFN8d.js → create-game-66c4iYJE.js} +76 -6
- package/dist/{create-game-X5iRu6pf.js → create-game-BNDSbhy-.js} +88 -6
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +3 -0
- package/dist/{errors-BMFaY68Q.d.ts → errors-1dXlIwoR.d.ts} +7 -1
- package/dist/{gameplay-CDFgSG6z.js → gameplay-DPMgZk9W.js} +108 -1
- package/dist/gameplay.d.ts +44 -3
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +151 -9
- package/dist/index.js +189 -4
- package/dist/{loader-y_X4zYO0.d.ts → loader-BcUDfNHn.d.ts} +1 -1
- package/dist/net.d.ts +46 -2
- package/dist/net.js +54 -2
- package/dist/particle-sim-CyUU7HVU.js +281 -0
- package/dist/{physics-2d-DmQ540uR.js → physics-2d-q2N362ph.js} +45 -4
- package/dist/{physics-3d-CDjuhtt_.js → physics-3d-CvHr31d4.js} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-CscQqB7V.js → register-Cbs7QxoV.js} +139 -4
- package/dist/{register-ClKnoILk.js → register-Cqjxrfq0.js} +254 -4
- package/dist/{register-C35HDZpm.js → register-CxmuI9FL.js} +1 -1
- package/dist/{particle-sim-CrTE7c02.js → register-DRPIjrKG.js} +1185 -278
- package/dist/test.d.ts +4 -10
- package/dist/test.js +11 -16
- package/editor/assets/{agent8-1WJU-yXr.js → agent8-B8QnWCeP.js} +1 -1
- package/editor/assets/{index-DzYFgZbl.js → index-CVhnNkb3.js} +81 -81
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +427 -0
- package/skills/incanto-3d-character.md +14 -0
- package/skills/incanto-audio.md +23 -0
- package/skills/incanto-building-2d-games.md +31 -0
- package/skills/incanto-building-3d-games.md +21 -0
- package/skills/incanto-gameplay-behaviors.md +28 -0
- package/skills/incanto-hud.md +24 -0
- package/skills/incanto-multiplayer.md +27 -0
- package/skills/incanto-node-reference.md +73 -0
- package/skills/incanto-physics-and-input.md +9 -0
- package/skills/incanto-verifying-your-game.md +40 -0
- package/dist/register-Dl_ixIJe.js +0 -834
package/dist/index.js
CHANGED
|
@@ -1,13 +1,128 @@
|
|
|
1
1
|
import { _ as registerBehavior, a as SCENE_FORMAT, c as SceneTree, d as resolveConstants, f as Node, g as getBehavior, h as clearBehaviors, i as serializeNode, l as CONST_REF_KEY, m as Behavior, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, v as registeredBehaviors, y as Signal } from "./loader-B4OEXDZ8.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { C as crossfadeGains, S as WebAudioMusicBackend, T as AudioBuses, _ as spatialPan, a as HudLayer, b as synthSfx, c as UiText, d as LogManager, f as InputMap, g as spatialGain, h as ROLLOFF_MODELS, i as UiDialogue, l as AudioPlayer, m as isAudioContextAvailable, n as Timer, o as UiBanner, p as SfxEngine, r as UiButton, s as UiBar, t as registerCoreNodes, u as Engine, v as SFX_PRESETS, w as fadeGain, x as MusicManager, y as SFX_PRESET_NAMES } from "./register-DRPIjrKG.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
|
-
import {
|
|
5
|
+
import { t as auditScene } from "./audit-C6rMyict.js";
|
|
6
6
|
import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
7
7
|
import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-BVJ2HbCn.js";
|
|
8
8
|
import { t as createNoise2D } from "./noise-CGUMx44x.js";
|
|
9
|
+
import { a as ORDER_GROUP_BASE, i as applyParticlePreset, n as PARTICLE_PRESETS, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CyUU7HVU.js";
|
|
9
10
|
import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-031PxtCR.js";
|
|
10
11
|
import { t as duplicateNode } from "./duplicate-BEvGBtb_.js";
|
|
12
|
+
//#region src/core/pathfinding.ts
|
|
13
|
+
/**
|
|
14
|
+
* A* from `from` to `to` (inclusive cell coords). Returns the cell path
|
|
15
|
+
* INCLUDING both endpoints, or null when unreachable. Straight steps cost
|
|
16
|
+
* 1, diagonals √2; the heuristic is octile (admissible for both modes).
|
|
17
|
+
*/
|
|
18
|
+
function findPath(grid, from, to, opts) {
|
|
19
|
+
const [sx, sy] = from;
|
|
20
|
+
const [tx, ty] = to;
|
|
21
|
+
const { width, height } = grid;
|
|
22
|
+
const inBounds = (x, y) => x >= 0 && y >= 0 && x < width && y < height;
|
|
23
|
+
if (!inBounds(sx, sy) || !inBounds(tx, ty)) return null;
|
|
24
|
+
if (grid.solid(sx, sy) || grid.solid(tx, ty)) return null;
|
|
25
|
+
if (sx === tx && sy === ty) return [[sx, sy]];
|
|
26
|
+
const diagonal = opts?.diagonal ?? true;
|
|
27
|
+
const maxExpansions = opts?.maxExpansions ?? 2e4;
|
|
28
|
+
const idx = (x, y) => y * width + x;
|
|
29
|
+
const gScore = new Float64Array(width * height).fill(Number.POSITIVE_INFINITY);
|
|
30
|
+
const cameFrom = new Int32Array(width * height).fill(-1);
|
|
31
|
+
const closed = new Uint8Array(width * height);
|
|
32
|
+
const heap = [];
|
|
33
|
+
const fScore = new Float64Array(width * height).fill(Number.POSITIVE_INFINITY);
|
|
34
|
+
const push = (n) => {
|
|
35
|
+
heap.push(n);
|
|
36
|
+
let i = heap.length - 1;
|
|
37
|
+
while (i > 0) {
|
|
38
|
+
const p = i - 1 >> 1;
|
|
39
|
+
if (fScore[heap[p]] <= fScore[heap[i]]) break;
|
|
40
|
+
const t = heap[p];
|
|
41
|
+
heap[p] = heap[i];
|
|
42
|
+
heap[i] = t;
|
|
43
|
+
i = p;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const pop = () => {
|
|
47
|
+
const top = heap[0];
|
|
48
|
+
const last = heap.pop();
|
|
49
|
+
if (heap.length > 0) {
|
|
50
|
+
heap[0] = last;
|
|
51
|
+
let i = 0;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const l = i * 2 + 1;
|
|
54
|
+
const r = l + 1;
|
|
55
|
+
let m = i;
|
|
56
|
+
if (l < heap.length && fScore[heap[l]] < fScore[heap[m]]) m = l;
|
|
57
|
+
if (r < heap.length && fScore[heap[r]] < fScore[heap[m]]) m = r;
|
|
58
|
+
if (m === i) break;
|
|
59
|
+
const t = heap[m];
|
|
60
|
+
heap[m] = heap[i];
|
|
61
|
+
heap[i] = t;
|
|
62
|
+
i = m;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return top;
|
|
66
|
+
};
|
|
67
|
+
const SQRT2 = Math.SQRT2;
|
|
68
|
+
const octile = (x, y) => {
|
|
69
|
+
const dx = Math.abs(x - tx);
|
|
70
|
+
const dy = Math.abs(y - ty);
|
|
71
|
+
return dx > dy ? dx - dy + SQRT2 * dy : dy - dx + SQRT2 * dx;
|
|
72
|
+
};
|
|
73
|
+
const start = idx(sx, sy);
|
|
74
|
+
gScore[start] = 0;
|
|
75
|
+
fScore[start] = octile(sx, sy);
|
|
76
|
+
push(start);
|
|
77
|
+
let expansions = 0;
|
|
78
|
+
while (heap.length > 0) {
|
|
79
|
+
if (++expansions > maxExpansions) return null;
|
|
80
|
+
const current = pop();
|
|
81
|
+
if (closed[current]) continue;
|
|
82
|
+
closed[current] = 1;
|
|
83
|
+
const cx = current % width;
|
|
84
|
+
const cy = current / width | 0;
|
|
85
|
+
if (cx === tx && cy === ty) {
|
|
86
|
+
const path = [];
|
|
87
|
+
let n = current;
|
|
88
|
+
while (n !== -1) {
|
|
89
|
+
path.push([n % width, n / width | 0]);
|
|
90
|
+
n = cameFrom[n];
|
|
91
|
+
}
|
|
92
|
+
path.reverse();
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
96
|
+
if (dx === 0 && dy === 0) continue;
|
|
97
|
+
const isDiag = dx !== 0 && dy !== 0;
|
|
98
|
+
if (isDiag && !diagonal) continue;
|
|
99
|
+
const nx = cx + dx;
|
|
100
|
+
const ny = cy + dy;
|
|
101
|
+
if (!inBounds(nx, ny) || grid.solid(nx, ny)) continue;
|
|
102
|
+
if (isDiag && (grid.solid(cx + dx, cy) || grid.solid(cx, cy + dy))) continue;
|
|
103
|
+
const n = idx(nx, ny);
|
|
104
|
+
if (closed[n]) continue;
|
|
105
|
+
const tentative = gScore[current] + (isDiag ? SQRT2 : 1);
|
|
106
|
+
if (tentative < gScore[n]) {
|
|
107
|
+
gScore[n] = tentative;
|
|
108
|
+
fScore[n] = tentative + octile(nx, ny);
|
|
109
|
+
cameFrom[n] = current;
|
|
110
|
+
push(n);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Build a PathGrid from row-strings ('#' = solid) — tests and tile games. */
|
|
117
|
+
function gridFromRows(rows) {
|
|
118
|
+
const height = rows.length;
|
|
119
|
+
return {
|
|
120
|
+
width: rows[0]?.length ?? 0,
|
|
121
|
+
height,
|
|
122
|
+
solid: (x, y) => (rows[y]?.[x] ?? "#") === "#"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
11
126
|
//#region src/core/preload.ts
|
|
12
127
|
/**
|
|
13
128
|
* Warm the HTTP cache for a list of asset urls with a progress callback —
|
|
@@ -40,6 +155,76 @@ function assetUrls(assets) {
|
|
|
40
155
|
return Object.values(assets ?? {}).map((a) => a.url).filter((u) => typeof u === "string" && !u.startsWith("data:"));
|
|
41
156
|
}
|
|
42
157
|
//#endregion
|
|
158
|
+
//#region src/core/replay.ts
|
|
159
|
+
const RECORDED_METHODS = [
|
|
160
|
+
"handleKey",
|
|
161
|
+
"handleMouseButton",
|
|
162
|
+
"handlePointerMove",
|
|
163
|
+
"handleWheel",
|
|
164
|
+
"pressAction",
|
|
165
|
+
"releaseAction"
|
|
166
|
+
];
|
|
167
|
+
/**
|
|
168
|
+
* Tap the engine's input entry points and tick clock. Recording starts at
|
|
169
|
+
* the CURRENT moment — start it before the first tick for full-run replays.
|
|
170
|
+
*/
|
|
171
|
+
function startRecording(engine) {
|
|
172
|
+
const ticks = [];
|
|
173
|
+
const events = [];
|
|
174
|
+
const input = engine.input;
|
|
175
|
+
const originals = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const name of RECORDED_METHODS) {
|
|
177
|
+
const original = input[name];
|
|
178
|
+
originals.set(name, original);
|
|
179
|
+
input[name] = (...args) => {
|
|
180
|
+
events.push([
|
|
181
|
+
ticks.length,
|
|
182
|
+
name,
|
|
183
|
+
...args
|
|
184
|
+
]);
|
|
185
|
+
return original.apply(engine.input, args);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const originalTick = engine.tick.bind(engine);
|
|
189
|
+
const engineAny = engine;
|
|
190
|
+
engineAny.tick = (nowMs) => {
|
|
191
|
+
ticks.push(nowMs);
|
|
192
|
+
originalTick(nowMs);
|
|
193
|
+
};
|
|
194
|
+
let stopped = false;
|
|
195
|
+
return { stop() {
|
|
196
|
+
if (!stopped) {
|
|
197
|
+
stopped = true;
|
|
198
|
+
for (const [name, fn] of originals) input[name] = fn;
|
|
199
|
+
engineAny.tick = originalTick;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
replay: 1,
|
|
203
|
+
ticks,
|
|
204
|
+
events
|
|
205
|
+
};
|
|
206
|
+
} };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Drive a fresh engine through a recording: for each recorded tick, feed
|
|
210
|
+
* that tick's input events first, then tick with the recorded timestamp.
|
|
211
|
+
* `onTick(index)` lets tests assert mid-run state.
|
|
212
|
+
*/
|
|
213
|
+
function replay(engine, recording, opts) {
|
|
214
|
+
const input = engine.input;
|
|
215
|
+
let cursor = 0;
|
|
216
|
+
for (let i = 0; i < recording.ticks.length; i++) {
|
|
217
|
+
while (cursor < recording.events.length && recording.events[cursor][0] === i) {
|
|
218
|
+
const [, method, ...args] = recording.events[cursor];
|
|
219
|
+
const fn = input[method];
|
|
220
|
+
if (typeof fn === "function") fn.apply(engine.input, args);
|
|
221
|
+
cursor += 1;
|
|
222
|
+
}
|
|
223
|
+
engine.tick(recording.ticks[i]);
|
|
224
|
+
opts?.onTick?.(i);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
43
228
|
//#region src/core/save.ts
|
|
44
229
|
const memoryStores = /* @__PURE__ */ new Map();
|
|
45
230
|
function createSaveStore(namespace) {
|
|
@@ -110,6 +295,6 @@ function newUid() {
|
|
|
110
295
|
//#endregion
|
|
111
296
|
//#region src/index.ts
|
|
112
297
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
113
|
-
const VERSION = "0.
|
|
298
|
+
const VERSION = "0.8.0";
|
|
114
299
|
//#endregion
|
|
115
|
-
export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, UiBanner, UiBar, UiText, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, synthSfx };
|
|
300
|
+
export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiText, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, startRecording, synthSfx };
|
package/dist/net.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
|
|
2
|
-
import {
|
|
2
|
+
import { $ as Node, l as PropSchema, tt as Signal, v as Engine } from "./behavior-Bod1AyJS.js";
|
|
3
3
|
|
|
4
4
|
//#region src/net/types.d.ts
|
|
5
5
|
type Unsubscribe = () => void;
|
|
@@ -424,4 +424,48 @@ declare class NetworkSpawner extends Node {
|
|
|
424
424
|
*/
|
|
425
425
|
declare function registerNodesNet(): void;
|
|
426
426
|
//#endregion
|
|
427
|
-
|
|
427
|
+
//#region src/net/split-screen.d.ts
|
|
428
|
+
interface SplitScreenPlayer {
|
|
429
|
+
account: string;
|
|
430
|
+
engine: Engine;
|
|
431
|
+
manager: NetworkManager;
|
|
432
|
+
}
|
|
433
|
+
interface SplitScreenOptions extends LocalGameServerOptions {
|
|
434
|
+
/** The shared scene JSON every panel loads. */
|
|
435
|
+
scene: SceneJson;
|
|
436
|
+
/** Player accounts, one panel each (default ['p1', 'p2']). */
|
|
437
|
+
accounts?: string[];
|
|
438
|
+
/** Called per player to finish wiring (renderer, input, HUD). */
|
|
439
|
+
setup: (player: SplitScreenPlayer, index: number) => void | Promise<void>;
|
|
440
|
+
/** name → scene JSON registered on every panel's NetworkManager. */
|
|
441
|
+
scenes?: Record<string, SceneJson>;
|
|
442
|
+
/** Engine seed base (each panel gets seed + index). */
|
|
443
|
+
seed?: number;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* N local clients on ONE page sharing ONE in-memory game server — the
|
|
447
|
+
* split-screen preview harness three examples were hand-wiring (~40 lines
|
|
448
|
+
* each). Wires: LocalGameServer + per-player Engine/scene/NetworkManager +
|
|
449
|
+
* the server tick driven from the first panel's clock.
|
|
450
|
+
*
|
|
451
|
+
* const { players, server } = await createSplitScreen({
|
|
452
|
+
* scene: gameJson,
|
|
453
|
+
* server: MyServerClass,
|
|
454
|
+
* scenes: { 'remote-player': remoteJson },
|
|
455
|
+
* setup: ({ engine }, i) => {
|
|
456
|
+
* new Renderer2D({ canvas: canvases[i], engine });
|
|
457
|
+
* engine.input.attachKeyboard(window);
|
|
458
|
+
* engine.start();
|
|
459
|
+
* },
|
|
460
|
+
* });
|
|
461
|
+
*
|
|
462
|
+
* Going live stays the one-line story: replace the transport with
|
|
463
|
+
* `createAgent8Server()` and boot ONE client per browser.
|
|
464
|
+
*/
|
|
465
|
+
declare function createSplitScreen(opts: SplitScreenOptions): Promise<{
|
|
466
|
+
server: ReturnType<typeof createLocalGameServer>;
|
|
467
|
+
players: SplitScreenPlayer[];
|
|
468
|
+
dispose(): void;
|
|
469
|
+
}>;
|
|
470
|
+
//#endregion
|
|
471
|
+
export { type CollectionOptions, LocalGameServer, type LocalGameServerOptions, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, type NetworkManagerOptions, NetworkSpawner, type NetworkTransport, type ServerClass, type SplitScreenOptions, type SplitScreenPlayer, type Unsubscribe, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
|
package/dist/net.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { n as loadScene } from "./loader-B4OEXDZ8.js";
|
|
2
|
+
import { u as Engine } from "./register-DRPIjrKG.js";
|
|
1
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
4
|
import { t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
3
5
|
import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
|
|
4
|
-
import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-
|
|
6
|
+
import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-CxmuI9FL.js";
|
|
5
7
|
//#region src/net/loopback.ts
|
|
6
8
|
/**
|
|
7
9
|
* In-memory multiplayer hub implementing the SAME kernel contract as
|
|
@@ -769,4 +771,54 @@ var LocalGameServerTransport = class {
|
|
|
769
771
|
}
|
|
770
772
|
};
|
|
771
773
|
//#endregion
|
|
772
|
-
|
|
774
|
+
//#region src/net/split-screen.ts
|
|
775
|
+
/**
|
|
776
|
+
* N local clients on ONE page sharing ONE in-memory game server — the
|
|
777
|
+
* split-screen preview harness three examples were hand-wiring (~40 lines
|
|
778
|
+
* each). Wires: LocalGameServer + per-player Engine/scene/NetworkManager +
|
|
779
|
+
* the server tick driven from the first panel's clock.
|
|
780
|
+
*
|
|
781
|
+
* const { players, server } = await createSplitScreen({
|
|
782
|
+
* scene: gameJson,
|
|
783
|
+
* server: MyServerClass,
|
|
784
|
+
* scenes: { 'remote-player': remoteJson },
|
|
785
|
+
* setup: ({ engine }, i) => {
|
|
786
|
+
* new Renderer2D({ canvas: canvases[i], engine });
|
|
787
|
+
* engine.input.attachKeyboard(window);
|
|
788
|
+
* engine.start();
|
|
789
|
+
* },
|
|
790
|
+
* });
|
|
791
|
+
*
|
|
792
|
+
* Going live stays the one-line story: replace the transport with
|
|
793
|
+
* `createAgent8Server()` and boot ONE client per browser.
|
|
794
|
+
*/
|
|
795
|
+
async function createSplitScreen(opts) {
|
|
796
|
+
const server = createLocalGameServer(opts);
|
|
797
|
+
const accounts = opts.accounts ?? ["p1", "p2"];
|
|
798
|
+
const players = [];
|
|
799
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
800
|
+
const account = accounts[i];
|
|
801
|
+
const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + i } : {});
|
|
802
|
+
engine.setScene(loadScene(jsonClone(opts.scene)));
|
|
803
|
+
const manager = await NetworkManager.create(engine, { transport: server.createClient(account) });
|
|
804
|
+
for (const [name, json] of Object.entries(opts.scenes ?? {})) manager.registerScene(name, json);
|
|
805
|
+
const player = {
|
|
806
|
+
account,
|
|
807
|
+
engine,
|
|
808
|
+
manager
|
|
809
|
+
};
|
|
810
|
+
players.push(player);
|
|
811
|
+
await opts.setup(player, i);
|
|
812
|
+
}
|
|
813
|
+
const disconnect = (players[0]?.engine)?.updated.connect((dt) => void server.tick(Math.min(dt, .05) * 1e3));
|
|
814
|
+
return {
|
|
815
|
+
server,
|
|
816
|
+
players,
|
|
817
|
+
dispose() {
|
|
818
|
+
disconnect?.();
|
|
819
|
+
for (const p of players) p.engine.dispose?.();
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { n as jsonEquals } from "./json-BLk7H2Qa.js";
|
|
2
|
+
//#region src/core/order-groups.ts
|
|
3
|
+
const ORDER_GROUP_BASE = {
|
|
4
|
+
background: -2e3,
|
|
5
|
+
terrain: -1e3,
|
|
6
|
+
default: 0,
|
|
7
|
+
characters: 1e3,
|
|
8
|
+
effects: 2e3,
|
|
9
|
+
overlay: 3e3
|
|
10
|
+
};
|
|
11
|
+
/** base + fine offset (unknown group names fall back to `default`). */
|
|
12
|
+
function effectiveOrder(group, renderOrder) {
|
|
13
|
+
return (ORDER_GROUP_BASE[group] ?? 0) + renderOrder;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/core/particle-presets.ts
|
|
17
|
+
const PARTICLE_PRESETS = {
|
|
18
|
+
fire: {
|
|
19
|
+
rate: 60,
|
|
20
|
+
lifetime: [.4, .9],
|
|
21
|
+
speed: [30, 70],
|
|
22
|
+
directionDeg: -90,
|
|
23
|
+
spreadDeg: 35,
|
|
24
|
+
gravity: [0, -30],
|
|
25
|
+
sizeStart: 14,
|
|
26
|
+
sizeEnd: 3,
|
|
27
|
+
colorStart: "#ffcf5a",
|
|
28
|
+
colorEnd: "#e8401f",
|
|
29
|
+
alphaStart: .9,
|
|
30
|
+
alphaEnd: 0,
|
|
31
|
+
blend: "add"
|
|
32
|
+
},
|
|
33
|
+
smoke: {
|
|
34
|
+
rate: 20,
|
|
35
|
+
lifetime: [1.5, 2.5],
|
|
36
|
+
speed: [10, 25],
|
|
37
|
+
directionDeg: -90,
|
|
38
|
+
spreadDeg: 40,
|
|
39
|
+
gravity: [0, -12],
|
|
40
|
+
sizeStart: 10,
|
|
41
|
+
sizeEnd: 26,
|
|
42
|
+
colorStart: "#8a8a8a",
|
|
43
|
+
colorEnd: "#3a3a3a",
|
|
44
|
+
alphaStart: .35,
|
|
45
|
+
alphaEnd: 0,
|
|
46
|
+
blend: "normal"
|
|
47
|
+
},
|
|
48
|
+
sparks: {
|
|
49
|
+
rate: 80,
|
|
50
|
+
lifetime: [.3, .7],
|
|
51
|
+
speed: [120, 260],
|
|
52
|
+
spreadDeg: 360,
|
|
53
|
+
gravity: [0, 240],
|
|
54
|
+
drag: 1,
|
|
55
|
+
sizeStart: 4,
|
|
56
|
+
sizeEnd: 1,
|
|
57
|
+
colorStart: "#fff3b0",
|
|
58
|
+
colorEnd: "#ff9d2e",
|
|
59
|
+
blend: "add"
|
|
60
|
+
},
|
|
61
|
+
fireworks: {
|
|
62
|
+
rate: 0,
|
|
63
|
+
burst: 120,
|
|
64
|
+
lifetime: [.8, 1.6],
|
|
65
|
+
speed: [80, 240],
|
|
66
|
+
spreadDeg: 360,
|
|
67
|
+
gravity: [0, 90],
|
|
68
|
+
drag: .6,
|
|
69
|
+
sizeStart: 5,
|
|
70
|
+
sizeEnd: 1,
|
|
71
|
+
colorStart: "#9ad8ff",
|
|
72
|
+
colorEnd: "#ff5ad8",
|
|
73
|
+
blend: "add"
|
|
74
|
+
},
|
|
75
|
+
explosion: {
|
|
76
|
+
rate: 0,
|
|
77
|
+
burst: 60,
|
|
78
|
+
lifetime: [.25, .6],
|
|
79
|
+
speed: [150, 420],
|
|
80
|
+
spreadDeg: 360,
|
|
81
|
+
drag: 3,
|
|
82
|
+
sizeStart: 16,
|
|
83
|
+
sizeEnd: 2,
|
|
84
|
+
colorStart: "#ffe08a",
|
|
85
|
+
colorEnd: "#ff3b1f",
|
|
86
|
+
blend: "add"
|
|
87
|
+
},
|
|
88
|
+
flash: {
|
|
89
|
+
rate: 0,
|
|
90
|
+
burst: 8,
|
|
91
|
+
lifetime: [.08, .16],
|
|
92
|
+
speed: [0, 30],
|
|
93
|
+
spreadDeg: 360,
|
|
94
|
+
sizeStart: 64,
|
|
95
|
+
sizeEnd: 96,
|
|
96
|
+
colorStart: "#ffffff",
|
|
97
|
+
colorEnd: "#ffffff",
|
|
98
|
+
alphaStart: 1,
|
|
99
|
+
alphaEnd: 0,
|
|
100
|
+
blend: "add"
|
|
101
|
+
},
|
|
102
|
+
lightning: {
|
|
103
|
+
rate: 90,
|
|
104
|
+
lifetime: [.05, .12],
|
|
105
|
+
speed: [400, 900],
|
|
106
|
+
directionDeg: -90,
|
|
107
|
+
spreadDeg: 14,
|
|
108
|
+
sizeStart: 6,
|
|
109
|
+
sizeEnd: 2,
|
|
110
|
+
colorStart: "#eaf2ff",
|
|
111
|
+
colorEnd: "#7fb4ff",
|
|
112
|
+
blend: "add"
|
|
113
|
+
},
|
|
114
|
+
rain: {
|
|
115
|
+
rate: 160,
|
|
116
|
+
lifetime: [.8, 1.2],
|
|
117
|
+
speed: [300, 420],
|
|
118
|
+
directionDeg: 90,
|
|
119
|
+
spreadDeg: 6,
|
|
120
|
+
sizeStart: 3,
|
|
121
|
+
sizeEnd: 3,
|
|
122
|
+
colorStart: "#9ec8ff",
|
|
123
|
+
colorEnd: "#9ec8ff",
|
|
124
|
+
alphaStart: .5,
|
|
125
|
+
alphaEnd: .2,
|
|
126
|
+
blend: "normal"
|
|
127
|
+
},
|
|
128
|
+
snow: {
|
|
129
|
+
rate: 40,
|
|
130
|
+
lifetime: [3, 5],
|
|
131
|
+
speed: [20, 50],
|
|
132
|
+
directionDeg: 90,
|
|
133
|
+
spreadDeg: 30,
|
|
134
|
+
drag: .2,
|
|
135
|
+
sizeStart: 5,
|
|
136
|
+
sizeEnd: 4,
|
|
137
|
+
colorStart: "#ffffff",
|
|
138
|
+
colorEnd: "#ffffff",
|
|
139
|
+
alphaStart: .9,
|
|
140
|
+
alphaEnd: .6,
|
|
141
|
+
blend: "normal"
|
|
142
|
+
},
|
|
143
|
+
magic: {
|
|
144
|
+
rate: 50,
|
|
145
|
+
lifetime: [.6, 1.4],
|
|
146
|
+
speed: [20, 80],
|
|
147
|
+
spreadDeg: 360,
|
|
148
|
+
gravity: [0, -20],
|
|
149
|
+
sizeStart: 7,
|
|
150
|
+
sizeEnd: 1,
|
|
151
|
+
colorStart: "#9dffe8",
|
|
152
|
+
colorEnd: "#7a5cff",
|
|
153
|
+
blend: "add"
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const PARTICLE_PRESET_NAMES = Object.keys(PARTICLE_PRESETS);
|
|
157
|
+
/**
|
|
158
|
+
* Apply the delta rule across three layers: schema default < preset < the
|
|
159
|
+
* scene's explicit props. A prop still equal to its SCHEMA default is
|
|
160
|
+
* considered "unset" and takes the preset value.
|
|
161
|
+
*/
|
|
162
|
+
function applyParticlePreset(target, presetName, schema) {
|
|
163
|
+
if (presetName === "custom") return;
|
|
164
|
+
const preset = PARTICLE_PRESETS[presetName];
|
|
165
|
+
if (!preset) return;
|
|
166
|
+
for (const [key, presetValue] of Object.entries(preset)) {
|
|
167
|
+
const def = schema[key]?.default;
|
|
168
|
+
const current = target[key];
|
|
169
|
+
if (def !== void 0 && jsonEquals(current, def)) target[key] = Array.isArray(presetValue) ? [...presetValue] : presetValue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/core/particle-sim.ts
|
|
174
|
+
const FIELDS = 9;
|
|
175
|
+
var ParticleSim = class {
|
|
176
|
+
config;
|
|
177
|
+
rng;
|
|
178
|
+
data;
|
|
179
|
+
alive = 0;
|
|
180
|
+
spawnAccumulator = 0;
|
|
181
|
+
everSpawned = false;
|
|
182
|
+
constructor(config, rng) {
|
|
183
|
+
this.config = config;
|
|
184
|
+
this.rng = rng;
|
|
185
|
+
this.data = new Float64Array(Math.max(1, config.maxParticles) * FIELDS);
|
|
186
|
+
}
|
|
187
|
+
/** Change the emission rate LIVE (particles/sec; 0 pauses emission) — lets an
|
|
188
|
+
* emitter toggle on/off at runtime (drift smoke, throttle flames). */
|
|
189
|
+
setRate(rate) {
|
|
190
|
+
this.config.rate = rate;
|
|
191
|
+
if (rate <= 0) this.spawnAccumulator = 0;
|
|
192
|
+
}
|
|
193
|
+
get count() {
|
|
194
|
+
return this.alive;
|
|
195
|
+
}
|
|
196
|
+
/** True when nothing is alive and at least one particle has ever spawned. */
|
|
197
|
+
get done() {
|
|
198
|
+
return this.everSpawned && this.alive === 0;
|
|
199
|
+
}
|
|
200
|
+
/** Spawn n particles immediately (fireworks, explosions, flashes). */
|
|
201
|
+
burst(n) {
|
|
202
|
+
for (let i = 0; i < n; i++) this.spawn();
|
|
203
|
+
}
|
|
204
|
+
update(dt) {
|
|
205
|
+
if (this.config.rate > 0) {
|
|
206
|
+
this.spawnAccumulator += this.config.rate * dt;
|
|
207
|
+
while (this.spawnAccumulator >= 1) {
|
|
208
|
+
this.spawnAccumulator -= 1;
|
|
209
|
+
this.spawn();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const d = this.data;
|
|
213
|
+
const [gx, gy, gz] = this.config.gravity;
|
|
214
|
+
const damp = this.config.drag > 0 ? Math.exp(-this.config.drag * dt) : 1;
|
|
215
|
+
let i = 0;
|
|
216
|
+
while (i < this.alive) {
|
|
217
|
+
const base = i * FIELDS;
|
|
218
|
+
const age = d[base + 6] + dt;
|
|
219
|
+
if (age >= d[base + 7]) {
|
|
220
|
+
this.kill(i);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
d[base + 6] = age;
|
|
224
|
+
d[base + 3] = (d[base + 3] + gx * dt) * damp;
|
|
225
|
+
d[base + 4] = (d[base + 4] + gy * dt) * damp;
|
|
226
|
+
d[base + 5] = (d[base + 5] + gz * dt) * damp;
|
|
227
|
+
d[base + 0] = d[base + 0] + d[base + 3] * dt;
|
|
228
|
+
d[base + 1] = d[base + 1] + d[base + 4] * dt;
|
|
229
|
+
d[base + 2] = d[base + 2] + d[base + 5] * dt;
|
|
230
|
+
i += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
forEach(fn) {
|
|
234
|
+
const d = this.data;
|
|
235
|
+
for (let i = 0; i < this.alive; i++) {
|
|
236
|
+
const base = i * FIELDS;
|
|
237
|
+
const age = d[base + 6];
|
|
238
|
+
const life = d[base + 7];
|
|
239
|
+
fn({
|
|
240
|
+
x: d[base + 0],
|
|
241
|
+
y: d[base + 1],
|
|
242
|
+
z: d[base + 2],
|
|
243
|
+
vx: d[base + 3],
|
|
244
|
+
vy: d[base + 4],
|
|
245
|
+
vz: d[base + 5],
|
|
246
|
+
age,
|
|
247
|
+
life,
|
|
248
|
+
t: life > 0 ? age / life : 1,
|
|
249
|
+
seed: d[base + 8]
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
spawn() {
|
|
254
|
+
if (this.alive >= this.config.maxParticles) return;
|
|
255
|
+
const base = this.alive * FIELDS;
|
|
256
|
+
const d = this.data;
|
|
257
|
+
const c = this.config;
|
|
258
|
+
const angle = (c.directionDeg + (this.rng.next() - .5) * c.spreadDeg) * Math.PI / 180;
|
|
259
|
+
const speed = c.speed[0] + this.rng.next() * (c.speed[1] - c.speed[0]);
|
|
260
|
+
const zTilt = c.spreadZ ? (this.rng.next() - .5) * (c.spreadDeg * Math.PI / 180) : 0;
|
|
261
|
+
const planar = Math.cos(zTilt);
|
|
262
|
+
d[base + 0] = 0;
|
|
263
|
+
d[base + 1] = 0;
|
|
264
|
+
d[base + 2] = 0;
|
|
265
|
+
d[base + 3] = Math.cos(angle) * speed * planar;
|
|
266
|
+
d[base + 4] = Math.sin(angle) * speed * planar;
|
|
267
|
+
d[base + 5] = Math.sin(zTilt) * speed;
|
|
268
|
+
d[base + 6] = 0;
|
|
269
|
+
d[base + 7] = c.lifetime[0] + this.rng.next() * (c.lifetime[1] - c.lifetime[0]);
|
|
270
|
+
d[base + 8] = this.rng.next();
|
|
271
|
+
this.alive += 1;
|
|
272
|
+
this.everSpawned = true;
|
|
273
|
+
}
|
|
274
|
+
kill(index) {
|
|
275
|
+
const last = this.alive - 1;
|
|
276
|
+
if (index !== last) this.data.copyWithin(index * FIELDS, last * FIELDS, last * FIELDS + FIELDS);
|
|
277
|
+
this.alive = last;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
//#endregion
|
|
281
|
+
export { ORDER_GROUP_BASE as a, applyParticlePreset as i, PARTICLE_PRESETS as n, effectiveOrder as o, PARTICLE_PRESET_NAMES as r, ParticleSim as t };
|