Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
95 lines
4.4 KiB
TypeScript
95 lines
4.4 KiB
TypeScript
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();
|
|
});
|