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'; export class ItemManager { constructor(filePath, 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._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._createLinkLines(); } catch (error) { console.error("Could not load or process content JSON:", error); } } async _processItemData(itemArray) { for (const itemDef of itemArray) { let contentObject = null; // 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; } } // Create the Item instance const position = new THREE.Vector3(itemDef.position.x, itemDef.position.y, itemDef.position.z); // The Item constructor expects a single number for scale, which matches your JSON. const scale = itemDef.scale; const newItem = new Item( this.rapierWorld, this.scene, this.player, itemDef.isCollider, position, scale, itemDef.name, itemDef.model, itemDef.texture, [], // spawnedObjects placeholder contentObject ); // Set item id and group id newItem.id = itemDef.id; newItem.groupId = itemDef.groupId; // Set links if (itemDef.links) { newItem.links = itemDef.links; } if (itemDef.isCollider) { this.interactableItems.push(newItem); } // Setting a key and value pair in the Map (id, item) this.itemData.set(itemDef.id, newItem); } } _createLinkLines() { this.itemData.forEach(item => { if (!item.links) return; item.links.forEach(linkedItemId => { // Goes through each connection; links = [0, 1, 2..] const linkedItem = this.itemData.get(linkedItemId); if (!linkedItem) return; if (item.groupId !== linkedItem.groupId) return; // Creates a key for the Set; a Set can only have unique values, so used for no overlap const key = [item.id, linkedItemId].sort().join('-'); if (this.drawnLinks.has(key)) return; const material = new THREE.LineDashedMaterial({ color: 0x0000ff, dashSize: 3, gapSize: 1, }); const sourcePos = item.spawnPosition.clone(); const terminalPos = linkedItem.spawnPosition.clone(); sourcePos.y = 1; terminalPos.y = 1; const points = [item.spawnPosition.clone(), linkedItem.spawnPosition.clone()]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(geometry, material); line.computeLineDistances(); this.scene.add(line); // The Set is used as the key "0-1", then the item and the linkedItem are set as values in the Map? this.linkLines.set(key, { line, item1: item, item2: linkedItem }); // Make sure it is not drawn again, adding the unique pair here this.drawnLinks.add(key); }); }); } _updateLinkLines() { this.linkLines.forEach(link => { const { line, item1, item2 } = link; // Only update/show the line if both items are loaded if (item1.loadState === 'loaded' && item2.loadState === 'loaded') { line.visible = true; const positions = line.geometry.attributes.position; positions.setXYZ(0, item1.object.position.x, 1, item1.object.position.z); positions.setXYZ(1, item2.object.position.x, 1, item2.object.position.z); positions.needsUpdate = true; } else { line.visible = false; } }); } 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.loadState === 'unloaded') { item.loadModel(); } // Check if item should be unloaded else if (distance > this.unloadDistance && item.loadState === 'loaded') { item.unloadModel(); } }); this._updateLinkLines(); } }