import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { createRainGearState } from '../src/controls/rainGear.ts'; import { createEmoteState } from '../src/controls/emotes.ts'; import { createSheep } from '../src/scene/sheep.ts'; import { createFlashlight } from '../src/scene/flashlight.ts'; import { FLASHLIGHT } from '../src/constants/flashlight.ts'; import { Vector3 } from 'three'; test('authored flashlight stays in the left grip and uses lumen power only when switched on', async () => { const [model, umbrella, raincoat, torch] = await Promise.all(['red-sheep', 'umbrella', 'raincoat', 'flashlight'].map(asset)); const sheep = createSheep(model, { umbrella, raincoat, flashlight: torch }); const beam = createFlashlight(sheep.group); assert.equal(torch.parent, model.getObjectByName('ArmLeft')); assert.equal(torch.visible, false); assert.equal(beam.light.visible, false); assert.equal(beam.light.intensity, FLASHLIGHT.lumens / (2 * Math.PI * (1 - Math.cos(FLASHLIGHT.halfAngle)))); sheep.setEquipment(true, false, false, true); sheep.pose('idle', 0, true, 1); beam.update(true); assert.equal(torch.visible, true); const direction = new Vector3().subVectors(beam.light.target.position, beam.light.position).normalize(); assert.ok(direction.z > .9 && direction.y < 0, 'beam points forward and slightly down'); const before = beam.light.position.clone(); sheep.group.position.x += 3; beam.update(true); assert.ok(Math.abs(beam.light.position.x-before.x-3) < .001); sheep.setEquipment(false, false, false, false); beam.update(false); assert.equal(torch.visible, false); assert.equal(beam.light.visible, false); beam.dispose(); sheep.dispose(); }); test('rain equipment is independent of emotes, remains in snow, and clears in dry weather', () => { const gear = createRainGearState(); const emote = createEmoteState(); assert.equal(gear.toggle('umbrella'), false); gear.updateWeather(1, 0); assert.equal(gear.toggle('hood'), false, 'hood requires the raincoat'); assert.ok(gear.toggle('umbrella')); assert.ok(gear.toggle('raincoat')); assert.ok(gear.toggle('hood')); assert.ok(gear.hood); emote.start('wave', false); emote.update(4, true, false); assert.equal(emote.current, 'idle'); assert.ok(gear.umbrella && gear.raincoat); emote.start('pray', true); gear.updateWeather(0, .3); assert.ok(gear.umbrella && gear.raincoat); gear.toggle('umbrella'); assert.ok(!gear.umbrella && gear.raincoat); gear.toggle('raincoat'); assert.equal(gear.hood, false); gear.updateWeather(0, 0); assert.ok(!gear.available && !gear.umbrella && !gear.raincoat); }); async function asset(name: string) { const bytes = await readFile(new URL(`../public/characters/${name}.glb`, import.meta.url)); return (await new GLTFLoader().parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), '')).scene; } test('Blender gear follows the sheep joints and keeps the umbrella grip through walking, waving and prayer', async () => { const [model, umbrella, raincoat] = await Promise.all(['red-sheep', 'umbrella', 'raincoat'].map(asset)); const sheep = createSheep(model, { umbrella, raincoat }); const right = model.getObjectByName('ArmRight')!; const left = model.getObjectByName('ArmLeft')!; const sleeve = model.getObjectByName('CoatArmRight')!; const hood = model.getObjectByName('CoatHood')!; const folded = model.getObjectByName('CoatFoldedHood')!; assert.equal(hood.parent, model.getObjectByName('Head')); assert.equal(sleeve.parent, right); assert.equal(umbrella.visible, false); sheep.setEquipment(true, true); assert.ok(!hood.visible && folded.visible); sheep.setEquipment(true, true, true); assert.ok(hood.visible && !folded.visible); sheep.pose('idle', 0, true, 1); const grip = right.quaternion.clone(); const leftWalking = left.quaternion.clone(); sheep.pose('wave', .2, false, 1); assert.ok(right.quaternion.angleTo(grip) < 1e-6); assert.ok(left.quaternion.angleTo(leftWalking) > .5); sheep.pose('pray', .2, false, 1); assert.ok(right.quaternion.angleTo(grip) < 1e-6); assert.ok(umbrella.visible && raincoat.visible && sleeve.visible); sheep.setEquipment(false, false); assert.ok(!umbrella.visible && !raincoat.visible && !sleeve.visible); assert.equal(hood.visible, false); sheep.dispose(); });