// // bingoScannerZoneServer.js // Created by Rebecca Stankus on 11/06/2018 // Copyright High Fidelity 2018 // // Licensed under the Apache 2.0 License // See accompanying license file or http://apache.org/ // /* global Audio, Entities, JSON, Math, Quat, Script, SoundCache */ (function() { var BINGO_WHEEL = "{57e5e385-3968-4ebf-8048-a7650423d83b}"; var STAGE_ENTRY_GATE = "{79ebf800-d2bb-4c20-b046-75e5f4f1553a}"; var MAIN_STAGE_BOUNCER_ZONE = "{5ca26b63-c61b-447e-8985-b0269b33eed0}"; var SCANNER_TRAP_DOOR = "{ab82d854-98da-44bf-8545-0544227bc56c}"; var WINNER_SIGN = "{2bba449a-c8a2-484a-825c-3f6823882023}"; var LOSER_SIGN = "{9500318e-bc57-40f5-b8c4-3b3919372521}"; var GAME_AUDIO_POSITION = { x: -79, y: -14, z: 6 }; var COMPUTING_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/bingoComputing.wav")); var SCANNER_WIN_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/bingoScannerWin.wav")); var SCANNER_LOSE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/bingoLose.wav")); var SAD_TROMBONE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/bingoSadTrombone.wav")); var WAIT_TO_CLOSE_TRAP_DOOR_MS = 1500; var WAIT_TO_CLOSE_WIN_GATE_MS = 2000; var WAIT_FOR_ENTITIES_TO_LOAD_MS = 2000; var SPREADSHEET_URL = Script.require(Script.resolvePath('../../secrets/bingoSheetURL.json')).sheetURL; var _this; var zonePosition; var scannerSpotlight; var confettiParticleEffect; var bingoParticleEffect; var currentlyScanningCard = false; var request = Script.require(Script.resolvePath('../../modules/request.js')).request; // ************************************* // START UTILITY FUNCTIONS // ************************************* /* ENCODE URL PARAMETERS: Formats data to send to Google sheet*/ function encodeURLParams(params) { var paramPairs = []; for (var key in params) { paramPairs.push(key + "=" + params[key]); } return paramPairs.join("&"); } /* PLAY A SOUND: Plays the specified sound at specified position and volume */ var injector; function playSound(sound, position, volume) { if (sound.downloaded) { if (injector) { injector.stop(); } injector = Audio.playSound(sound, { position: GAME_AUDIO_POSITION, volume: volume }); } } // ************************************* // END UTILITY FUNCTIONS // ************************************* var BingoScannerZone = function() { _this = this; }; BingoScannerZone.prototype = { remotelyCallable: ['alreadyCalledNumbersReply', 'scanCard', 'userLeftZone'], /* ON LOADING THE SCRIPT: Save a reference to this entity ID. Set a timeout for 2 seconds to ensure that entities have loaded and then find and save references to other necessary entities. */ preload: function(entityID) { _this.entityID = entityID; Script.setTimeout(function() { var properties = Entities.getEntityProperties(_this.entityID, ['parentID', 'position']); zonePosition = properties.position; var zoneMarker = properties.parentID; Entities.getChildrenIDs(zoneMarker).forEach(function(childOfZoneMarker) { var name = Entities.getEntityProperties(childOfZoneMarker, 'name').name; if (name === "Bingo Scanner Spotlight") { scannerSpotlight = childOfZoneMarker; } else if (name === "Bingo Particle Bingo") { bingoParticleEffect = childOfZoneMarker; } else if (name === "Bingo Particle Confetti") { confettiParticleEffect = childOfZoneMarker; } }); }, WAIT_FOR_ENTITIES_TO_LOAD_MS); }, /* SCAN USER'S CARD: Get the called numbers for this bingo round from the wheel's server script then get the user's assigned card numbers and delete any cards already around the scanner. Turn the spotlight on and play an enter sound then set a timeout for the duration of that sound. When the sound has finished, if the user's numbers were found, play the computing sound, and set a timeout for part of the duration of that sound after which we beging to validate their card winning. If the user's numbers were not found, create a sign above the machine stating that. */ scanCard: function(thisID, params) { if (currentlyScanningCard) { return; } currentlyScanningCard = true; Entities.editEntity(SCANNER_TRAP_DOOR, { localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 })}); playSound(COMPUTING_SOUND, GAME_AUDIO_POSITION, 1); // eslint-disable-next-line no-magic-numbers playSound(COMPUTING_SOUND, zonePosition, 0.5); Entities.editEntity(scannerSpotlight, { visible: true }); Entities.callEntityMethod(BINGO_WHEEL, 'requestAlreadyCalledNumbers', ["bingoScanner", _this.entityID, params[0]]); }, /* RECEIVE NUMBERS THAT HAVE BEEN CALLED THIS ROUND */ alreadyCalledNumbersReply: function(id, args) { var calledLettersAndNumbers = JSON.parse(args[0]); var username = args[1]; _this.getUsersCardNumbers(username, calledLettersAndNumbers); }, /* GET USER'S CARD NUMBERS: Get the Google sheet URL from a private text file, then search the sheet for the user. If the user is found, save their card numbers. */ getUsersCardNumbers: function(username, calledLettersAndNumbers) { var searchParamString = encodeURLParams({ type: "searchOnly", username: username }); request({ uri: SPREADSHEET_URL + "?" + searchParamString }, function (error, response) { if (error || !response || response.status !== "success") { return; } if (response.newUser) { _this.lose(); } else { _this.validateWin(username, response.userCardNumbers, calledLettersAndNumbers); } }); }, /* VALIDATE A WIN */ validateWin: function(username, userNumbers, calledLettersAndNumbers) { var rowIterator, colIterator; // Strip away the letter and space from every element in calledLettersAndNumbers // -1 is the free space and was always "called" var calledNumbers = [-1]; for (var i = 0; i < calledLettersAndNumbers.length; i++) { // eslint-disable-next-line no-magic-numbers calledNumbers.push(parseInt(calledLettersAndNumbers[i].substring(2))); } var userNumbers2D = []; var numberOfRowsAndColumns = 5; var centerRowAndColumnIndex = 2; for (rowIterator = 0; rowIterator < numberOfRowsAndColumns; rowIterator++) { var currentColumn = []; for (colIterator = 0; colIterator < numberOfRowsAndColumns; colIterator++) { // Handle free space if (rowIterator === centerRowAndColumnIndex && colIterator === centerRowAndColumnIndex) { currentColumn.push(-1); } else { currentColumn.push(userNumbers.shift()); } } userNumbers2D.push(currentColumn); } function checkRow(rowIndex) { for (colIterator = 0; colIterator < numberOfRowsAndColumns; colIterator++) { if (calledNumbers.indexOf(userNumbers2D[rowIndex][colIterator]) === -1) { return false; } } return true; } function checkColumn(columnIndex) { for (rowIterator = 0; rowIterator < numberOfRowsAndColumns; rowIterator++) { if (calledNumbers.indexOf(userNumbers2D[rowIterator][columnIndex]) === -1) { return false; } } return true; } function checkDiagonalTopLeftToBottomRight() { for (rowIterator = 0; rowIterator < numberOfRowsAndColumns; rowIterator++) { if (calledNumbers.indexOf(userNumbers2D[rowIterator][rowIterator]) === -1) { return false; } } return true; } function checkDiagonalTopRightToBottomLeft() { for (rowIterator = 0; rowIterator < numberOfRowsAndColumns; rowIterator++) { // eslint-disable-next-line no-magic-numbers if (calledNumbers.indexOf(userNumbers2D[4 - rowIterator][rowIterator]) === -1) { return false; } } return true; } for (i = 0; i < numberOfRowsAndColumns; i++) { if (checkRow(i)) { _this.win(username); return; } if (checkColumn(i)) { _this.win(username); return; } } if (checkDiagonalTopLeftToBottomRight() || checkDiagonalTopRightToBottomLeft()) { _this.win(username); return; } _this.lose(); }, /* WIN: Play the winning sound and turn on confetti and bingo particles. */ win: function(username) { Entities.editEntity(WINNER_SIGN, { visible: true }); playSound(SCANNER_WIN_SOUND, GAME_AUDIO_POSITION, 1); // eslint-disable-next-line no-magic-numbers playSound(SCANNER_WIN_SOUND, zonePosition, 0.5); Entities.callEntityMethod(bingoParticleEffect, 'turnOn'); Entities.callEntityMethod(confettiParticleEffect, 'turnOn'); try { var bouncerZoneUserData = JSON.parse( Entities.getEntityProperties(MAIN_STAGE_BOUNCER_ZONE, 'userData').userData); if (bouncerZoneUserData.whitelist.usernames.indexOf(username) === -1) { bouncerZoneUserData.whitelist.usernames.push(username); Entities.editEntity(MAIN_STAGE_BOUNCER_ZONE, { userData: JSON.stringify(bouncerZoneUserData) }); } } catch (err) { print("Error adding winner to bouncer zone userData"); } Entities.callEntityMethod(STAGE_ENTRY_GATE, 'openGate'); Entities.callEntityMethod(BINGO_WHEEL, 'addCurrentRoundWinner', [username]); currentlyScanningCard = false; }, /* LOSE: Play the losing buzzer and lower the trap door. Set a timeout for after the buzzer sound has finished to play the sad sound */ lose: function() { Entities.editEntity(LOSER_SIGN, { visible: true }); playSound(SCANNER_LOSE_SOUND, GAME_AUDIO_POSITION, 1); // eslint-disable-next-line no-magic-numbers playSound(SCANNER_LOSE_SOUND, zonePosition, 0.5); Script.setTimeout(function() { playSound(SAD_TROMBONE_SOUND, GAME_AUDIO_POSITION, 1); // eslint-disable-next-line no-magic-numbers playSound(SAD_TROMBONE_SOUND, zonePosition, 0.5); // eslint-disable-next-line no-magic-numbers }, SCANNER_LOSE_SOUND.duration * 1000); Entities.editEntity(SCANNER_TRAP_DOOR, { localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })}); currentlyScanningCard = false; }, /* HIDE BANNERS ABOVE MACHINE: Delete the bingo card created by this script and any others found that may have lost their referenece due to crash or errors. */ hideBanners: function() { Entities.editEntity(WINNER_SIGN, { visible: false }); Entities.editEntity(LOSER_SIGN, { visible: false }); }, /* WHEN USER LEAVES ZONE: Clear any open gates or trap door. Clear the array of squares attached to the user's card and delete the card. Turn off particles. */ userLeftZone: function(thisID, userID) { Script.setTimeout(function() { _this.hideBanners(); Entities.callEntityMethod(STAGE_ENTRY_GATE, 'closeGate'); }, WAIT_TO_CLOSE_WIN_GATE_MS); Entities.editEntity(scannerSpotlight, { visible: false }); Entities.callEntityMethod(bingoParticleEffect, 'turnOff'); Entities.callEntityMethod(confettiParticleEffect, 'turnOff'); Script.setTimeout(function() { Entities.editEntity(SCANNER_TRAP_DOOR, { localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 })}); }, WAIT_TO_CLOSE_TRAP_DOOR_MS); }, /* ON UNLOADING SCRIPT: Delete the card, clear the array of squares from the card, turn off particles and spotlight and clear any interval if necessary. */ unload: function(entityID) { _this.hideBanners(); Entities.editEntity(scannerSpotlight, { visible: false }); Entities.editEntity(bingoParticleEffect, { emitRate: 0 }); Entities.editEntity(confettiParticleEffect, { emitRate: 0 }); Entities.editEntity(SCANNER_TRAP_DOOR, { angularVelocity: { x: 0, y: 0, z: 0 }, localRotation: { x: 0, y: 0, z: 0 } }); } }; return new BingoScannerZone(); });