| import * as THREE from "three"; | |
| export class InteractionSystem { | |
| constructor({ scene, camera, input, store, bus, player, interactables }) { | |
| this.scene = scene; | |
| this.camera = camera; | |
| this.input = input; | |
| this.store = store; | |
| this.bus = bus; | |
| this.player = player; | |
| this.interactables = interactables; | |
| this.byId = new Map(interactables.map(item => [item.id, item])); | |
| this.raycaster = new THREE.Raycaster(); | |
| this.raycaster.far = 3; | |
| this.lastPrompt = ""; | |
| } | |
| update() { | |
| if (this.store.state.mode !== "PLAYING") return this.publish(null); | |
| if (this.player.sitting) { | |
| const prompt = `${this.binding()} Stand up`; | |
| this.publish({ id: "player.stand", enabled: true, prompt }); | |
| if (this.input.consume("INTERACT")) this.player.stand(); | |
| return; | |
| } | |
| this.raycaster.setFromCamera({ x: 0, y: 0 }, this.camera); | |
| const intersections = this.raycaster.intersectObject(this.scene, true); | |
| let target = null; | |
| for (const hit of intersections) { | |
| let visible = true; | |
| for (let node = hit.object; node; node = node.parent) visible &&= node.visible; | |
| if (!visible) continue; | |
| const metadata = this.byId.get(hit.object.userData.entityId); | |
| if (!metadata || hit.distance > metadata.interactionDistance) break; | |
| const enabled = metadata.enabled ? metadata.enabled(this.store.state) : true; | |
| const label = enabled ? metadata.prompt(this.store.state) : metadata.disabledPrompt?.(this.store.state); | |
| target = { id: metadata.id, enabled, prompt: `${enabled ? this.binding() : ""} ${label || ""}`.trim(), metadata }; | |
| this.raycaster.ray.direction.normalize(); | |
| if (enabled && this.input.consume("INTERACT")) { | |
| metadata.onInteract({ | |
| direction: this.raycaster.ray.direction.clone(), | |
| point: hit.point, | |
| distance: hit.distance, | |
| }); | |
| } | |
| break; | |
| } | |
| this.publish(target); | |
| } | |
| binding() { | |
| if (this.input.device === "gamepad") return "[X]"; | |
| if (this.input.device === "touch") return "[USE]"; | |
| return "[E]"; | |
| } | |
| publish(target) { | |
| const signature = target ? `${target.id}:${target.prompt}:${target.enabled}` : ""; | |
| if (signature === this.lastPrompt) return; | |
| this.lastPrompt = signature; | |
| this.store.state.session.targetId = target?.id || null; | |
| this.bus.emit("INTERACTION_TARGET", target); | |
| } | |
| interactById(id) { | |
| const metadata = this.byId.get(id); | |
| if (!metadata) return false; | |
| const enabled = metadata.enabled ? metadata.enabled(this.store.state) : true; | |
| if (!enabled) return false; | |
| metadata.onInteract({ direction: new THREE.Vector3(0, 0, -1), point: new THREE.Vector3(), distance: 0 }); | |
| return true; | |
| } | |
| } | |