// // spawnerEntity.js // /* TODO: quick docs for Wade reference: "respawnRadius": 0, // how far the ball needs to move before respawn is triggered "respawnDelay": 0, // wait this long after respawnRadius movement before rezzing a next ball "autospawnRadius": 4, // Avatars walking into this radius will autospawn a ball (unless there already are balls present) "maxChildren": 4, // absolute maximum children within the safetyRadius (if exceeded respawning will abort) "safetyRadius": 10, // radius to check max child count within "soundURL": "https://hifi-content.s3.amazonaws.com/wadewatts/Ring2.wav", "volume": 0.001 */ (function() { const ZERO_UUID = '{00000000-0000-0000-0000-000000000000}'; const SANETIZE_PROPERTIES = ['childEntities', 'parentID', 'id']; const DEFAULT_TRIGGER_SOUND_URL = 'http://hifi-content.s3.amazonaws.com/DomainContent/Tutorial/Sounds/advance.L.wav'; var CHILD_DESCRIPTON = 'spawner-child'; // earmark put on child entities for throttling var triggerSound = null; var lastUserData = null; var parsedUserData = null; var rezEntityTree = []; function entityListToTree(entitiesList) { function entityListToTreeRecursive(properties) { properties.childEntities = []; entitiesList.forEach(function(entityProperties) { if (properties.id === entityProperties.parentID) { properties.childEntities.push(entityListToTreeRecursive(entityProperties)); } }); return properties; } var entityTree = []; entitiesList.forEach(function(entityProperties) { if (entityProperties.parentID === undefined || entityProperties.parentID === ZERO_UUID) { entityTree.push(entityListToTreeRecursive(entityProperties)); } }); return entityTree; } // TODO: ATP support (currently the JS API for ATP does not support file links, only hashes) function importEntitiesJSON(importLink, parentProperties, overrideProperties) { if (parentProperties === undefined) { parentProperties = {}; } if (overrideProperties !== undefined) { parentProperties.overrideProperties = overrideProperties; } var request = new XMLHttpRequest(); request.open('GET', importLink, false); request.send(); try { var response = JSON.parse(request.responseText); parentProperties.childEntities = entityListToTree(response.Entities); return parentProperties; } catch (e) { print('Failed importing entities JSON because: ' + JSON.stringify(e)); } return null; } //Creates an entity and returns a mixed object of the creation properties and the assigned entityID var createEntity = function(entityProperties, parent, overrideProperties) { // JSON.stringify -> JSON.parse trick to create a fresh copy of JSON data var newEntityProperties = JSON.parse(JSON.stringify(entityProperties)); if (overrideProperties !== undefined) { Object.keys(overrideProperties).forEach(function(key) { newEntityProperties[key] = overrideProperties[key]; }); } if (parent.rotation !== undefined) { if (newEntityProperties.rotation !== undefined) { newEntityProperties.rotation = Quat.multiply(parent.rotation, newEntityProperties.rotation); } else { newEntityProperties.rotation = parent.rotation; } } if (parent.position !== undefined) { var localPosition = (parent.rotation !== undefined) ? Vec3.multiplyQbyV(parent.rotation, newEntityProperties.position) : newEntityProperties.position; newEntityProperties.position = Vec3.sum(localPosition, parent.position) } if (parent.id !== undefined) { newEntityProperties.parentID = parent.id; } newEntityProperties.id = Entities.addEntity(newEntityProperties); return newEntityProperties; }; var createEntitiesFromTree = function(entityTree, parent, overrideProperties) { if (parent === undefined) { parent = {}; } if (parent.overrideProperties !== undefined) { overrideProperties = parent.overrideProperties; } var createdTree = []; entityTree.forEach(function(entityProperties) { var sanetizedProperties = {}; Object.keys(entityProperties).forEach(function(propertyKey) { if (!entityProperties.hasOwnProperty(propertyKey) || SANETIZE_PROPERTIES.indexOf(propertyKey) !== -1) { return true; } sanetizedProperties[propertyKey] = entityProperties[propertyKey]; }); // Allow for non-entity parent objects, this allows us to offset groups of entities to a specific position/rotation var parentProperties = sanetizedProperties; if (entityProperties.type !== undefined) { parentProperties = createEntity(sanetizedProperties, parent, overrideProperties); } if (entityProperties.childEntities !== undefined) { parentProperties.childEntities = createEntitiesFromTree(entityProperties.childEntities, parentProperties, overrideProperties); } createdTree.push(parentProperties); }); return createdTree; }; function stopRespawner() { if (stopRespawner.interval) { Script.clearInterval(stopRespawner.interval); stopRespawner.interval = 0; } } function spawnEntity(entityID) { stopRespawner(); var properties = Entities.getEntityProperties(entityID, ['userData', 'position']); if (properties.userData !== lastUserData) { lastUserData = properties.userData; try { parsedUserData = JSON.parse(lastUserData); } catch (e) { print('Failed to parse userdata for entitySpawner: ' + entityID); return; } rezEntityTree = importEntitiesJSON(parsedUserData.importJSON, { position: parsedUserData.position }, { lifetime: parsedUserData.lifetime }); } if (rezEntityTree === null) { return; } if (triggerSound !== null && triggerSound.downloaded) { Audio.playSound(triggerSound, { position: properties.position, volume: isFinite(parsedUserData && parsedUserData.volume) ? parsedUserData.volume : 0.1 }); } // Calculate possible rez-position var createdEntities = createEntitiesFromTree([rezEntityTree]); var createdEntityID = createdEntities[0].childEntities[0].id; print('Created an entity with ID ' + createdEntityID); Entities.editEntity(createdEntityID, { description: CHILD_DESCRIPTON }); function pos() { return Entities.getEntityProperties(createdEntityID, ['position']).position || null; } var respawnRadius = parseFloat(parsedUserData.respawnRadius) || 0.001; var respawnDelay = parseFloat(parsedUserData.respawnDelay) || 1000; var maxChildren = parseFloat(parsedUserData.maxChildren) || 16; var safetyRadius = parseFloat(parsedUserData.safetyRadius) || 10; var startPosition = pos(); stopRespawner.interval = Script.setInterval(function() { var newPosition = pos(); if (!newPosition) { // entity probably deleted etc. return stopRespawner(); } var moved = Vec3.distance(startPosition, pos()); if (moved > respawnRadius) { stopRespawner(); print('scheduling respawning -- baseball moved: '+moved+'m', [respawnRadius, respawnDelay]); stopRespawner.interval = Script.setTimeout(function() { stopRespawner(); var nearbyChildren = countChildren(entityID, safetyRadius); if (nearbyChildren >= maxChildren) { print('safety abort -- not respawning more entities since there are already '+nearbyChildren+' within '+safetyRadius+' meters'); return; } Messages.sendMessage('baseball-spawner', [entityID, 'respawning -- baseball moved: '+moved+'m']); spawnEntity(entityID); }, respawnDelay); } }, 1000); } function countChildren(entityID, radius) { var pos = Entities.getEntityProperties(entityID, ['position']).position; return Entities.findEntities(pos, radius).map(function(id) { return Entities.getEntityProperties(id, ['description']).description; }).filter(function(s) { return s === CHILD_DESCRIPTON; }).length; } function scanner() { if (stopRespawner.interval) { return; } var pos = Entities.getEntityProperties(scanner.entityID, ['position']).position, distance = Vec3.distance(MyAvatar.position, pos), withinRange = distance < scanner.autospawnRadius; if (withinRange && !scanner.busy) { scanner.busy = true; var nearbyChildren = countChildren(scanner.entityID, scanner.autospawnRadius); if (nearbyChildren) { print('scanner detected avatar -- but there are '+nearbyChildren+' nearby children (so not autospawning...)'); } else { print('scanner detected avatar -- autospawning'); spawnEntity(scanner.entityID); } } else if (!withinRange && scanner.busy) { scanner.busy = false; } } this.stopScanner = function() { scanner.interval && Script.clearInterval(scanner.interval); scanner.interval = 0; }; this.unload = function(entityID) { stopRespawner(); this.stopScanner(); }; this.preload = function(entityID) { var data = {}; try { data = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); scanner.autospawnRadius = parseFloat(data.autospawnRadius); if (scanner.autospawnRadius) { print('starting scanner for autospawnRadius of: ' + scanner.autospawnRadius+'m'); scanner.entityID = entityID; scanner.interval = Script.setInterval(scanner, 1000); } } catch(e) { } triggerSound = SoundCache.getSound(data.soundURL || DEFAULT_TRIGGER_SOUND_URL); }; this.startFarTrigger = function(entityID, args) { spawnEntity(entityID); }; this.clickReleaseOnEntity = function(entityID, mouseEvent) { if (!mouseEvent.isLeftButton) { return; } spawnEntity(entityID); }; })