Files
lhns--steam-frame-nix/modules/pet/tests/sim.js
T
Pierre Kisters 4034fd4ded VR pet: a cat or dog in SteamVR's scene (steamFrame.pet)
A SteamVR dashboard patch (vr-pet) drawing a baked 3D pet next to the
windows: the Toon Cat in five coats, a Shiba Inu, a Fox and a Dachshund,
plus models from steamFrame.pet.extraModels. It walks around, follows,
sits, lies and sleeps, can be picked up by its grip bar, petted and
switched in its menu; its spot, pose and model are saved state.

Also the vr-pet command, "Pet" in the "+" menu with the current model's
icon (steam-frame-nix-pet-icon), the flake outputs pet-models,
pet-icons and pet-preview, and the check pet. A built-in model with
"private": true fails evaluation; private models belong in extraModels.
2026-10-01 04:33:39 +02:00

98 lines
4.1 KiB
JavaScript

// The scripted world of the core's tests (core.test.js, species.test.js):
// the user at (0, 1.6) looking toward -z, a right hand moved by a script,
// deterministic randomness, every clip shown and prefetched recorded.
'use strict';
const path = require('path');
const Core = require(path.join(__dirname, '..', 'core.js'));
const HZ = 24, DT = 1 / HZ;
// Deterministic randomness (mulberry32); SEED_OFFSET=n: other random runs.
function seed(s) {
s += +process.env.SEED_OFFSET || 0;
Math.random = () => {
s |= 0; s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// A core on `baked` in the scripted world. setScript(f): the right hand
// follows f(t, api) -> { x, y, z, press, carried } | null (velocity by finite
// differences, like a tracked controller's). press >= 0.5: the laser drags
// the pet by the scruff (world.held(), as systemui.js' drag), the grab point
// the hand's position; with `carried` ({ p, q }: where the adapter's
// compositor has it, systemui.js' root carried by the hand)
// world.heldPose() reports that. A shown clip or frame that is not baked
// throws.
function sim(baked, seedValue, { opts = {}, saved } = {}) {
seed(seedValue);
const head = { x: 0, y: 1.6, z: 1.6, fx: 0, fz: -1 };
let script = () => null, t = 0, prev = null, hand = null, held = null, carried = null;
const shown = [], asked = new Set(), logs = [];
const world = {
head: () => head,
hands: () => ({ left: null, right: hand }),
held: () => held,
heldPose: () => (carried ? { x: carried.p[0], y: carried.p[1], z: carried.p[2], q: carried.q } : null),
show: (clip, frame, at) => {
if (!baked.clips[clip] || frame < 0 || frame >= baked.clips[clip].frames) throw new Error(`shown ${clip} ${frame}: not baked`);
shown.push({ t, clip, frame, ...at });
},
prefetch: (clips) => { for (const c of clips) asked.add(c); },
ready: () => true,
load: () => saved, save: () => {},
log: (...a) => logs.push(`${t.toFixed(2)} ${a.join(' ')}`),
};
const cat = Core.create(world, opts, baked);
const api = {
cat, shown, asked, logs,
get t() { return t; },
setScript(f) { script = f; prev = null; },
run(seconds, check) {
for (let end = t + seconds - 1e-9; t < end;) {
t += DT;
const p = script(t, api);
hand = p ? { x: p.x, y: p.y, z: p.z,
vx: prev ? (p.x - prev.x) / DT : 0, vy: prev ? (p.y - prev.y) / DT : 0, vz: prev ? (p.z - prev.z) / DT : 0 } : null;
prev = p;
carried = p?.press >= 0.5 && p.carried ? p.carried : (p?.carried && held ? p.carried : null); // (read on the release tick too)
held = p?.press >= 0.5 ? (p.carried ? { source: 'laser', hand: 'right' } : { source: 'laser', hand: 'right', ...hand }) : null;
cat.tick(DT);
check?.(cat.state(), t);
}
return cat.state();
},
zone: (n) => cat.zone(n),
clipsShown: () => new Set(shown.map((s) => s.clip)),
// A hand moving toward `target()` at most `speed` m/s (no teleports).
toward(target, speed = 0.3, press = 0) {
let at = null;
return () => {
const g = target();
if (!at) at = { ...g };
const d = Math.hypot(g.x - at.x, g.y - at.y, g.z - at.z), k = d > speed * DT ? speed * DT / d : 1;
at = { x: at.x + (g.x - at.x) * k, y: at.y + (g.y - at.y) * k, z: at.z + (g.z - at.z) * k };
return { ...at, press: typeof press === 'function' ? press() : press };
};
},
};
cat.tick(DT); // placed in front of the user
cat.command('stand'); // calm start: rests 20 s
return api;
}
// A synchronous test runner: test(name, f), then done().
let failed = 0;
function test(name, f) {
try { f(); console.log(`ok ${name}`); } catch (e) { failed++; console.log(`FAIL ${name}: ${e.stack}`); }
}
function assert(c, msg) { if (!c) throw new Error(msg); }
const near = (a, b, tol) => Math.abs(a - b) <= tol;
function done() {
if (failed) { console.log(`${failed} failed`); process.exit(1); }
console.log('all passed');
}
module.exports = { Core, DT, seed, sim, test, assert, near, done };