import * as THREE from 'three'; import { Player } from './Player'; import { Item } from './Item'; import { TextContent, ImageContent, VideoContent, AudioContent } from './Content'; import { GeometryUtils } from 'three/examples/jsm/Addons.js'; import { Line2 } from 'three/examples/jsm/lines/Line2.js'; import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'; export class ItemManager { constructor(filePath, preloadedObjects, dynamicGroups, scene, rapierWorld, player, interactableItems) { this.filePath = filePath; this.scene = scene; this.rapierWorld = rapierWorld; this.player = player, this.interactableItems = interactableItems; this.audioListener = player.audioListener; this.itemData = new Map(); this.loadDistance = 250; this.unloadDistance = 300; this.linkLines = new Map(); this.drawnLinks = new Set(); this.preloadedObjects = preloadedObjects; this.dynamicGroups = dynamicGroups; this.groupCentres = new Map(); this.groupCenterLinks = new Map(); this.interGroupLinks = new Map(); this._init(); } async _init() { await this._loadFromJSON(); } async _loadFromJSON() { try { const resp = await fetch(this.filePath); if (!resp.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await resp.json(); await this._processItemData(data.items); this._createGroupCenterLinks(); if (data.groupLinks) this._createInterGroupLinks(data.groupLinks); } catch (error) { console.error("Could not load or process content JSON:", error); } } async _processItemData(itemArray) { for (const itemDef of itemArray) { let contentObject = null; const preloadedObject = this.preloadedObjects.get(itemDef.name); if (!preloadedObject) { console.warn(`Could not find a preloaded 3D object for item name: "${itemDef.name}"`); continue; } // Find the correct THREE.Group for this item const parentGroup = this.dynamicGroups.get(itemDef.groupId); if (!parentGroup) { console.warn(`Could not find a group for groupId: "${itemDef.groupId}"`); continue; } // Create content object based on contentType and file if (itemDef.contentType && itemDef.file) { switch (itemDef.contentType) { case 'text': // Fetch the text content from the specified file try { const textResponse = await fetch(itemDef.file); if (!textResponse.ok) throw new Error(`Failed to fetch text: ${textResponse.status}`); const textData = await textResponse.text(); contentObject = new TextContent(textData); } catch (e) { console.error(`Could not load text content for item "${itemDef.name}" from ${itemDef.file}`, e); } break; case 'image': contentObject = new ImageContent(itemDef.file); break; case 'video': contentObject = new VideoContent(itemDef.file); break; case 'audio': if (!this.audioListener) { console.error("AudioListener not provided. Cannot create AudioContent."); continue; // Skip this item } contentObject = new AudioContent(itemDef.file, this.audioListener); break; default: console.warn(`Unknown content type: ${itemDef.contentType}`); break; } } const newItem = new Item( this.rapierWorld, parentGroup, this.player, true, preloadedObject.position, 1, itemDef.name, null, null, [], // spawnedObjects placeholder contentObject, preloadedObject ); // Set item id and group id newItem.id = itemDef.id; newItem.groupId = parentGroup.name; // Item is already loaded from world glb, but this func creates collider etc. await newItem.loadModel(); newItem.show(); this.interactableItems.push(newItem); // Setting a key and value pair in the Map (id, item) this.itemData.set(itemDef.id, newItem); } } _createGroupCenterLinks() { this.itemData.forEach(item => { const groupCenter = this._getGroupCentre(item.groupId); if (!groupCenter) return; const material = new THREE.LineBasicMaterial({ color: 0xFF0000, transparent: false, opacity: 1 }); const points = [groupCenter.clone(), item.spawnPosition.clone()]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(geometry, material); line.visible = false; // Initially hidden this.scene.add(line); // Store the line and the group it belongs to, keyed by the item's ID this.groupCenterLinks.set(item.id, { line, groupId: item.groupId }); }); } _createInterGroupLinks(groupLinks) { groupLinks.forEach(linkPair => { const [groupA_ID, groupB_ID] = linkPair; const centerA = this._getGroupCentre(groupA_ID); const centerB = this._getGroupCentre(groupB_ID); if (!centerA || !centerB) { console.warn(`Could not find centers for group link: ${groupA_ID} to ${groupB_ID}`); return; } const lineGeometry = new LineGeometry(); lineGeometry.setPositions([ centerA.x, centerA.y, centerA.z, centerB.x, centerB.y, centerB.z ]); const lineMaterial = new LineMaterial({ color: 0xffa500, // Orange color for distinction linewidth: 2, // in pixels resolution: new THREE.Vector2(window.innerWidth, window.innerHeight), fog:true }); const line = new Line2(lineGeometry, lineMaterial); this.scene.add(line); const linkKey = `${groupA_ID}-${groupB_ID}`; this.interGroupLinks.set(linkKey, { line, groupA_ID, groupB_ID }); }); } _getGroupCentre(groupId) { const group = this.dynamicGroups.get(groupId); if (!group) { console.warn(`Group with id ${groupId} not found`); return null; } const bbox = new THREE.Box3().setFromObject(group); const centre = new THREE.Vector3(); bbox.getCenter(centre); return centre; } _updateGroupCentres() { this.dynamicGroups.forEach((group, groupId) => { const center = this._getGroupCentre(groupId); if (center) { let helper = this.groupCentres.get(groupId); if (!helper) { helper = new THREE.AxesHelper(3); // The number defines the size of the helper this.scene.add(helper); this.groupCentres.set(groupId, helper); } helper.position.copy(center); } }); } _updateGroupCenterLinks() { this.groupCenterLinks.forEach((linkData, itemId) => { const { line, groupId } = linkData; const item = this.itemData.get(itemId); if (item && item.isVisible) { const groupCenter = this._getGroupCentre(groupId); if (!groupCenter) return; line.visible = true; const positions = line.geometry.attributes.position; positions.setXYZ(0, groupCenter.x, groupCenter.y, groupCenter.z); positions.setXYZ(1, item.object.position.x, item.object.position.y, item.object.position.z); positions.needsUpdate = true; } else { line.visible = false; } }); } _updateInterGroupLinks() { this.interGroupLinks.forEach(link => { const centerA = this._getGroupCentre(link.groupA_ID); const centerB = this._getGroupCentre(link.groupB_ID); if (centerA && centerB) { link.line.geometry.setPositions([ centerA.x, centerA.y, centerA.z, centerB.x, centerB.y, centerB.z ]); } }); } update() { const playerPosition = this.player.camera.position; this.itemData.forEach(item => { let distance; if (item.lastPosition === null) { const initPositon = item.spawnPosition; distance = initPositon.distanceTo(playerPosition); } else { distance = item.lastPosition.distanceTo(playerPosition); } // Check if item should be loaded if (distance < this.loadDistance && !item.isVisible) { item.show(); } // Check if item should be unloaded else if (distance > this.unloadDistance && item.isVisible) { item.hide(); } }); this._updateGroupCenterLinks(); this._updateGroupCentres(); this._updateInterGroupLinks(); } }