You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
13 KiB
347 lines
13 KiB
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;
|
|
|
|
// Set links
|
|
if (itemDef.links) {
|
|
newItem.links = itemDef.links;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// _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: 0xffa500,
|
|
// dashSize: 10,
|
|
// 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')
|
|
// && (item1.isVisible && item2.isVisible)) {
|
|
// 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;
|
|
// }
|
|
// });
|
|
// }
|
|
}
|