// ACmoneyTree.js // Created by Mark Brosche on 10-18-2018 // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* global EventBridge Users AccountServices Agent Avatar EntityViewer */ Agent.isAvatar = true; Avatar.skeletonModelURL = 'http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst'; Avatar.displayName = "Money Tree Agent"; Avatar.position = {"x":-19.109256744384766,"y":-20.8349714279174805,"z":-11.181184768676758}; // Tree Position var googleURL = "https://script.google.com/a/highfidelity.io/macros/s/AKfycbxvLCGJwYKGOlszXwm2m-Xa1U90RmnIEJiD6nD7Z8-1RlZaADY/exec"; var AVERAGE_INTERVAL = 2.5, // Two minutes AVERAGE_HFC_AMOUNT = 10, STANDARD_DEVIATION = 5, SHOW_TIME_LENGTH = 60000, // One minute RECIPIENT_MAX = 3, MONEY_TREE_CHANNEL = "MoneyTreeChannel", AC_SCRIPT_RUN = true, SEARCH_CENTER = {x: -18.1834, y: -7.7738, z: -11.8755}, SEARCH_AREA= 1000; var populationID, marquisID, marquisMessage = "", targetGiver = [], targetRecipients = [], randomHFCAmount = null, payOnce = false, userList= [], coinSpawner = [], coinSpawnInterval = null, userListGiverIndex = null, bankersPresent = [], treePower = true, tempList = [], messageHandler, powerButtonSpawner = [], octreeInterval = null; var approvedBankerList = [ "markb", "ryan" ]; var staffList = [ "b", "Jazmin", "marko8904", "mhard", "theo", "adamisawake", "Aitolda", "Alan_", "alt.howard", "amantley", "amer", "amukul", "AndrewNyan", "andy_batman", "antoninas", "asbecker8", "Ashleigh_Harris", "Becky", "BenB", "bimyou", "birarda", "c", "Caitlyn", "ChangX", "Cheetah_Pattern", "Clement", "dantescalves", "dback", "diogenes", "dogbones", "emily", "Emily_Sykes", "emilythethird", "Firebird25", "freidrica", "GoofyGoober", "hifiDave", "hifi_jamil", "HifiJaz", "hifimarko", "HiFiMicki", "High.Fidelity", "howard.stearns", "huffman", "huffman2", "hyperlogic", "ingerjm0", "JeffClinton", "Jess", "Jherico", "john.highfidelity", "JSON", "jyoum", "Kayla_HF", "kencooke", "kkurian", "lameboycool", "leviathan", "luiscuenca", "markb", "maspring", "miladn", "mold", "MissLiviRose", "nbaldassini", "Nik", "Nissim", "nvrndr", "OneLisa", "ovohightower", "philip", "r3tk0n", "realMissLiviRose", "rickcarr044", "Roxie", "Sabrina", "sam", "SamGondelman", "sbbsteam", "sbirarda", "Sean", "seanjones2848", "serling", "sethalves", "shanzam", "simon_walton", "sophocles", "TheBanker", "thoys", "WadeWatts", "wayne", "yoHighness", "Ypsilanti", "zack_hifi", "ZappoMan", "zfox", "zfox1", "zeenai", "Battery", "conference2", "conference3", "demo1", "demo2", "Langton", "market", "purchases", "Anton.von.Trier", "kitchen", "mic1", "mic2", "ejahner", "Freyja", "qligmaloney", "userTest", "highfidelity", "ryan", "skelina", "siphnos", "ephesos", "pytho" ]; messageHandler = function(channel, message, senderUUID, localOnly) { if (channel !== MONEY_TREE_CHANNEL) { print("not on channel"); return; } else { message = JSON.parse(message); } if (message.type === 'moneyGiven') { if (payOnce === false) { if (senderUUID === targetGiver.nodeID){ for (var i = 0; i < targetRecipients.length; i++) { if (message.recipientID === targetRecipients[i].nodeID){ payOnce= true; sendInput(targetRecipients[i].username); spawnReceiverMessage(targetRecipients[i].nodeID, randomHFCAmount); Entities.editEntity(marquisID, {text: "PLZ WAIT 4 $"}); console.log("payOnce consumed", payOnce); Script.setTimeout(function(){ payOnce = false; console.log("payOnce returned to", payOnce); }, SHOW_TIME_LENGTH); } } } } } else if (message.type === 'tree power') { if (bankersPresent.length > 0){ treePower = message.state; if (treePower === true) { if (coinSpawnInterval){ Script.clearInterval(coinSpawnInterval); } startTree(); Entities.editEntity(marquisID, {text: "STARTING..."}); } else { if (coinSpawnInterval){ Script.clearInterval(coinSpawnInterval); } Entities.editEntity(marquisID, {text: "OUT OF ORDER"}); } } } else if (message.type === 'entering') { var avatarsInDomain = AvatarList.getAvatarIdentifiers(); var nameOnList = false; var bankerOnList = false; avatarsInDomain.forEach(function(nodeID){ // Only accept messages with valid Avatar UUIDs! if(message.nodeID === nodeID && senderUUID === nodeID){ for ( var i = 0; i < approvedBankerList.length; i++) { if (message.username === approvedBankerList[i]) { for (var i = 0; i < bankersPresent.length; i++) { if ( message.username === bankersPresent[i].username) { bankerOnList = true; } } if (!bankerOnList) { bankersPresent.push({ username: message.username, nodeID: message.nodeID }); console.log("added a banker, ", message.username, senderUUID, message.nodeID); bankerOverlay(message.nodeID, false); Messages.sendMessage(MONEY_TREE_CHANNEL, JSON.stringify({ type: "tree power", state: treePower })); bankerOnList = true; } Entities.editEntity(populationID, {text: userList.length}); marquisMessage = Entities.getEntityProperties(marquisID, ['text']); if (marquisMessage.text !== "OUT OF ORDER"){ if (userList.length > 3 && (marquisMessage.text === "MOAR PPL PLZ" || marquisMessage.text === "ZZZzzzZZZzzz")) { Entities.editEntity(marquisID, {text: "PREPARE 4 $"}); } } } } userList.forEach(function(index){ // If the username or UUID is already on the list, do not add it to the list! if (message.username.toLowerCase() === index.username || Uuid.isEqual(message.nodeID, index.nodeID) || Uuid.isEqual(senderUUID, index.nodeID)) { nameOnList = true; } }); if (!nameOnList) { // Add name and nodeID to list userList.push({ username: message.username.toLowerCase(), nodeID: message.nodeID, staff: false }); // If user is staff, mark [].staff 'true' staffList.forEach(function(username){ if (message.username === username){ userList[userList.length-1].staff = true; } }) // Update Signs Entities.editEntity(populationID, {text: userList.length}); console.log("[MONEY TREE] user list length: ", userList.length); marquisMessage = Entities.getEntityProperties(marquisID, ['text']); if (marquisMessage.text !== "OUT OF ORDER"){ if (userList.length >= 3 && (marquisMessage.text === "MOAR PPL PLZ" || marquisMessage.text === "ZZZzzzZZZzzz")) { Entities.editEntity(marquisID, {text: "PREPARE 4 $"}); } else if (userList.length < 3) { Entities.editEntity(marquisID, {text: "MOAR PPL PLZ"}); } } } } }); } else if (message.type === 'leaving') { try { for ( var i = 0; i < bankersPresent.length; i++) { if (message.username === bankersPresent[i].username) { bankersPresent.splice(i, 1)[0]; bankerOverlay(message.nodeID, true); console.log("removed a banker, bankers remaining: ,", JSON.stringify(bankersPresent)); } } for (var i = 0; i < userList.length; i++) { if (message.username.toLowerCase() === userList[i].username) { userList.splice(i, 1)[0]; Entities.editEntity(populationID, {text: userList.length}); i--; marquisMessage = Entities.getEntityProperties(marquisID, ['text']); if (marquisMessage.text !== "OUT OF ORDER"){ if (userList.length < 3) { Entities.editEntity(marquisID, {text: "ZZZzzzZZZzzz"}); } } } } } catch (e) { print("[MONEY TREE] Error in retrieving username for deletion", e); return; } } }; // This function is used to allow the AC script to see and change entities // in the domain. function allowEntityAccess() { Entities.setPacketsPerSecond(6000); EntityViewer.setPosition(SEARCH_CENTER); EntityViewer.setCenterRadius(SEARCH_AREA); // This should allow us to see nano-scale entities from great distances EntityViewer.setVoxelSizeScale(Number.MAX_VALUE); octreeInterval = Script.setInterval(function() { EntityViewer.queryOctree(); }, 1000); console.log("This AC script now has access to entities in this domain!"); Script.setTimeout(function(){ try { if (Entities.getEntityProperties(Entities.findEntitiesByName("Money Tree Counter", SEARCH_CENTER, SEARCH_AREA)[0], ["id"]).id != undefined) { populationID = Entities.getEntityProperties(Entities.findEntitiesByName("Money Tree Counter", SEARCH_CENTER, SEARCH_AREA)[0], ["id"]).id; Entities.editEntity(populationID, {text: "--"}); marquisID = Entities.getEntityProperties(Entities.findEntitiesByName("Money Tree Status", SEARCH_CENTER, SEARCH_AREA)[0], ["id"]).id; Entities.editEntity(marquisID, {text: "BOOTING UP"}); powerButtonSpawner = Entities.findEntitiesByName("Power Button Spawner", SEARCH_CENTER, SEARCH_AREA); powerButtonSpawner.forEach(function(entityID){Entities.deleteEntity(entityID)}); } else { populationID = Entities.addEntity({ type: "Text", dimensions: { x: 0.3, y: 0.1854, z: 0.01 }, lineHeight: 0.125, text: "-", textColor: {"red": 255, "green": 255, "blue": 255}, backgroundColor: {"red": 0, "green": 0, "blue": 0}, name: "Money Tree Counter", position: {"x":-17.9620418548584,"y":-10.537128448486328,"z":-10.67333984375}, rotation: {"x":-0.16071832180023193,"y":0.6887953877449036,"z":0.1588284969329834,"w":0.6887840032577515}, visible: true, collisionless: true, userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" }); marquisID = Entities.addEntity({ type: "Text", dimensions: { x: 0.7585, y: 0.1695, z: 0.01 }, lineHeight: 0.115, text: "STARTING...", textColor: {"red": 255, "green": 255, "blue": 255}, backgroundColor: {"red": 0, "green": 0, "blue": 0}, name: "Money Tree Status", position: {"x":-17.9667,"y":-10.5250,"z":-11.4147}, rotation: {"x":-0.15957793593406677,"y":0.6889334321022034,"z":0.1589823216199875,"w":0.6888881921768188}, visible: true, collisionless: true, userData: "{ \"grabbableKey\": { \"grabbable\": false, \"kinematic\": false } }" }); counterID = Entities.getEntityProperties(populationID, ["id"]).id; statusID = Entities.getEntityProperties(marquisID, ["id"]).id; console.log("[MONEY TREE] created stuff", counterID, statusID); } } catch (e) { console.log("[MONEY TREE] could not find or create anything", e); } Messages.subscribe(MONEY_TREE_CHANNEL); Messages.messageReceived.connect(messageHandler); startTree(); }, 10000); } // This function checks to make sure that the entity server exists // and that the AC script has Rez permissions. // If one or both of those things is false, we'll check again in 5 seconds. function maybeAllowEntityAccess() { console.log("Attempting to give this AC script entity access..."); if (Entities.serversExist() && Entities.canRez()) { allowEntityAccess(); } else { if (!Entities.canRez()) { console.log("This AC script doesn't have rez permissions!"); } Script.setTimeout(maybeAllowEntityAccess, 5000); } } // This function will be called on startup. function startup() { maybeAllowEntityAccess(); } function bankerOverlay(index, remove){ try { if (remove) { var deleteMe = Entities.findEntitiesByName("Power Button Spawner", SEARCH_CENTER, SEARCH_AREA); deleteMe.forEach(function(entityID){ var entityData = Entities.getEntityProperties(entityID, ['userData']); userData = JSON.parse(entityData.userData); var validID = userData.bankerID; if (validID === index) { Entities.deleteEntity(entityID); } }); } else { var userData = { bankerID: index }; console.log("[MONEY TREE] CREATING POWER BUTTON, TREE is:", treePower); powerButtonSpawner = Entities.addEntity({ type: "Box", dimensions: { x: 0.5, y: 0.5, z: 0.5 }, name: "Power Button Spawner", script: "https://hifi-content.s3.amazonaws.com/brosche/DomainContent/Hub/MoneyTree/moneyTreeBankerClient.js", userData: JSON.stringify(userData), position: { x: -16.9779, y: -9.132, z: -10.7944 }, visible: false, collisionless: true }); } } catch (e) { console.log(e, "error attempting bankeroverlay()"); } } // If someone leaves the domain from within the tree zone, function leftDomain(id){ bankersPresent.forEach(function(index){ if (id === index.nodeID) { console.log("banker left domain", index.username); bankersPresent.splice(bankersPresent.indexOf(index),1)[0]; bankerOverlay(id, true); } }); for (var i = 0; i < userList.length; i++) { if (userList[i].nodeID === id){ console.log("user left domain", userList[i].username) userList.splice(i, 1)[0]; Entities.editEntity(populationID, {text: userList.length}); if (userList.length < 3) { Entities.editEntity(marquisID, {text: "ZZZzzzZZZzzz"}); } } } } function spawnReceiverMessage(receiver, amount){ print("[MONEY TREE] spawing reciever message", JSON.stringify(receiver)); var userData = { receiverID: receiver, amount: amount }; var avatar = AvatarList.getAvatar(receiver); Entities.addEntity({ type: "Box", dimensions: { x: 0.5, y: 0.5, z: 0.5 }, name: "Tree Gift Receipt", script: "https://hifi-content.s3.amazonaws.com/brosche/DomainContent/Hub/MoneyTree/moneyTreeReceiverClient.js", userData: JSON.stringify(userData), lifetime: 5, position: Vec3.sum(avatar.position, Vec3.multiplyQbyV(avatar.orientation, { x: -2.5, y: 0, z: -5 })), visible: false, collisionless: true, parentID: receiver }); try { print("[MONEY TREE] making coin particle"); Entities.addEntity({ lifetime: 5, collidesWith: "", collisionMask: 0, collisionless: true, position: Vec3.sum(avatar.position, Vec3.multiplyQbyV(avatar.orientation, { x: 0, y: 0, z: -1 })), dimensions: { x: 0.15, y: 0.15, z: 0.15 }, isEmitting: true, name: "Coin Particle", type: "ParticleEffect", userData: "{\"grabbableKey\":{\"grabbable\":false}}", lifespan: 0.5, maxParticles: 10, textures: "https://hifi-content.s3.amazonaws.com/brosche/Trivia/Four%20Square/assets/pictures/tempsnip.png", emitRate: 10, emitSpeed: 1.5, speedSpread: 2, emitDimensions: { x: 0, y: 0, z: 0 }, emitOrientation: { x: 0, y: 0, z: 0 }, emitterShouldTrail: false, particleRadius: 0.15, radiusSpread: 0, radiusStart: 0, radiusFinish: 0.15, color:{ red:255, blue:255, green:255 }, colorSpread:{ red:0, blue:0, green:0 }, colorStart:{ red:255, blue:255, green:255 }, colorFinish:{ red:255, blue:255, green:255 }, emitAcceleration:{ x:0, y:-10, z:1}, accelerationSpread:{ x:5, y:3, z:5 }, alpha: 1, alphaSpread: 0, alphaStart: 1, alphaFinish: 1, particleSpin: 0, spinSpread: 0, spinStart: 0, spinFinish: 0, rotateWithEntity: true, polarStart: 0, polarFinish: 0, azimuthStart: -2.9321532249450684, azimuthFinish: 0.5235987901687622 }); } catch (e) { print("[MONEY TREE] error spawning coin particle", e); } } function randomizeHFC(){ var funds = true; var rand = gaussian(AVERAGE_HFC_AMOUNT, STANDARD_DEVIATION); var listLength =Math.sqrt(userList.length); var amount = Math.ceil(rand*listLength) - Math.ceil(rand*listLength) % 5; if (!funds){ Entities.editEntity(marquisID, {text: "BANKRUPT!"}); return 0; } if (amount >= 50){ amount = 50; } else if (amount <= 5){ amount = 5; } return amount; } function randomizeInterval(){ var interval = gaussian(AVERAGE_INTERVAL, 1); return interval; } function createCoinSpawner() { console.log("[MONEY TREE] SPAWNING COINS"); if (coinSpawner.length > 0){ console.log("[MONEY TREE] DELETING COINS"); for (var i = 0; i < coinSpawner.length; i++ ) { Entities.deleteEntity(coinSpawner[i]); } } // Show the coins to the giver as overlays if (targetRecipients[0] == null){ console.log("[MONEY TREE] could not find eligible recipients"); return; } else if (targetRecipients.length < 2){ console.log("[MONEY TREE] not enough eligible recipients"); return; } else { console.log("[MONEY TREE] SPAWNING COINS..."); for (var i = 0; i < targetRecipients.length; i++){ var avatar = AvatarList.getAvatar(targetRecipients[i].nodeID); var sum = Vec3.sum(avatar.position, Vec3.UP); var userData = { giverID: targetGiver.nodeID }; coinSpawner[i] = Entities.addEntity({ type: "Box", dimensions: { x: 0.5, y: 0.5, z: 0.5 }, name: "Money Tree Gift", script: "https://hifi-content.s3.amazonaws.com/brosche/DomainContent/Hub/MoneyTree/moneyTreeClient.js", userData: JSON.stringify(userData), lifetime: 60, position: sum, visible: false, collisionless: true, parentID: avatar.sessionUUID }); } } } function pickAGiver(){ console.log("[MONEY TREE] PICKING GIVER"); // Choose a random user to display coins for giving // Do not choose a user again until all other users in range have been selected. userListGiverIndex = randInt(0, userList.length-1); if (userListGiverIndex < 0){ return; } else { if (targetGiver){ if (userList[userListGiverIndex].username === ""){ userList.splice(userListGiverIndex, 1)[0]; if (userListGiverIndex >= userList.length){ userListGiverIndex = randInt(0, userList.length-1); } } if (targetGiver.username !== userList[userListGiverIndex].username){ targetGiver = userList[userListGiverIndex]; } else { userListGiverIndex = randInt(0, userList.length-1); targetGiver = userList[userListGiverIndex]; } } else { targetGiver = userList[userListGiverIndex]; } } } function pickRecipients(){ console.log("[MONEY TREE] PICKING RECIPIENTS"); targetRecipients = []; tempList = []; tempList = userList.slice(); tempList.splice(userListGiverIndex, 1)[0]; for (var i = 0; i < tempList.length; i++) { if (tempList[i].staff === true) { console.log("[MONEY TREE] removing staff", tempList[i].username); tempList.splice(i,1)[0]; i--; } } if (tempList.length < 2) { console.log("[MONEY TREE] Not enough eligible recipients for receiver selection."); Entities.editEntity(marquisID, {text: "MOAR PPL PLZ"}); return; } var recipientCount = (tempList.length > RECIPIENT_MAX) ? RECIPIENT_MAX : tempList.length; for (var i = 0; i < recipientCount; i++){ var index = randInt(0, tempList.length-1); targetRecipients.push(tempList.splice(index-i, 1)[0]); console.log("[MONEY TREE] recipient:", JSON.stringify(targetRecipients[i].username)); } } function randFloat(low, high) { return low + Math.random() * (high - low); } function randInt(low, high) { return Math.floor(randFloat(low, high)); } function gaussian(mean, stdev) { // returns a gaussian random function with the given mean and stdev. var y2; var useLast = false; var y1; if (useLast) { y1 = y2; useLast = false; } else { var x1, x2, w; do { x1 = 2.0 * Math.random() - 1.0; x2 = 2.0 * Math.random() - 1.0; w = x1 * x1 + x2 * x2; } while ( w >= 1.0); w = Math.sqrt((-2.0 * Math.log(w))/w); y1 = x1 * w; y2 = x2 * w; useLast = true; } var retval = mean + stdev * y1; if (retval > 0) { return retval; } return -retval; } function sendInput(recipientUsername) { var paramString = encodeURLParams({ date: new Date().toLocaleString(), recipientUsername: recipientUsername, amount: randomHFCAmount }); var request = new XMLHttpRequest(); request.open('GET', googleURL + "?" + paramString); request.timeout = 10000; request.send(); } function encodeURLParams(params) { var paramPairs = []; for (var key in params) { paramPairs.push(key + "=" + params[key]); } return paramPairs.join("&"); } function sweep(){ // try { // var homelessParticles = Entities.findEntitiesByType("ParticleEffect", SEARCH_CENTER, SEARCH_AREA); // if (homelessParticles.length > 0) { // for (var i = 0; i < homelessParticles.length; i++ ) { // Entities.deleteEntity(homelessParticles[i]); // } // } // } catch (e) { // print("[MONEY TREE] particle sweep failed."); // return; // } try { var avatarsInDomain = AvatarList.getAvatarIdentifiers(); bankersPresent.forEach(function(index){ var count = 0; avatarsInDomain.forEach(function(nodeID){ if(index.nodeID === nodeID){ count += 1; } }); if (count < 1) { bankerOverlay(index, true); bankersPresent.splice(bankersPresent.indexOf(index),1)[0]; } }); userList.forEach(function(index){ var count = 0; avatarsInDomain.forEach(function(nodeID){ if(index.nodeID === nodeID){ count += 1; } }); if (count < 1) { userList.splice(userList.indexOf(index),1)[0]; } }); var collect = Entities.findEntitiesByName("Coin Particle", SEARCH_CENTER, SEARCH_AREA); if (collect.length > 0) { collect.forEach(function(entityID){ Entities.deleteEntity(entityID) }); } var extraCoins = Entities.findEntitiesByName("Money Tree Gift", SEARCH_CENTER, SEARCH_AREA); if (extraCoins.length > 0){ extraCoins.forEach(function(entityID){ Entities.deleteEntity(entityID); }); } if (!Array.isArray(powerButtonSpawner) || !powerButtonSpawner.length) { // array does not exist, is not an array, or is empty } var extraButtons = Entities.findEntitiesByName("Power Button Spawner", SEARCH_CENTER, SEARCH_AREA); if (extraButtons.length > 0 && bankersPresent.length === 0){ extraButtons.forEach(function(entityID){ Entities.deleteEntity(entityID); }); } } catch (e) { print("[MONEY TREE] entity sweep failed."); return; } } function startTree(){ console.log("new tree interval"); userList.forEach(function(user){ console.log("USERS ", JSON.stringify(user.username)); }); coinSpawnInterval = Script.setInterval(function(){ sweep(); Entities.editEntity(marquisID, {text: "SEARCHING..."}); if (userList.length >= RECIPIENT_MAX){ randomHFCAmount = randomizeHFC(); if (randomHFCAmount === 0) { Script.clearInterval(coinSpawnInterval); return; } pickAGiver(); console.log("[MONEY TREE] GIVER", JSON.stringify(targetGiver.username)); pickRecipients(); if (targetRecipients.length > 1) { Entities.editEntity(marquisID, {text: "OOH SHINY!"}); createCoinSpawner(); } } else { Entities.editEntity(marquisID, {text: "MOAR PPL PLZ"}); } }, randomizeInterval()*SHOW_TIME_LENGTH); } function appEnding() { sweep(); if (coinSpawnInterval){ Script.clearInterval(coinSpawnInterval); } if (octreeInterval){ Script.clearInterval(octreeInterval); } Messages.unsubscribe(MONEY_TREE_CHANNEL); Messages.messageReceived.disconnect(messageHandler); } AvatarList.avatarRemovedEvent.connect(leftDomain); Script.scriptEnding.connect(appEnding); startup();