mirror of
https://github.com/samvallad33/vestige.git
synced 2026-04-27 01:36:22 +02:00
feat(v2.3-terrarium): Memory Birth Ritual + event pipeline fix
v2.3 "Terrarium" headline feature. When a MemoryCreated event arrives, a
glowing orb materialises in the cosmic center (camera-relative z=-40),
gestates for ~800ms growing from a tiny spark into a full orb, then arcs
along a dynamic quadratic Bezier curve to the live position of the real
node, and on arrival hands off to the existing RainbowBurst + Shockwave +
RippleWave cascade. The target position is re-resolved every frame so
the force simulation can move the destination during flight without the
orb losing its mark.
**New primitive — EffectManager.createBirthOrb()** (effects.ts):
Accepts a camera, a color, a live target-position getter, and an
arrival callback. Owns a sprite pair (outer halo + inner bright core),
both depthTest:false with renderOrder 999/1000 so the orb is always
visible through the starfield and the graph.
- Gestation phase: easeOutCubic growth + sinusoidal pulse, halo tints
from neutral to event color as the ritual charges.
- Flight phase: QuadraticBezierCurve3 with control point at midpoint
raised on Y by 30 + 15% of orb-to-target distance (shooting-star
arc). Sampled with easeInOutQuad. Orb shrinks ~35% approaching target.
- Arrival: fires onArrive callback once, then fades out over 8 frames
while expanding slightly (energy dispersal).
- Caller's onArrive triggers the burst cascade at arrivePos (NOT the
original spawnPos — the force sim may have moved the target during
the ritual, so we re-read nodeManager.positions on arrival).
- Dispose path integrated with existing EffectManager.dispose().
**Event pipeline fix — Graph3D.processEvents()**:
Previously tracked `processedEventCount` assuming APPEND order, but
websocket.ts PREPENDS new events (index 0) and caps the array at
MAX_EVENTS. Result: only the first MemoryCreated event after page
load fired correctly; subsequent ones reprocessed the oldest entry.
Fixed to walk from index 0 until hitting the last-processed event
by reference identity — correct regardless of array direction or
eviction pressure. Events are then processed oldest-first so causes
precede effects. Found while wiring the v2.3 demo button; would have
manifested as "first orb only" in production.
**Demo trigger** (Settings -> Birth Ritual Preview):
Button that calls websocket.injectEvent() with a synthetic
MemoryCreated event, cycling through node types (fact / concept /
pattern / decision / person / place) to showcase the type-color
mapping. Downstream consumers can't distinguish synthetic from real,
so this drives the full ritual end-to-end. Intended for demo clip
recording for the Wednesday launch.
**Test coverage:**
- events.test.ts now tests the v2.3 birth ritual path: spawns 2+
sprites in the scene immediately, and fires the full arrival
cascade after driving the effects.update() loop past the ritual
duration.
- three-mock.ts extended with Vector3.addVectors, Vector3.applyQuaternion,
Color.multiplyScalar, Quaternion, QuadraticBezierCurve3, Texture,
and Object3D.quaternion/renderOrder so production code runs unaltered
in tests.
Build + typecheck:
- npm run check: 0 errors, 0 warnings across 583 files
- npm test: 251/251 pass (net +0 from v2.2)
- npm run build: clean adapter-static output
The Sanhedrin Shatter (anti-birth ritual for hallucination veto) needs
server-side event plumbing and is deferred. Ship this as the Wednesday
visual mic-drop.
This commit is contained in:
parent
f01375b815
commit
f40aa2e086
8 changed files with 393 additions and 29 deletions
|
|
@ -155,7 +155,7 @@ describe('Event-to-Mutation Pipeline', () => {
|
|||
expect(distToN1).toBeLessThan(20);
|
||||
});
|
||||
|
||||
it('triggers rainbow burst effect', () => {
|
||||
it('spawns a v2.3 birth orb in the scene', () => {
|
||||
const childrenBefore = scene.children.length;
|
||||
|
||||
mapEventToEffects(
|
||||
|
|
@ -168,16 +168,19 @@ describe('Event-to-Mutation Pipeline', () => {
|
|||
allNodes
|
||||
);
|
||||
|
||||
// Scene should have new particles (rainbow burst + shockwave + possibly more)
|
||||
expect(scene.children.length).toBeGreaterThan(childrenBefore);
|
||||
// Birth orb adds a halo sprite + bright core sprite to the scene
|
||||
// immediately. The arrival-cascade effects (rainbow burst, shockwaves,
|
||||
// ripple wave) are deferred to the orb's onArrive callback — covered
|
||||
// by the "fires arrival cascade after ritual" test below.
|
||||
expect(scene.children.length).toBeGreaterThanOrEqual(childrenBefore + 2);
|
||||
});
|
||||
|
||||
it('triggers double shockwave (second delayed)', () => {
|
||||
it('fires the arrival cascade after the birth ritual completes', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
mapEventToEffects(
|
||||
makeEvent('MemoryCreated', {
|
||||
id: 'double-shock',
|
||||
id: 'cascade-check',
|
||||
content: 'test',
|
||||
node_type: 'fact',
|
||||
}),
|
||||
|
|
@ -185,13 +188,23 @@ describe('Event-to-Mutation Pipeline', () => {
|
|||
allNodes
|
||||
);
|
||||
|
||||
const initialChildren = scene.children.length;
|
||||
const afterSpawn = scene.children.length;
|
||||
|
||||
// Advance past the setTimeout
|
||||
vi.advanceTimersByTime(200);
|
||||
// Drive the effects update loop past the full ritual duration
|
||||
// (gestation 48 + flight 90 = 138 frames). Each tick is one frame;
|
||||
// we run 150 to give onArrive room to fire.
|
||||
for (let i = 0; i < 150; i++) {
|
||||
effects.update(nodeManager.meshMap, camera, nodeManager.positions);
|
||||
}
|
||||
|
||||
// Second shockwave should have been added
|
||||
expect(scene.children.length).toBeGreaterThan(initialChildren);
|
||||
// Advance the setTimeout that schedules the delayed second shockwave.
|
||||
vi.advanceTimersByTime(250);
|
||||
|
||||
// The arrival cascade should have added a rainbow burst, shockwave,
|
||||
// ripple wave, and delayed second shockwave to the scene. Even after
|
||||
// the orb fades out and is removed, the burst particles persist long
|
||||
// enough that children.length should exceed the post-spawn count.
|
||||
expect(scene.children.length).toBeGreaterThan(afterSpawn);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,6 +93,52 @@ export class Vector3 {
|
|||
this.z = s;
|
||||
return this;
|
||||
}
|
||||
|
||||
addVectors(a: Vector3, b: Vector3) {
|
||||
this.x = a.x + b.x;
|
||||
this.y = a.y + b.y;
|
||||
this.z = a.z + b.z;
|
||||
return this;
|
||||
}
|
||||
|
||||
applyQuaternion(_q: Quaternion) {
|
||||
// Mock: identity transform. Tests don't care about actual
|
||||
// camera-relative positioning; production uses real THREE math.
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Quaternion {
|
||||
x = 0;
|
||||
y = 0;
|
||||
z = 0;
|
||||
w = 1;
|
||||
}
|
||||
|
||||
export class QuadraticBezierCurve3 {
|
||||
v0: Vector3;
|
||||
v1: Vector3;
|
||||
v2: Vector3;
|
||||
constructor(v0: Vector3, v1: Vector3, v2: Vector3) {
|
||||
this.v0 = v0;
|
||||
this.v1 = v1;
|
||||
this.v2 = v2;
|
||||
}
|
||||
getPoint(t: number): Vector3 {
|
||||
// Standard quadratic Bezier evaluation, faithful enough for tests
|
||||
// that only care that points land on the curve.
|
||||
const one = 1 - t;
|
||||
return new Vector3(
|
||||
one * one * this.v0.x + 2 * one * t * this.v1.x + t * t * this.v2.x,
|
||||
one * one * this.v0.y + 2 * one * t * this.v1.y + t * t * this.v2.y,
|
||||
one * one * this.v0.z + 2 * one * t * this.v1.z + t * t * this.v2.z
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Texture {
|
||||
needsUpdate = false;
|
||||
dispose() {}
|
||||
}
|
||||
|
||||
export class Vector2 {
|
||||
|
|
@ -157,6 +203,13 @@ export class Color {
|
|||
offsetHSL(_h: number, _s: number, _l: number) {
|
||||
return this;
|
||||
}
|
||||
|
||||
multiplyScalar(s: number) {
|
||||
this.r *= s;
|
||||
this.g *= s;
|
||||
this.b *= s;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class BufferAttribute {
|
||||
|
|
@ -329,6 +382,8 @@ export class SpriteMaterial extends BaseMaterial {
|
|||
export class Object3D {
|
||||
position = new Vector3();
|
||||
scale = new Vector3(1, 1, 1);
|
||||
quaternion = new Quaternion();
|
||||
renderOrder = 0;
|
||||
userData: Record<string, unknown> = {};
|
||||
children: Object3D[] = [];
|
||||
parent: Object3D | null = null;
|
||||
|
|
@ -428,6 +483,9 @@ export function installThreeMock() {
|
|||
Vector3,
|
||||
Vector2,
|
||||
Color,
|
||||
Quaternion,
|
||||
QuadraticBezierCurve3,
|
||||
Texture,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
SphereGeometry,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue