@babylonjsmarket/arcade 0.3.11 → 0.3.12
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/dist/cli/eject.d.ts +24 -1
- package/dist/cli/eject.d.ts.map +1 -1
- package/dist/cli/eject.js +27 -22
- package/dist/cli/eject.js.map +1 -1
- package/package.json +3 -2
- package/src/Components/Animation/Animation.core.test.ts +11 -0
- package/src/Components/Animation/Animation.test.ts +26 -0
- package/src/Components/ArcCamera/ArcCamera.test.ts +168 -0
- package/src/Components/Bullet/Bullet.test.ts +223 -0
- package/src/Components/BulletPool/BulletPool.test.ts +50 -1
- package/src/Components/Bumper/Bumper.test.ts +18 -0
- package/src/Components/CameraFollow/CameraFollow.core.test.ts +19 -0
- package/src/Components/CameraFollow/CameraFollow.test.ts +92 -0
- package/src/Components/DirectionalLight/DirectionalLight.test.ts +48 -0
- package/src/Components/Enemy/Enemy.core.test.ts +20 -1
- package/src/Components/Enemy/Enemy.test.ts +456 -1
- package/src/Components/EnemySpawner/EnemySpawner.test.ts +155 -0
- package/src/Components/EntityPool/EntityPool.test.ts +95 -1
- package/src/Components/EnvironmentTexture/EnvironmentTexture.test.ts +31 -0
- package/src/Components/Flash/Flash.test.ts +44 -0
- package/src/Components/Flipper/Flipper.core.test.ts +9 -0
- package/src/Components/Flipper/Flipper.test.ts +63 -0
- package/src/Components/Health/Health.test.ts +184 -0
- package/src/Components/HealthBar/HealthBar.test.ts +158 -0
- package/src/Components/HemisphericLight/HemisphericLight.test.ts +36 -0
- package/src/Components/KeyboardMover/KeyboardMover.test.ts +120 -0
- package/src/Components/LineOfSight/LineOfSight.core.test.ts +26 -0
- package/src/Components/LineOfSight/LineOfSight.test.ts +38 -0
- package/src/Components/Mesh/Mesh.test.ts +201 -0
- package/src/Components/MeshPrimitive/MeshPrimitive.test.ts +222 -0
- package/src/Components/MouseHole/MouseHole.test.ts +40 -0
- package/src/Components/Movement/Movement.core.test.ts +50 -0
- package/src/Components/Movement/Movement.test.ts +73 -0
- package/src/Components/Obstacle/Obstacle.core.test.ts +82 -0
- package/src/Components/Obstacle/Obstacle.test.ts +72 -0
- package/src/Components/ObstacleField/ObstacleField.test.ts +8 -0
- package/src/Components/Physics/Physics.core.test.ts +48 -0
- package/src/Components/Physics/Physics.test.ts +101 -0
- package/src/Components/PinballBuilder/PinballBuilder.test.ts +129 -0
- package/src/Components/PinballBuilderInput/PinballBuilderInput.test.ts +76 -0
- package/src/Components/PinballCamera/PinballCamera.test.ts +39 -0
- package/src/Components/PinballLayout/PinballLayout.test.ts +82 -0
- package/src/Components/PinballTable/PinballTable.test.ts +91 -0
- package/src/Components/PlayerInput/PlayerInput.core.test.ts +56 -0
- package/src/Components/PlayerInput/PlayerInput.test.ts +139 -0
- package/src/Components/Plunger/Plunger.test.ts +55 -0
- package/src/Components/Score/Score.test.ts +60 -0
- package/src/Components/Scoreboard/Scoreboard.core.test.ts +12 -0
- package/src/Components/Scoreboard/Scoreboard.test.ts +36 -0
- package/src/Components/Shadow/Shadow.test.ts +69 -0
- package/src/Components/ShooterCamera/ShooterCamera.test.ts +11 -0
- package/src/Components/SkeletonAnimator/SkeletonAnimator.test.ts +182 -0
- package/src/Components/Skybox/Skybox.test.ts +83 -0
- package/src/Components/Spinner/Spinner.test.ts +30 -0
- package/src/Components/TwinStickShooter/TwinStickShooter.test.ts +105 -0
- package/templates/main.ts +27 -8
package/dist/cli/eject.d.ts
CHANGED
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Where eject reads its source from. Defaults to this package's shipped dirs;
|
|
3
|
+
* overridable so tests can point at an isolated fixture tree instead of
|
|
4
|
+
* mutating the real package (which other suites read concurrently).
|
|
5
|
+
*/
|
|
6
|
+
export interface EjectRoots {
|
|
7
|
+
componentsSrc: string;
|
|
8
|
+
payloadSrc: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Rewrite imports in a copied file so they resolve from `src/components/Name/`:
|
|
12
|
+
*
|
|
13
|
+
* - `../Sibling/...` where Sibling was NOT ejected → the package subpath
|
|
14
|
+
* `@babylonjsmarket/arcade/Sibling`. Co-ejected siblings keep their relative
|
|
15
|
+
* path (it still resolves under src/components/).
|
|
16
|
+
* - `../../inputEvents` (arcade-core constants, two levels up to the package
|
|
17
|
+
* root in the source tree) → the package root `@babylonjsmarket/arcade`,
|
|
18
|
+
* which re-exports them. Otherwise the copied path would dangle.
|
|
19
|
+
*
|
|
20
|
+
* ecs/arcade/viz/solid/babylon package imports are left alone — they resolve
|
|
21
|
+
* from node_modules in the ejected project.
|
|
22
|
+
*/
|
|
23
|
+
export declare function rewriteImports(src: string, ejected: Set<string>, allNames: Set<string>): string;
|
|
24
|
+
export declare function eject(args: string[], roots?: EjectRoots): Promise<void>;
|
|
2
25
|
//# sourceMappingURL=eject.d.ts.map
|
package/dist/cli/eject.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../../src/cli/eject.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../../src/cli/eject.ts"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAgGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GACpB,MAAM,CAeR;AA4FD,wBAAsB,KAAK,CACzB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,GAAE,UAA0B,GAChC,OAAO,CAAC,IAAI,CAAC,CA0Df"}
|
package/dist/cli/eject.js
CHANGED
|
@@ -9,20 +9,24 @@ const { cyan, green, yellow, dim, bold } = colors;
|
|
|
9
9
|
// `src/Components` and `.claude-payload` ship in the published `files` array.
|
|
10
10
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const PACKAGE_ROOT = path.resolve(HERE, "..", "..");
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const COMPONENTS_SRC_DEFAULT = path.resolve(PACKAGE_ROOT, "src", "Components");
|
|
13
|
+
const PAYLOAD_SRC_DEFAULT = path.resolve(PACKAGE_ROOT, ".claude-payload", "skills", "babylonjsmarket");
|
|
14
|
+
const DEFAULT_ROOTS = {
|
|
15
|
+
componentsSrc: COMPONENTS_SRC_DEFAULT,
|
|
16
|
+
payloadSrc: PAYLOAD_SRC_DEFAULT,
|
|
17
|
+
};
|
|
14
18
|
/** The component universe = top-level folders under src/Components. */
|
|
15
|
-
function arcadeComponentNames() {
|
|
16
|
-
if (!fs.existsSync(
|
|
19
|
+
function arcadeComponentNames(componentsSrc) {
|
|
20
|
+
if (!fs.existsSync(componentsSrc))
|
|
17
21
|
return [];
|
|
18
22
|
return fs
|
|
19
|
-
.readdirSync(
|
|
23
|
+
.readdirSync(componentsSrc, { withFileTypes: true })
|
|
20
24
|
.filter((e) => e.isDirectory())
|
|
21
25
|
.map((e) => e.name);
|
|
22
26
|
}
|
|
23
27
|
/** Every .ts/.tsx file inside a component folder. */
|
|
24
|
-
function componentFiles(name) {
|
|
25
|
-
const dir = path.join(
|
|
28
|
+
function componentFiles(componentsSrc, name) {
|
|
29
|
+
const dir = path.join(componentsSrc, name);
|
|
26
30
|
const out = [];
|
|
27
31
|
const walk = (d) => {
|
|
28
32
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
@@ -54,9 +58,9 @@ function parseUsedComponents(projectDir, names) {
|
|
|
54
58
|
return used;
|
|
55
59
|
}
|
|
56
60
|
/** Component names a given component imports via `../Sibling/...` or lists in meta.json. */
|
|
57
|
-
function directDeps(name, names) {
|
|
61
|
+
function directDeps(componentsSrc, name, names) {
|
|
58
62
|
const deps = new Set();
|
|
59
|
-
const metaPath = path.join(
|
|
63
|
+
const metaPath = path.join(componentsSrc, name, "meta.json");
|
|
60
64
|
if (fs.existsSync(metaPath)) {
|
|
61
65
|
try {
|
|
62
66
|
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
@@ -69,7 +73,7 @@ function directDeps(name, names) {
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
const importRe = /['"]\.\.\/([A-Z][A-Za-z0-9_]*)\//g;
|
|
72
|
-
for (const file of componentFiles(name)) {
|
|
76
|
+
for (const file of componentFiles(componentsSrc, name)) {
|
|
73
77
|
const src = fs.readFileSync(file, "utf8");
|
|
74
78
|
let m;
|
|
75
79
|
while ((m = importRe.exec(src)))
|
|
@@ -79,13 +83,13 @@ function directDeps(name, names) {
|
|
|
79
83
|
return deps;
|
|
80
84
|
}
|
|
81
85
|
/** Expand a seed set to its transitive component-dependency closure. */
|
|
82
|
-
function resolveClosure(seed, allNames) {
|
|
86
|
+
function resolveClosure(componentsSrc, seed, allNames) {
|
|
83
87
|
const all = new Set(allNames);
|
|
84
88
|
const out = new Set(seed);
|
|
85
89
|
const queue = [...seed];
|
|
86
90
|
while (queue.length) {
|
|
87
91
|
const next = queue.pop();
|
|
88
|
-
for (const dep of directDeps(next, all)) {
|
|
92
|
+
for (const dep of directDeps(componentsSrc, next, all)) {
|
|
89
93
|
if (!out.has(dep)) {
|
|
90
94
|
out.add(dep);
|
|
91
95
|
queue.push(dep);
|
|
@@ -107,7 +111,7 @@ function resolveClosure(seed, allNames) {
|
|
|
107
111
|
* ecs/arcade/viz/solid/babylon package imports are left alone — they resolve
|
|
108
112
|
* from node_modules in the ejected project.
|
|
109
113
|
*/
|
|
110
|
-
function rewriteImports(src, ejected, allNames) {
|
|
114
|
+
export function rewriteImports(src, ejected, allNames) {
|
|
111
115
|
return src
|
|
112
116
|
.replace(/(['"])\.\.\/\.\.\/inputEvents\1/g, (_whole, quote) => `${quote}@babylonjsmarket/arcade${quote}`)
|
|
113
117
|
.replace(/(['"])(\.\.\/([A-Z][A-Za-z0-9_]*)\/[^'"]*)\1/g, (whole, quote, _spec, sibling) => {
|
|
@@ -159,11 +163,11 @@ function patchRegistry(projectDir, names, dryRun) {
|
|
|
159
163
|
}
|
|
160
164
|
return added.map((n) => ` patched registry: ${n}`);
|
|
161
165
|
}
|
|
162
|
-
function buildPlan(args, projectDir) {
|
|
166
|
+
function buildPlan(args, projectDir, componentsSrc) {
|
|
163
167
|
const argv = mri(args, { boolean: ["all", "dry-run"] });
|
|
164
168
|
const all = argv.all;
|
|
165
169
|
const explicit = argv._.map(String);
|
|
166
|
-
const names = arcadeComponentNames();
|
|
170
|
+
const names = arcadeComponentNames(componentsSrc);
|
|
167
171
|
if (names.length === 0) {
|
|
168
172
|
throw new Error("No component source found. Run inside a project with @babylonjsmarket/arcade installed (the package must ship src/Components).");
|
|
169
173
|
}
|
|
@@ -185,10 +189,11 @@ function buildPlan(args, projectDir) {
|
|
|
185
189
|
throw new Error("Couldn't detect any arcade components in src/scene.ts or src/registry.ts. Pass names explicitly (`arcade eject Bullet Enemy`) or use --all.");
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
|
-
const components = [...resolveClosure(seed, names)].sort();
|
|
192
|
+
const components = [...resolveClosure(componentsSrc, seed, names)].sort();
|
|
189
193
|
return { components };
|
|
190
194
|
}
|
|
191
|
-
export async function eject(args) {
|
|
195
|
+
export async function eject(args, roots = DEFAULT_ROOTS) {
|
|
196
|
+
const { componentsSrc, payloadSrc } = roots;
|
|
192
197
|
if (args.includes("-h") || args.includes("--help")) {
|
|
193
198
|
console.log(EJECT_HELP);
|
|
194
199
|
return;
|
|
@@ -197,7 +202,7 @@ export async function eject(args) {
|
|
|
197
202
|
const projectDir = process.cwd();
|
|
198
203
|
let plan;
|
|
199
204
|
try {
|
|
200
|
-
plan = buildPlan(args, projectDir);
|
|
205
|
+
plan = buildPlan(args, projectDir, componentsSrc);
|
|
201
206
|
}
|
|
202
207
|
catch (e) {
|
|
203
208
|
console.error(yellow(e.message));
|
|
@@ -205,14 +210,14 @@ export async function eject(args) {
|
|
|
205
210
|
return;
|
|
206
211
|
}
|
|
207
212
|
const ejectedSet = new Set(plan.components);
|
|
208
|
-
const allNames = new Set(arcadeComponentNames());
|
|
213
|
+
const allNames = new Set(arcadeComponentNames(componentsSrc));
|
|
209
214
|
const componentsDir = path.join(projectDir, "src", "components");
|
|
210
215
|
console.log(bold(dryRun ? "\nEject plan (dry run — nothing written):" : "\nEjecting:"));
|
|
211
216
|
console.log(` components: ${cyan(plan.components.join(", "))}`);
|
|
212
217
|
// 1. Copy each component folder (with its own tests).
|
|
213
218
|
for (const name of plan.components) {
|
|
214
219
|
if (!dryRun)
|
|
215
|
-
copyDir(path.join(
|
|
220
|
+
copyDir(path.join(componentsSrc, name), path.join(componentsDir, name));
|
|
216
221
|
console.log(green(` + src/components/${name}/`));
|
|
217
222
|
}
|
|
218
223
|
// 2. Rewrite imports across everything copied: a `../X/` whose X was NOT
|
|
@@ -227,9 +232,9 @@ export async function eject(args) {
|
|
|
227
232
|
}
|
|
228
233
|
// 4. Drop the babylonjsmarket Claude skill into the project.
|
|
229
234
|
const skillDest = path.join(projectDir, ".claude", "skills", "babylonjsmarket");
|
|
230
|
-
if (fs.existsSync(
|
|
235
|
+
if (fs.existsSync(payloadSrc)) {
|
|
231
236
|
if (!dryRun)
|
|
232
|
-
copyDir(
|
|
237
|
+
copyDir(payloadSrc, skillDest);
|
|
233
238
|
console.log(green(" + .claude/skills/babylonjsmarket/"));
|
|
234
239
|
}
|
|
235
240
|
else {
|
package/dist/cli/eject.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eject.js","sourceRoot":"","sources":["../../src/cli/eject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,MAAM,MAAM,YAAY,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;AAElD,+EAA+E;AAC/E,8EAA8E;AAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACpD,MAAM,
|
|
1
|
+
{"version":3,"file":"eject.js","sourceRoot":"","sources":["../../src/cli/eject.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babylonjsmarket/arcade",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Reusable arcade-style components (mesh, camera, lights, input, physics, scoring, animation) for the @babylonjsmarket/ecs framework",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"build": "tsc -p tsconfig.build.json",
|
|
80
80
|
"build:watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
81
81
|
"test": "vitest run",
|
|
82
|
+
"test:coverage": "vitest run --coverage",
|
|
82
83
|
"test:watch": "vitest",
|
|
83
84
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
84
85
|
"lint": "eslint src",
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
"sync-templates:check": "node scripts/sync-templates.js --check",
|
|
87
88
|
"try:scaffold": "node scripts/try-scaffold.mjs",
|
|
88
89
|
"validate:scenes": "tsx scripts/validate-example-scenes.ts",
|
|
89
|
-
"prepublishOnly": "npm run build && npm run test"
|
|
90
|
+
"prepublishOnly": "npm run build && npm run test:coverage"
|
|
90
91
|
},
|
|
91
92
|
"dependencies": {
|
|
92
93
|
"@babylonjsmarket/ecs": "^0.5.3",
|
|
@@ -137,6 +137,17 @@ describe('Animation core', () => {
|
|
|
137
137
|
}).not.toThrow();
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it('pins walk (no run band) when runSpeed <= walkSpeed and speed exceeds walk', () => {
|
|
141
|
+
// Degenerate config: runSpeed equals walkSpeed → no meaningful run band.
|
|
142
|
+
// A speed above walkSpeed must settle on pure walk, never run.
|
|
143
|
+
const a = createAnimation({ walkSpeed: 3, runSpeed: 3, blendSpeed: 50 });
|
|
144
|
+
for (let i = 0; i < 400; i++) a.update(5, 1 / 60);
|
|
145
|
+
const out = a.update(5, 1 / 60);
|
|
146
|
+
expect(out.walkWeight).toBeCloseTo(1, 3);
|
|
147
|
+
expect(out.runWeight).toBeCloseTo(0, 3);
|
|
148
|
+
expect(out.state).toBe('walk');
|
|
149
|
+
});
|
|
150
|
+
|
|
140
151
|
it('treats negative dt as zero (never unwinds weights)', () => {
|
|
141
152
|
const a = createAnimation();
|
|
142
153
|
const before = a.getState();
|
|
@@ -171,4 +171,30 @@ describe('AnimationSystem', () => {
|
|
|
171
171
|
const playCalls = renderer.calls.filter((c) => c.method === 'playAnimation');
|
|
172
172
|
expect(playCalls).toHaveLength(0);
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
it('builds instances for entities that exist before the system initializes', () => {
|
|
176
|
+
const bus = new EventBus();
|
|
177
|
+
const r = new MockRendererAdapter();
|
|
178
|
+
const w = new World({ eventBus: bus, renderer: r, detectRaces: false });
|
|
179
|
+
const entity = w.createEntity();
|
|
180
|
+
entity.add(new AnimationComponent({ walkSpeed: 2 }));
|
|
181
|
+
// System added AFTER the entity → onInitialize scans and wires the instance.
|
|
182
|
+
w.addSystem(new AnimationSystem(bus));
|
|
183
|
+
w.initialize();
|
|
184
|
+
const comp = entity.get(AnimationComponent);
|
|
185
|
+
expect(comp?.instance).not.toBeNull();
|
|
186
|
+
expect(comp?.instance?.getParams().walkSpeed).toBe(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('does not rebuild the core instance for an entity already wired up', () => {
|
|
190
|
+
const entity = world.createEntity();
|
|
191
|
+
entity.add(new AnimationComponent());
|
|
192
|
+
const comp = entity.get(AnimationComponent)!;
|
|
193
|
+
const firstInstance = comp.instance;
|
|
194
|
+
expect(firstInstance).not.toBeNull();
|
|
195
|
+
// A second update (and the internal ensureInstance guard) must not replace
|
|
196
|
+
// the existing instance.
|
|
197
|
+
world.update(1 / 60);
|
|
198
|
+
expect(comp.instance).toBe(firstInstance);
|
|
199
|
+
});
|
|
174
200
|
});
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
ArcCameraSystem,
|
|
5
5
|
ArcCameraEvents,
|
|
6
6
|
} from './ArcCamera';
|
|
7
|
+
import { MeshPrimitiveSystem, MeshPrimitiveComponent } from '../MeshPrimitive/MeshPrimitive';
|
|
8
|
+
import { getArcCameraRuntime } from './ArcCamera.core';
|
|
7
9
|
import { EventBus, World } from '@babylonjsmarket/ecs';
|
|
8
10
|
import { MockRendererAdapter } from '@babylonjsmarket/ecs';
|
|
11
|
+
import type { MeshHandle } from '@babylonjsmarket/ecs/renderer-types';
|
|
9
12
|
|
|
10
13
|
describe('ArcCameraComponent (core)', () => {
|
|
11
14
|
it('applies sensible defaults', () => {
|
|
@@ -184,4 +187,169 @@ describe('ArcCameraSystem — adapter integration', () => {
|
|
|
184
187
|
world.update(4);
|
|
185
188
|
expect(renderer.calls.some((c) => c.method === 'nudgeCameraAlpha')).toBe(false);
|
|
186
189
|
});
|
|
190
|
+
|
|
191
|
+
it('locks radius each frame when controls are disabled', () => {
|
|
192
|
+
const entity = world.createEntity();
|
|
193
|
+
entity.add(new ArcCameraComponent({ controlsEnabled: false, distance: 30 }));
|
|
194
|
+
renderer.calls.length = 0;
|
|
195
|
+
world.update(0.016);
|
|
196
|
+
const radius = renderer.calls.find((c) => c.method === 'setCameraRadius');
|
|
197
|
+
expect(radius).toBeDefined();
|
|
198
|
+
expect(radius!.args[1]).toBe(30);
|
|
199
|
+
const limits = renderer.calls.find((c) => c.method === 'setCameraRadiusLimits');
|
|
200
|
+
expect(limits).toBeDefined();
|
|
201
|
+
expect(limits!.args[1]).toBe(30);
|
|
202
|
+
expect(limits!.args[2]).toBe(30);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('does not lock the radius for a free-orbit camera (controls on)', () => {
|
|
206
|
+
const entity = world.createEntity();
|
|
207
|
+
entity.add(new ArcCameraComponent({ controlsEnabled: true }));
|
|
208
|
+
renderer.calls.length = 0;
|
|
209
|
+
world.update(0.016);
|
|
210
|
+
expect(renderer.calls.some((c) => c.method === 'setCameraRadius')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('tracks an active target by reading its mesh world position each frame', () => {
|
|
214
|
+
const entity = world.createEntity();
|
|
215
|
+
entity.add(new ArcCameraComponent({ target: 'hero', follow: true }));
|
|
216
|
+
const fakeMeshHandle = { __mockHandle: 'mesh:hero' } as unknown as MeshHandle;
|
|
217
|
+
renderer.meshWorldPositions.set(fakeMeshHandle, [10, 2, 6]);
|
|
218
|
+
eventBus.emit('meshprimitive.created', { entityId: 'hero', handle: fakeMeshHandle });
|
|
219
|
+
renderer.calls.length = 0;
|
|
220
|
+
world.update(0.016);
|
|
221
|
+
const setTarget = renderer.calls.find((c) => c.method === 'setCameraTarget');
|
|
222
|
+
expect(setTarget).toBeDefined();
|
|
223
|
+
// base + targetOffset(0,0,0)
|
|
224
|
+
expect(setTarget!.args[1]).toBeCloseTo(10);
|
|
225
|
+
expect(setTarget!.args[2]).toBeCloseTo(2);
|
|
226
|
+
expect(setTarget!.args[3]).toBeCloseTo(6);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('holds a captured free base when a target is named but its mesh is not ready', () => {
|
|
230
|
+
const entity = world.createEntity();
|
|
231
|
+
// target is named but no meshprimitive.created ever fires for it, so the
|
|
232
|
+
// System captures the current look point (origin, the seeded target) as the
|
|
233
|
+
// free base and pans it by targetOffset.
|
|
234
|
+
const comp = new ArcCameraComponent({ target: 'ghost', follow: true, targetOffset: [1, 0, 0] });
|
|
235
|
+
entity.add(comp);
|
|
236
|
+
renderer.calls.length = 0;
|
|
237
|
+
world.update(0.016);
|
|
238
|
+
const setTarget = renderer.calls.find((c) => c.method === 'setCameraTarget');
|
|
239
|
+
expect(setTarget).toBeDefined();
|
|
240
|
+
// base (captured origin) + offset [1,0,0].
|
|
241
|
+
expect(setTarget!.args[1]).toBeCloseTo(1);
|
|
242
|
+
expect(setTarget!.args[2]).toBeCloseTo(0);
|
|
243
|
+
expect(setTarget!.args[3]).toBeCloseTo(0);
|
|
244
|
+
|
|
245
|
+
// The free base is cached on the runtime so panning is relative to it.
|
|
246
|
+
const rt = getArcCameraRuntime(comp);
|
|
247
|
+
expect(rt.freeBase).toEqual([0, 0, 0]);
|
|
248
|
+
|
|
249
|
+
// Second frame still drives the target from the same cached base.
|
|
250
|
+
renderer.calls.length = 0;
|
|
251
|
+
world.update(0.016);
|
|
252
|
+
expect(renderer.calls.some((c) => c.method === 'setCameraTarget')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('resets the idle timer the frame the camera receives input while auto-rotating', () => {
|
|
256
|
+
const entity = world.createEntity();
|
|
257
|
+
const comp = new ArcCameraComponent({ autoRotate: true, autoRotateSpeed: 1 });
|
|
258
|
+
entity.add(comp);
|
|
259
|
+
// Let it idle past the threshold so it begins nudging.
|
|
260
|
+
world.update(4);
|
|
261
|
+
expect(renderer.calls.some((c) => c.method === 'nudgeCameraAlpha')).toBe(true);
|
|
262
|
+
// Now simulate user input: bump the reported alpha so onUpdate detects
|
|
263
|
+
// rotation, which must reset idleTime to 0 and suppress nudging this frame.
|
|
264
|
+
const angles = [...renderer.cameraAngles.values()][0];
|
|
265
|
+
if (angles) angles.alpha += 1;
|
|
266
|
+
renderer.calls.length = 0;
|
|
267
|
+
world.update(0.016);
|
|
268
|
+
expect(renderer.calls.some((c) => c.method === 'nudgeCameraAlpha')).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('seeds the camera target from a target mesh that already exists at creation', () => {
|
|
272
|
+
// Build a fresh world where the target mesh is created BEFORE the
|
|
273
|
+
// ArcCamera entity, so ensureCamera resolves the handle and seeds from it.
|
|
274
|
+
const bus = new EventBus();
|
|
275
|
+
const r = new MockRendererAdapter();
|
|
276
|
+
const w = new World({ eventBus: bus, renderer: r });
|
|
277
|
+
w.addSystem(new MeshPrimitiveSystem(bus));
|
|
278
|
+
w.addSystem(new ArcCameraSystem(bus));
|
|
279
|
+
w.initialize();
|
|
280
|
+
|
|
281
|
+
const hero = w.createEntity('hero');
|
|
282
|
+
hero.add(new MeshPrimitiveComponent());
|
|
283
|
+
const mp = hero.get(MeshPrimitiveComponent)!;
|
|
284
|
+
r.meshWorldPositions.set(mp.handle as MeshHandle, [5, 1, 3]);
|
|
285
|
+
|
|
286
|
+
const cam = w.createEntity('Cam');
|
|
287
|
+
cam.add(new ArcCameraComponent({ target: 'hero', follow: true, targetOffset: [0, 2, 0] }));
|
|
288
|
+
|
|
289
|
+
const create = r.calls.find((c) => c.method === 'createArcCamera');
|
|
290
|
+
expect(create).toBeDefined();
|
|
291
|
+
const spec = create!.args[1] as { target: [number, number, number] };
|
|
292
|
+
expect(spec.target[0]).toBeCloseTo(5);
|
|
293
|
+
expect(spec.target[1]).toBeCloseTo(3); // 1 + offset 2
|
|
294
|
+
expect(spec.target[2]).toBeCloseTo(3);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('no-ops on a renderer-less world', () => {
|
|
298
|
+
const bus = new EventBus();
|
|
299
|
+
const w = new World({ eventBus: bus });
|
|
300
|
+
w.addSystem(new ArcCameraSystem(bus));
|
|
301
|
+
w.initialize();
|
|
302
|
+
const e = w.createEntity();
|
|
303
|
+
expect(() => e.add(new ArcCameraComponent())).not.toThrow();
|
|
304
|
+
expect(() => w.update(0.016)).not.toThrow();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('creates cameras for entities that exist before the system initializes', () => {
|
|
308
|
+
const bus = new EventBus();
|
|
309
|
+
const r = new MockRendererAdapter();
|
|
310
|
+
const w = new World({ eventBus: bus, renderer: r });
|
|
311
|
+
const entity = w.createEntity();
|
|
312
|
+
entity.add(new ArcCameraComponent({ distance: 22 }));
|
|
313
|
+
// System added AFTER the entity → onInitialize scans and creates it.
|
|
314
|
+
w.addSystem(new ArcCameraSystem(bus));
|
|
315
|
+
w.initialize();
|
|
316
|
+
const create = r.calls.find((c) => c.method === 'createArcCamera');
|
|
317
|
+
expect(create).toBeDefined();
|
|
318
|
+
expect((create!.args[1] as { radius: number }).radius).toBe(22);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('centres restTarget when not following', () => {
|
|
322
|
+
const entity = world.createEntity();
|
|
323
|
+
entity.add(new ArcCameraComponent({ follow: false, restTarget: 'table' }));
|
|
324
|
+
const fakeMeshHandle = { __mockHandle: 'mesh:table' } as unknown as MeshHandle;
|
|
325
|
+
renderer.meshWorldPositions.set(fakeMeshHandle, [0, 0, 9]);
|
|
326
|
+
eventBus.emit('meshprimitive.created', { entityId: 'table', handle: fakeMeshHandle });
|
|
327
|
+
renderer.calls.length = 0;
|
|
328
|
+
world.update(0.016);
|
|
329
|
+
const setTarget = renderer.calls.find((c) => c.method === 'setCameraTarget');
|
|
330
|
+
expect(setTarget).toBeDefined();
|
|
331
|
+
expect(setTarget!.args[3]).toBeCloseTo(9);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('seeds from `target` when not following and no restTarget is set', () => {
|
|
335
|
+
// follow:false with no restTarget → seedName falls back to `target`.
|
|
336
|
+
const bus = new EventBus();
|
|
337
|
+
const r = new MockRendererAdapter();
|
|
338
|
+
const w = new World({ eventBus: bus, renderer: r });
|
|
339
|
+
w.addSystem(new MeshPrimitiveSystem(bus));
|
|
340
|
+
w.addSystem(new ArcCameraSystem(bus));
|
|
341
|
+
w.initialize();
|
|
342
|
+
|
|
343
|
+
const hero = w.createEntity('hero');
|
|
344
|
+
hero.add(new MeshPrimitiveComponent());
|
|
345
|
+
const mp = hero.get(MeshPrimitiveComponent)!;
|
|
346
|
+
r.meshWorldPositions.set(mp.handle as MeshHandle, [7, 0, 0]);
|
|
347
|
+
|
|
348
|
+
const cam = w.createEntity('Cam');
|
|
349
|
+
cam.add(new ArcCameraComponent({ target: 'hero', follow: false }));
|
|
350
|
+
|
|
351
|
+
const create = r.calls.find((c) => c.method === 'createArcCamera');
|
|
352
|
+
const spec = create!.args[1] as { target: [number, number, number] };
|
|
353
|
+
expect(spec.target[0]).toBeCloseTo(7);
|
|
354
|
+
});
|
|
187
355
|
});
|