Improve character controls and add idle breathing

This commit is contained in:
2026-11-02 11:56:28 +09:00
parent 0d58170211
commit 21b0bb7e98
15 changed files with 458 additions and 65 deletions
+30 -1
View File
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Group } from 'three';
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three';
import { createServerClock } from '../src/multiplayer/clock.ts';
import { createRemotePlayers } from '../src/multiplayer/remotePlayers.ts';
import type { Sheep } from '../src/scene/sheep.ts';
@@ -16,6 +16,35 @@ test('server clock uses round trip midpoint and monotonic elapsed time', () => {
assert.equal(clock.now(), 1001020);
});
test('companion selection picks visible players and chooses the nearest reachable unoccupied side', () => {
const scene = new Group();
const geometry = new BoxGeometry(1, 2, 1), material = new MeshBasicMaterial();
const template = { clone() {
const group = new Group(); group.add(new Mesh(geometry, material));
return { group, setEquipment() {}, pose: () => false, dispose() { group.removeFromParent(); } } as unknown as Sheep;
} } as Sheep;
const remote = createRemotePlayers(template, scene, () => 0);
const player: PlayerState = { id: 'friend', nickname: '친구', x: 0, z: 0, yaw: 0, moving: false,
emote: 'idle', equipment: { umbrella: false, raincoat: false, hood: false }, updatedAt: 1000, emoteStartedAt: 1000 };
remote.synchronize([player], 'self');
const ray = new Raycaster(new Vector3(0, 0, 5), new Vector3(0, 0, -1));
assert.equal(remote.pick(ray, 10), 'friend');
assert.equal(remote.pick(ray, 2), undefined, 'terrain in front prevents selection');
const from = new Vector3(3, 0, 0);
const right = remote.beside('friend', from, () => true)!;
assert.ok(right.position.x > 0 && right.position.z === 0);
assert.equal(right.yaw, 0);
const left = remote.beside('friend', from, point => point.x < 0)!;
assert.ok(left.position.x < 0);
assert.equal(remote.beside('friend', from, () => false), undefined);
remote.synchronize([player, { ...player, id: 'occupied', x: right.position.x }], 'self');
assert.ok(remote.beside('friend', from, () => true)!.position.x < 0);
remote.synchronize([], 'self');
assert.equal(remote.get('friend'), undefined);
assert.equal(remote.beside('friend', from, () => true), undefined);
geometry.dispose(); material.dispose();
});
test('remote snapshot reconciles membership, equipment and interpolated motion without local duplicate', () => {
const scene = new Group();
const equipment: boolean[][] = [];