import { type Object3D, MathUtils, Vector3 } from 'three'; import type { Sheep } from '../scene/sheep.ts'; import type { PlayerState } from './protocol.ts'; import { MULTIPLAYER } from '../constants/multiplayer.ts'; export function createRemotePlayers(template: Sheep, scene: Object3D, heightAt: (x: number, z: number) => number) { const players = new Map(); return { synchronize(states: PlayerState[], selfId: string) { const present = new Set(states.filter(p => p.id !== selfId).map(p => p.id)); for (const [id, entry] of players) if (!present.has(id)) { entry.sheep.dispose(); players.delete(id); } for (const state of states) { if (state.id === selfId) continue; let entry = players.get(state.id); if (!entry) { const sheep = template.clone(); sheep.group.name = state.nickname; sheep.group.position.set(state.x, heightAt(state.x, state.z), state.z); sheep.group.rotation.y = state.yaw; entry = { sheep, state, from: sheep.group.position.clone(), yaw: state.yaw, elapsed: 0 }; players.set(state.id, entry); scene.add(sheep.group); } else if (state.updatedAt !== entry.state.updatedAt) { entry.from.copy(entry.sheep.group.position); entry.yaw = entry.sheep.group.rotation.y; entry.elapsed = 0; } entry.state = state; entry.sheep.setEquipment(state.equipment.umbrella, state.equipment.raincoat, state.equipment.hood); } }, update(delta: number, serverTime: number) { let changed = false; for (const entry of players.values()) { const { sheep, state, from } = entry; const duration = MULTIPLAYER.interpolationMs / 1000; const interpolating = entry.elapsed < duration; entry.elapsed += Math.max(0, delta); const fraction = Math.min(1, entry.elapsed / duration); const x = MathUtils.lerp(from.x, state.x, fraction), z = MathUtils.lerp(from.z, state.z, fraction); sheep.group.position.set(x, heightAt(x, z), z); const angle = Math.atan2(Math.sin(state.yaw - entry.yaw), Math.cos(state.yaw - entry.yaw)); sheep.group.rotation.y = entry.yaw + angle * fraction; const active = sheep.pose(state.emote, Math.max(0, serverTime - state.emoteStartedAt) / 1000, state.moving && serverTime - state.updatedAt < MULTIPLAYER.staleMovementMs, serverTime / 1000); changed = changed || interpolating || active; } return changed; }, clear() { for (const entry of players.values()) entry.sheep.dispose(); players.clear(); }, }; }