// .env Variables require('dotenv').config({path: '../.env'}); // Node Modules const Discord = require('discord.js'); const client = new Discord.Client(); const cron = require('node-cron'); // Bot Modules const dataRequest = require('../modules/dataRequest'); const calcRandom = require('../modules/calcRandom'); const PlayerClass = require('./playerClass'); const DungeonClass = require('./dungeonClass'); // JSON const rooms = require('./rooms.json'); // Playing dialogs const nothingMessage = "with locks." // State Machine var BotEnumState = { NOTHING: 0, WAITING_FOR_PLAYERS: 1, ACTIVE: 2 } // Directions var Directions = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 } // Dungeon State var DungeonState = { WAITING_FOR_USERS: 0, INSIDE_DUNGEON: 1, FIGHTING: 2, BOSS_BATTLE: 3 } // Modes (these better not be final, me.) var DungeonModes = { HEIRARCHY: 0, // Uses heirarchy to determine movement ANARCHY: 1, // Anyone can determine movement DEMOCRACY: 2 // Voting system } // Dungeons class DungeonRaidInstance { constructor(room) { this.room = room; this.mode = DungeonModes.DEMOCRACY; this.players = []; this.location; this.state = DungeonState.WAITING_FOR_USERS; this.dialogObj; this.reroutedRooms = []; this.items = []; this.isTyping; this.timer; this.startTimer; this.crystalsGained = 0; this.directionalCollector; this.directionalMessageID; } } // Dungeon collections; var dungeonCollection = []; // The ready event is vital, it means that your bot will only start reacting to information // from Discord _after_ ready is emitted client.on('ready', async () => { // Generates invite link try { let link = await client.generateInvite(["ADMINISTRATOR"]); console.log("Invite Link: " + link); } catch(e) { console.log(e.stack); } // You can set status to 'online', 'invisible', 'away', or 'dnd' (do not disturb) client.user.setStatus('invisible'); console.log(`Connected! \ \nLogged in as: ${client.user.username} - (${client.user.id})`); //client.guilds.get(process.env.SANCTUM_ID).members.get(client.user.id).setNickname(""); }); // Create an event listener for messages client.on('message', async message => { // Ignores ALL bot messages if (message.author.bot) return; // Message has to be in test or hell's gate if (!(message.channel.id !== process.env.TEST_CHANNEL_ID || message.channel.id !== process.env.HELLS_GATE_CHANNEL_ID)) { const args = message.content.slice(process.env.PREFIX.length).trim().split(/ +/g); const command = args.shift().toLowerCase(); if (command !== "party") return; }; // Has to be (prefix)command if (message.content.indexOf(process.env.PREFIX) !== 0) return; // "This is the best way to define args. Trust me." // - Some tutorial dude on the internet const args = message.content.slice(process.env.PREFIX.length).trim().split(/ +/g); const command = args.shift().toLowerCase(); switch (command) { case "ping": if (isAdmin(message.author.id)) { return message.reply("Pong!"); } break; case "summonr": if (isAdmin(message.author.id)) { return NewDungeonSequence(process.env.TEST_CHANNEL_ID); } break; case "vanishr": if (isAdmin(message.author.id) && getDungeonPlayer(message.author.id).playerDungeon !== undefined) { return BotActive(getDungeonPlayer(message.author.id).playerDungeon); } break; case "help": if (args[0] && message.mentions.members.first() !== undefined) { if (message.mentions.members.first().id !== client.user.id) break; let tempVar = ''; tempVar += "Here are the commands you can do." tempVar += "\n\n" tempVar += "!map (!m)\n```Checks your high-tech map about the dungeon.```" tempVar += "\n" tempVar += "!party (!p)\n```Lists all members in your party.```" tempVar += "\n" tempVar += "!promote (!pr)\n```If you're the leader, you can promote others to be a party leader with you in order to move around.```" tempVar += "\n" tempVar += "!demote (!de)\n```If you're the leader, you can demote others if needed if they are a commander.```" return sendMessage(message.channel.id, tempVar); } break; case "leave": leaveMessage(message); break; } // Scans if player is in the raid collection const dungeonPlayer = getDungeonPlayer(message.author.id); var isLeader = dungeonPlayer.isLeader; // Gets if author is leader of raid var isCommander = dungeonPlayer.isCommander; // Gets if userID is commander var playerID = dungeonPlayer.playerID; // Grabs message.author if this is part of raid var playerDungeon = dungeonPlayer.playerDungeon; // Grab's the player's dungeon if joined // If user has not joined a party /* if (!playerID) { switch (command) { case "join": joinMessage(message, getRoomName(args[0])); break; case "j": joinMessage(message, getRoomName(args[0])); break; } } */ // Party member commands only if (!playerID) return; // Party commands switch (command) { case "check": return sendMessage(message.author.id, message.channel.id, "Please use **!map** for checking the map."); case "members": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return partyMembers(playerDungeon, num); } } else { return partyMembers(playerDungeon); } break; case "party": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return partyMembers(playerDungeon, num); } } else { return partyMembers(playerDungeon); } break; case "p": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return partyMembers(playerDungeon, num); } } else { return partyMembers(playerDungeon); } break; case "promote": return promoteCommander(message.author, message.mentions.members.first(), isLeader, message.channel.id); case "pr": return promoteCommander(message.author, message.mentions.members.first(), isLeader, message.channel.id); case "demote": return demoteCommander(message.author, message.mentions.members.first(), isLeader, message.channel.id); case "de": return demoteCommander(message.author, message.mentions.members.first(), isLeader, message.channel.id); case "transferleadership": return transferLeader(message, message.mentions.members.first()); case "giveleadership": return transferLeader(message, message.mentions.members.first()); case "map": return sendEmbedLocation(playerDungeon); case "m": return sendEmbedLocation(playerDungeon); case "location": return sendEmbedLocation(playerDungeon); case "l": return sendEmbedLocation(playerDungeon); case "locationtext": return sendLocation(playerDungeon, message.author); case "lt": return sendLocation(playerDungeon, message.author); case "pinventory": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return sendInventory(playerDungeon, num); } } else { return sendInventory(playerDungeon); } case "pinv": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return sendInventory(playerDungeon, num); } } else { return sendInventory(playerDungeon); } case "pi": if (args[0] !== undefined) { var num = Math.floor(parseInt(args[0])); if (Number.isSafeInteger(num) && Math.sign(num) > 0) { return sendInventory(playerDungeon, num); } } else { return sendInventory(playerDungeon); } case "timer": return sendTimerInChat(playerDungeon); case "cleardialog": console.log("Clearing player dungeon by command trigger..."); playerDungeon.dialogObj = undefined; return; case "debug": console.log(playerDungeon.reroutedRooms); return; } // The dungeon has to be active now if (playerDungeon.state === DungeonState.WAITING_FOR_USERS) { //return sendMessage(message.author.id, message.channel.id, "**Command denied.** Please wait for until the dungeon closes."); return; } // Custom commands from JSON if (playerDungeon.dialogObj === undefined) { var newCommand = playerDungeon.location[command]; customCommands(newCommand, playerDungeon, message.author, command, args); // More custom commands that are children from other custom commands } else { var newCommand = playerDungeon.dialogObj[command]; customCommands(newCommand, playerDungeon, message.author, command, args); return; } // Leader or commander only commands (movement) return; switch (command) { case "north": move(Directions.NORTH, playerDungeon, (isLeader || isCommander), message.author); break; case "n": move(Directions.NORTH, playerDungeon, (isLeader || isCommander), message.author); break; case "east": move(Directions.EAST, playerDungeon, (isLeader || isCommander), message.author); break; case "e": move(Directions.EAST, playerDungeon, (isLeader || isCommander), message.author); break; case "south": move(Directions.SOUTH, playerDungeon, (isLeader || isCommander), message.author); break; case "s": move(Directions.SOUTH, playerDungeon, (isLeader || isCommander), message.author); break; case "west": move(Directions.WEST, playerDungeon, (isLeader || isCommander), message.author); break; case "w": move(Directions.WEST, playerDungeon, (isLeader || isCommander), message.author); break; case "mode": break; case "m": break; } }); client.on('error', console.error); client.on('warn', console.warn); // Gets if user has an Overseers rank function isAdmin(userID) { var guild = client.guilds.get(process.env.SANCTUM_ID); return guild.members.get(userID).roles.find(role => role.name === "Overseers"); } // Creates an array that creates multiple different arrays inside of that array -> [[1, 2, 3], [4, 5, 6], [7, 8, 9]] // http://www.frontcoded.com/splitting-javascript-array-into-chunks.html var createGroupedArray = function(arr, chunkSize) { var groups = [], i; for (i = 0; i < arr.length; i += chunkSize) { groups.push(arr.slice(i, i + chunkSize)); } return groups; } // Gets the user's role by member function getFactionID(member) { var playerRole; var isGroupA = member.roles.has(process.env.GROUP_A_ROLE); var isGroupB = member.roles.has(process.env.GROUP_B_ROLE); var isGroupC = member.roles.has(process.env.GROUP_C_ROLE); if (isGroupA) playerRole = process.env.GROUP_A_ROLE; else if (isGroupB) playerRole = process.env.GROUP_B_ROLE; else if (isGroupC) playerRole = process.env.GROUP_C_ROLE; else playerRole = "none"; return playerRole; } // Gets the Combat Class name by role function getCombatClassName(member) { var combatClass; var isTank = member.roles.find(role => role.name === "🛡️ Tank"); var isRogue = member.roles.find(role => role.name === "🗡️ Rogue"); var isDPSMelee = member.roles.find(role => role.name === "⚔️ DPS Melee"); var isDPSRange = member.roles.find(role => role.name === "🏹 DPS Range"); var isSupport = member.roles.find(role => role.name === "❤️ Support"); if (isTank) combatClass = "Tank"; else if (isRogue) combatClass = "Rogue"; else if (isDPSMelee) combatClass = "DPS Melee"; else if (isDPSRange) combatClass = "DPS Range"; else if (isSupport) combatClass = "Support"; else combatClass = "None"; return combatClass; } // Returns JSON room data by room name function getRoomName(name) { if (name !== undefined) { if (name.toLowerCase() === "hellsgate") return rooms.hellsgate; } else { name = ""; } return { "undefined": name }; } // Updates Player data in Dungeon Collection function updatePlayers() { // Scans if player is in the raid collection // Totally seemingly not intensive test of for loops (Wish I knew) if (dungeonCollection !== undefined) { // Loops through every dungeon active for (let index = 0; index < dungeonCollection.length; index++) { const dungeonElement = dungeonCollection[index]; // Loops through every player in that dungeon for (let index = 0; index < dungeonElement.players.length; index++) { const playerElement = dungeonElement.players[index]; const thisGuild = client.guilds.get(process.env.SANCTUM_ID); const newMember = thisGuild.members.get(playerElement.userID); // If user is in one of the dungeon if (newMember) { // Start updating playerElement.factionID = getFactionID(newMember); playerElement.combatClass = getCombatClassName(newMember); //playerElement.badgeEmote; } } } } } // Collects usable username for !party function playerName(userID) { const server = client.guilds.get(process.env.SANCTUM_ID); const sizeLimit = 19; var name = server.members.get(userID).displayName; if (name.length > sizeLimit) { name = truncate(name, sizeLimit); } return name; } // Truncates super long text that is way too lon... function truncate(string, amount) { if (string.length > amount) return string.substring(0, amount - 3) + '...'; else return string; }; // Gets dungeon player by userID function getDungeonPlayer(userID) { // Scans if player is in the raid collection var isLeader = false; // Gets if author is leader of raid var isCommander = false; // Gets if user is commander var playerID; // Grabs userID if this is part of raid var playerDungeon; // Grabs the player's dungeon if joined var player; // Gets player class // Totally seemingly not intensive test of for loops (Wish I knew) if (dungeonCollection !== undefined) { // Loops through every dungeon active for (let index = 0; index < dungeonCollection.length; index++) { const dungeonElement = dungeonCollection[index]; //console.log("dungeonElement | " + dungeonElement); // Loops through every player in that dungeon for (let index = 0; index < dungeonElement.players.length; index++) { const playerElement = dungeonElement.players[index]; //console.log("playerElement | " + playerElement); // If user is in one of the dungeon if (userID === playerElement.userID) { playerID = userID; player = playerElement; playerDungeon = dungeonElement; // Sets player's dungeon to this if (playerElement.leader) { isLeader = true; } if (playerElement.commander) { isCommander = true; } } } } } else { console.log("UNDEFINED DUNGEON COLLECTION" + dungeonCollection) } return { isLeader, isCommander, playerID, playerDungeon, player }; } // Async Waiting function sleep(time) { return new Promise((resolve, reject) => { setTimeout(resolve, time); }); } // Checks for if custom command is valid async function customCommands(newCommand, playerDungeon, author, command, args) { function items() { var array = []; newCommand.pickup.forEach(element => { array.push(element); }); } // Grabs the object if it exists console.log(`[Custom Commands] Type I | newCommand exists: ${newCommand !== undefined}`) if (newCommand !== undefined && newCommand.command) { if (playerDungeon.state === DungeonState.WAITING_FOR_USERS) return; console.log(`[Custom Commands] Type II | internal: ${newCommand.internal} | command: ${command}`); // If command is meant to be an internal, inscript command if (newCommand.internal) { if (newCommand.pickup) pickupItem(playerDungeon, newCommand, author, newCommand.pickup); } else { playerDungeon.dialogObj = newCommand; typingDialog(playerDungeon, newCommand, author); } } } // Executes custom commands by emote text function customCommandEmoteProcessor(playerDungeon, newCommand, author, returnElement, newMessage) { //console.log("[Emote Processor] Emote reaction custom command: " + JSON.stringify(returnElement, null, 4)); //console.log("[Emote Processor] newCommand: " + JSON.stringify(newCommand, null, 4)); console.log("[Emote Processor] newCommand.type = " + newCommand.type); switch (newCommand.type) { case "chest": switch (returnElement.emote) { // JSON : chest.options.unlock case "🔓": openChest(playerDungeon, newCommand[returnElement.command], author, false, newMessage, newCommand.embed) break; case "🔐": openChest(playerDungeon, newCommand[returnElement.command], author, true, newMessage, newCommand.embed); break; case "🔏": lockpickChest(playerDungeon, newCommand, author, newMessage, newCommand.embed); break; case "❌": leaveObject(playerDungeon, newCommand[returnElement.command], author); break; } break; case "door": switch (returnElement.emote) { case "🔐": openDoor(playerDungeon, newCommand[returnElement.command], author, returnElement, newMessage, newCommand.embed); break; case "🔏": lockpickDoor(playerDungeon, newCommand, author, newMessage, newCommand.embed); break; case "❌": leaveObject(playerDungeon, newCommand[returnElement.command], author); break; } break; } } // Adds item to inventory function addItem(playerDungeon, item, amount) { // If item exists in inventory console.log("[Add Item] " + amount); var i = playerDungeon.items.findIndex(i => i.name === item.name); console.log("[Item Amount Item Exists] i Equals " + i); // Item is internally handled if (item.internal) { switch (item.name.toLowerCase()) { case "crystals": addCrystalsToPartyMember(playerDungeon, amount); break; default: break; } } // If there is an item that exists in inventory if (i !== -1) { console.log("[Item Amount Start] " + playerDungeon.items[i].amount); playerDungeon.items[i].amount += item.amount; // Increments amount value console.log("[Item Amount Finish] " + playerDungeon.items[i].amount); // Disallows adding objects such as crystals } else if (!item.internal) { console.log("[Item Amount Wait] Created item for the first time."); var clonedItem = JSON.parse(JSON.stringify(item)); // Clones item in order to not actually increment the rooms.js item JSON data playerDungeon.items.push(clonedItem); } } // Removes item from party inventory (TODO: multi item stacks & only take one item) function removeItem(playerDungeon, item, amount) { let i = playerDungeon.items.findIndex( i => i.name === item.name ); console.log("[Remove Item] i Equals " + i + ` | For item: ${item.name}`); if (i !== -1) { if (playerDungeon.items[i].amount > 2) { console.log("[Item Amount Start] " + playerDungeon.items[i].amount); playerDungeon.items[i].amount -= item.amount; // Decrements amount value console.log("[Item Amount Finish] " + playerDungeon.items[i].amount); } else { console.log("[Item Amount Wait] Destroying item:\n" + playerDungeon.items.length); playerDungeon.items.splice(i, 1); console.log("[Item Amount Done] Finished Destroying item:\n" + playerDungeon.items.length); } console.log("[Remove Item] Successfully removed item " + item.name + " from " + playerDungeon.room.name) return true; } console.log("[Remove Item] Couldn't remove item!"); return false; } // Adds crystals to EACH player dungeon players (GAMBLE IS BEING USED TO ADD) function addCrystalsToPartyMember(playerDungeon, amount) { playerDungeon.players.forEach(element => { dataRequest.sendServerData("gambleWon", amount, element.userID); playerDungeon.crystalsGained += amount; }); } // Pickup Item async function pickupItem(playerDungeon, newCommand, author, items) { // Loops through all the items in array, and adds them into the item string var itemString = ""; console.log("[Pickup Item] JSON Data:\n" + JSON.stringify(items, null, 4)); for (let i = 0; i < items.length; i++) { const element = items[i].item; const item = rooms.items[element]; console.log("[Pickup Item] " + element + "\nJSON:\n" + JSON.stringify(item, null, 4)); // Emote processing var emoteText = ""; if (item.emote !== undefined && item.emote !== "") emoteText = `${item.emote} `; // Adds to item string & inventory var amount = 1; // Generates random number if array if (Array.isArray(items[i].amount)) amount = calcRandom.random(items[i].amount[0], items[i].amount[1]); // Treats it as an int instead else { // If amount variables exists, set it to so if (items[i].amount) amount = items[i].amount; } var newAmount; switch (element) { // Meant to have each player get a set amount of crystals case "crystals": newAmount = amount * playerDungeon.players.length; break; // Normal handling default: newAmount = amount; break; } itemString += `> **${emoteText}${item.name}** [${newAmount}x]\n`; addItem(playerDungeon, item, amount); } // Sends embed const embed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) .setColor(playerDungeon.room.color) .setTitle("Item Get!") .setDescription(`You picked up...\n${itemString}You can find it in the party's **!pinventory**.\nAny collected crystals or materials will be distributed evenly.`) client.channels.get(playerDungeon.room.channel).send({embed}); // Debug // console.log("DEBUG JSON DATA: " + JSON.stringify(item. null, 4)); await sleep(2500); playerDungeon.dialogObj = newCommand; playerDungeon.reroutedRooms.push([newCommand.moveFrom, newCommand.moveTo]); move(newCommand.moveTo, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } // Lock picks a chest async function lockpickChest(playerDungeon, newCommand, author, newMessage, embedData) { var random = calcRandom.gamble(50); var channel = client.channels.get(playerDungeon.room.channel); var successMessage = `:lock_with_ink_pen: **You have successfully lockpicked the chest!** The party has saved a key.`; var failedMessage = `:lock_with_ink_pen: **The unlock has failed.** Better luck next time!`; if (random) { if (!embedData) { if (!newMessage) channel.send(successMessage); } else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = successMessage; newMessage.edit({embed}); } await sleep(2500); var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); lootChest(playerDungeon, newCommand.unlock, author, newMessage, embedData); } else { if (!embedData) channel.send(failedMessage); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = failedMessage; newMessage.edit({embed}); } await sleep(2500); playerDungeon.dialogObj = newCommand.lockpick; move(newCommand.lockpick.moveToFail, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } } // Opens the chest async function openChest(playerDungeon, newCommand, author, useKey, newMessage, embedData) { var successMessage = `${author} used the **:key: Key** to open the chest.`; var failedMessage = "The party does not have a **:key: Key** to :closed_lock_with_key: unlock this chest."; console.log("[Open Chest] " + successMessage + " | " + failedMessage); if (useKey && playerDungeon.dialogObj.removeItem) { // Checks for key in inventory let removeKey = playerDungeon.items.findIndex( i => i.name === "Key" ); console.log("[Remove Key] " + removeKey); // If key exist if (removeKey !== -1) { removeItem(playerDungeon, rooms.items["key"]); if (!embedData) sendMessage(author.id, playerDungeon.room.channel, successMessage) else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocking") .setDescription("...") await newMessage.edit({embed}); embed.description = successMessage; newMessage.edit({embed}); await sleep(2500); } } else { if (!embedData) sendMessage(author.id, playerDungeon.room.channel, failedMessage); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = failedMessage; newMessage.edit({embed}); await sleep(2500); } return typingDialog(playerDungeon, playerDungeon.dialogObj, author); } } else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); } // Open chest lootChest(playerDungeon, newCommand, author, newMessage, embedData); } // Lockpicks door (lockpickChest IS VERY SIMILAR) async function lockpickDoor(playerDungeon, newCommand, author, newMessage, embedData) { var random = calcRandom.gamble(50); var channel = client.channels.get(playerDungeon.room.channel); var successMessage = `:lock_with_ink_pen: **You have successfully lockpicked the door!** The party has saved a key.`; var failedMessage = `:lock_with_ink_pen: **The unlock has failed.** Better luck next time!`; if (random) { if (!embedData) { if (!newMessage) channel.send(successMessage); } else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = successMessage; newMessage.edit({embed}); } await sleep(2500); var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); actualOpenDoor(playerDungeon, newCommand.unlock, author, newMessage, embedData); } else { if (!embedData) channel.send(failedMessage); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = failedMessage; newMessage.edit({embed}); } await sleep(2500); playerDungeon.dialogObj = newCommand.lockpick; move(newCommand.lockpick.moveToFail, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } } // Opens the door (TODO: ADD NO EMOTE COMPATIBILITY, AND REMOVE KEY) async function openDoor(playerDungeon, newCommand, author, returnElement, newMessage, embedData) { var hasAllItems = true; const noneString = "**> [None]**\n"; var itemsHasString = noneString; var itemsNotHaveString = noneString; console.log("[Open Door] JSON Data: " + JSON.stringify(newCommand, null, 4) + "\nreturnElement: " + JSON.stringify(returnElement, null, 4)); // Loops over all items for (let i = 0; i < newCommand.required.length; i++) { // Element = item object const objData = newCommand.required[i]; // Contains data from rooms.json const item = rooms.items[objData.item]; // Checks for key in inventory let itemRemove = playerDungeon.items.findIndex( i => i.name === item.name ); console.log("[Remove Item Door] " + "itemRemove = " + itemRemove); // If item exist var failed; if (itemRemove !== -1) { if (removeItem(playerDungeon, item, objData.amount)) { if (itemsHasString === noneString) itemsHasString = ""; // Erases None string itemsHasString += `**> ${item.emote} ${item.name} [${objData.amount}x]**\n`; } else { console.log(`[Remove Item Door] Remove fail at removeItem().`) failed = true; } } else { console.log(`[Remove Item Door] Couldn't find item name.`) failed = true; } if (failed) { hasAllItems = false; if (itemsNotHaveString === noneString) itemsNotHaveString = ""; // Erases None string itemsNotHaveString += `**> ${item.emote} ${item.name} [${objData.amount}x]**\n`; } } // Variables var successMessage = `${author} used:\n${itemsHasString} to open the door.`; var failedMessage = `The party does have:\n${itemsHasString}However, the party does not have:\n${itemsNotHaveString}\n...in order to :closed_lock_with_key: unlock the door.`; console.log("[Open Door] " + successMessage + " | " + failedMessage); // If no items needed if (hasAllItems) { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocking") .setDescription("...") await newMessage.edit({embed}); embed.description = successMessage; newMessage.edit({embed}); await sleep(2500); } else { // Send fail message if (!embedData) sendMessage(author.id, playerDungeon.room.channel, failedMessage); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle) .setDescription("...") await newMessage.edit({embed}); embed.description = failedMessage; newMessage.edit({embed}); await sleep(2500); } return typingDialog(playerDungeon, playerDungeon.dialogObj, author); } actualOpenDoor(playerDungeon, newCommand, author, newMessage, embedData); } function actualOpenDoor(playerDungeon, newCommand, author, newMessage, embedData) { if (newMessage) { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocking") .setDescription(`:unlock: The door has been opened!`) newMessage.edit({embed}); } playerDungeon.dialogObj = newCommand; playerDungeon.reroutedRooms.push([newCommand.moveFrom, newCommand.moveTo]); move(newCommand.moveTo, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } // Loots the chest (TODO: REMOVE GAMBLWON ON SENDSERVERDATA) async function lootChest(playerDungeon, newCommand, author, newMessage, embedData) { var channel = client.channels.get(playerDungeon.room.channel); var content = `:lock: **Opening chest.`; var openMessage = `:unlock: **You obtained :wind_blowing_face: Air!** This item will be distributed through the party at random.`; // Generates a random item drop group //console.log(JSON.stringify(newCommand, null, 4)); var randomDropGroup = calcRandom.random(0, newCommand.objects.length - 1); var randomItemGroup = calcRandom.random(0, newCommand.objects[randomDropGroup].length - 1) /* console.log("newCommand.objects: " + JSON.stringify(newCommand.objects, null, 4)); console.log("newCommand.objects.length: " + newCommand.objects.length); console.log("newCommand.objects[randomDropGroup]: " + JSON.stringify(newCommand.objects[randomDropGroup], null, 4));= console.log("newCommand.objects[0][0]: " + JSON.stringify(newCommand.objects[0][0], null, 4)); console.log("IF THIS DOESNT WORK, AHHH: " + newCommand.objects[0][0].name) */ console.log("[RNG] " + randomDropGroup + " " + randomItemGroup + " | " + newCommand.objects[randomDropGroup].length); var randomItem = newCommand.objects[randomDropGroup][randomItemGroup]; switch (randomItem.name) { case "crystals": if (randomItem["amount"]) { var amount = calcRandom.random(randomItem["amount"][0], randomItem["amount"][1]); openMessage = `:unlock: **You obtained <:crystals:460974340247257089> ${amount * playerDungeon.players.length} Crystals!** This item will be distributed through the party evenly.`; addCrystalsToPartyMember(playerDungeon, amount); } break; case "materials": openMessage = `:unlock: **You obtained materials that I have implemented!**`; break; default: console.warn("Message not in switch statement: " + randomItem.name) break; } if (!embedData || !newMessage) { var message = await channel.send(content + "**").catch(console.error); } else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocking") .setDescription(content + "**") newMessage.edit({embed}); } for (let index = 1; index < 3; index++) { var stringOfDots = ""; for (let i = 0; i < index; i++) { console.log("[Loot Chest] | Number of dots: " + index); stringOfDots += "."; } await sleep(750); console.log("[Loot Chest] " + content + stringOfDots + " | " + index); if (!embedData) await message.edit(content + stringOfDots + "**"); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocking") .setDescription(content + stringOfDots + "**") await newMessage.edit({embed}) } } await sleep(750); if (!embedData) message.edit(openMessage); else { var dungeonName = embedData.setAuthor.title.replace("${dungeon.room.name}", playerDungeon.room.name); var newImage = embedData.setAuthor.image.replace("${profile_pic}", client.user.avatarURL); const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(embedData.setTitle + ": Unlocked") .setDescription(openMessage) newMessage.edit({embed}); } await sleep(2500); // Moves user to empty chest room playerDungeon.dialogObj = newCommand; //console.log(JSON.stringify(newCommand, null, 4)); playerDungeon.reroutedRooms.push([newCommand.moveFrom, newCommand.moveTo]); move(newCommand.moveTo, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } // Leaves the chest alone async function leaveObject(playerDungeon, newCommand, author) { playerDungeon.dialogObj = newCommand; move(newCommand.moveTo, playerDungeon, true, author); playerDungeon.dialogObj = undefined; } // Passes all the user IDs function passDungeonUserIDs(playerDungeon) { var userIDArray = []; playerDungeon.players.forEach(element => { userIDArray.push(element.userID); }); console.log(userIDArray); return userIDArray; } // https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds function fmtMSS(s){ // accepts seconds as Number or String. Returns m:ss return( s - // take value s and subtract (will try to convert String to Number) ( s %= 60 ) // the new value of s, now holding the remainder of s divided by 60 // (will also try to convert String to Number) ) / 60 + ( // and divide the resulting Number by 60 // (can never result in a fractional value = no need for rounding) // to which we concatenate a String (converts the Number to String) // who's reference is chosen by the conditional operator: 9 < s // if seconds is larger than 9 ? ':' // then we don't need to prepend a zero : ':0' // else we do need to prepend a zero ) + s ; // and we add Number s to the string (converting it to String as well) } // Creates new dungeon, inits BotWaitingPlayers() async function NewDungeonSequence(channel) { console.log('Attempting to start a raid!'); // If bot summoning for a dungeon is free var newRoomID = rooms.rooms[calcRandom.random(0, rooms.rooms.length - 1)]; // calcRandom.random is inclusive for low + high console.log(newRoomID); var newRoom = rooms[newRoomID] var newDungeon; dungeonCollection.forEach(element => { if (element.room.name === newRoom.name) { return console.log("Raid already happening."); } }); // Decides on new dungeon newDungeon = new DungeonRaidInstance(newRoom); newDungeon.location = newDungeon.room.entrance; dungeonCollection.push(newDungeon); // Minutes var sleepTime = 2 * 60; BotWaitingPlayers(channel, newDungeon, sleepTime); } // Shows the join message for the specified dungeon async function BotWaitingPlayers(channel, dungeon, timer) { dungeon.startTimer = timer; var decrementTime = 1; // Seconds var newMessage; var textTimer = `⏰ ${fmtMSS(dungeon.startTimer)} min. left before leaving`; var crystalCost = 50; const botDialog = `*Psst.* Hey travelers, **${dungeon.room.name}** doesn't have any Ravagers outside guarding it. ` + `I could lockpick it, but it's gunna cost you **<:crystals:460974340247257089> 50**, for each of you.`; const description = `In order to join the dungeon, you have to pay **<:crystals:460974340247257089> ${crystalCost}**. \nReact with <:crystals:460974340247257089> to enter, and the crystals will be subtracted from your account.`; client.user.setStatus('online'); client.user.setActivity(`${textTimer} ${dungeon.room.name}.`) dungeon.state = DungeonState.WAITING_FOR_USERS; const embed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) .setColor(dungeon.room.color) .setDescription(description) .setFooter(textTimer + ".") newMessage = await client.channels.get(channel).send(botDialog, {embed}); await newMessage.react('460974340247257089'); // Collector for emotes const collector = newMessage.createReactionCollector( (reaction, user) => reaction.emoji.name === 'crystals' && user.id !== client.user.id , { time: timer * 1000 }); // Removes crystals collector.on("collect", async reaction => { var user = reaction.users.last(); //console.log(reaction.emoji.name) //console.log("Added new user: " + user.username) var attacker = String(dataRequest.loadServerData("userStats", user.id)); var attackerWallet = parseFloat(attacker.split(",")[6]); if (attackerWallet >= crystalCost) { dataRequest.sendServerData("buyDrink", crystalCost, user.id); joinMessage(user, channel, dungeon.room); } else { newMessage.channel.send(`:x: ${user} You don't have enough crystals to pay me. Come back later, when you do. I ain't doing this for free.`); } }); // Clears reactions collector.once("end", async collecter => { console.log("[Reaction Options] Ended collector, clearing emotes and sending timing out message if needed."); newMessage.clearReactions(); }); // Creates description and displays it (join, no-join) function timerFunction() { setTimeout(async () => { if (dungeon.state !== DungeonState.WAITING_FOR_USERS) return; const timeInveral = 30; var quarterMinuteRemainder = dungeon.startTimer % timeInveral; console.log("[Timer Function] timer: " + dungeon.startTimer + " => " + (dungeon.startTimer - decrementTime) + " | " + quarterMinuteRemainder) // If it's been 30 seconds (divisible without any remainder, & not exactly starting time) if (quarterMinuteRemainder == 0 && dungeon.startTimer !== timer) { var descriptionString = ""; var finished = `The travelers entered the dungeon. You can no longer join.` var textTimer = `⏰ ${fmtMSS(dungeon.startTimer)} min. left before leaving`; var footerText = textTimer + "."; if (dungeon.startTimer > 0) { descriptionString = description; client.user.setActivity(`${textTimer} ${dungeon.room.name}.`); } else { descriptionString = finished; footerText = "⏰ You can no longer join." } const embed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) .setColor(dungeon.room.color) .setDescription(descriptionString) .setFooter(`${footerText}`) newMessage.edit(botDialog, embed); } else if (dungeon.startTimer === timer) { console.log("[Debug] " + dungeon.startTimer + " | " + timer); } dungeon.startTimer -= decrementTime; if (dungeon.startTimer > -1) timerFunction(); else { if (dungeon.state === DungeonState.WAITING_FOR_USERS && dungeon.players.length > 0) { console.log(dungeon.players.length); BotActive(dungeon); } else { BotInactive(dungeon); } } }, decrementTime * 1000); } timerFunction(); } // Sends closing dungeon messages async function BotActive(dungeon) { //sendMessage(channel, `${dungeon.room.name} has closed, trapping the travelers inside the dungeon. \ //\n\n***YOU NO LONGER CAN !JOIN***`); if (dungeon.players.length > 0) { await sendEmbedLocation(dungeon); client.channels.get(dungeon.room.channel).send(`**<#${dungeon.room.channel}>** got closed behind us. I guess we should start venturing deeper.` + `\nUse **!help ${client.user}** to get all the commands you are able to use here, if you need anythin'.`); client.user.setStatus('away'); dungeon.state = DungeonState.INSIDE_DUNGEON; } else { BotInactive(dungeon); } // Starts timer var decrementTime = 1000; // 1 every second dungeon.timer = 10 * 60 * 1000; sendTimerInChat(dungeon); // Decrements dungeon timer and displays it in chat. function displayTimer() { setTimeout(async () => { // Makes sure dungeon is still there if (dungeon) { if (dungeon.state === DungeonState.INSIDE_DUNGEON) { dungeon.timer -= decrementTime; // Timers if (dungeon.timer > 0) { // Events var oneMinuteRemainder = dungeon.timer % (1 * 60 * 1000); if (oneMinuteRemainder == 0) { sendTimerInChat(dungeon); client.user.setActivity(`⏰ ${fmtMSS(dungeon.timer / 1000)} min. left | ${dungeon.room.name}`).then(presence => console.log(`Activity set to ${presence.game ? presence.game.name : 'none'}`)) .catch(console.error); } // Recursion displayTimer(); // Leaves dungeon } else { await sendTimerInChat(dungeon); await client.channels.get(dungeon.room.channel).send("An overwhelming amount of Ravagers came back in," + " and the party escaped, however not completing the dungeon.\n\n***THE DUNGEON HAS FAILED TO BE COMPLETED IN TIME.***"); await client.channels.get(process.env.TEST_CHANNEL_ID).send("Alright travelers, we didn't make it in time, but we managed to get out alive, I guess. Better luck next time."); BotInactive(dungeon); } } } }, decrementTime); } displayTimer(); } // Inactive async function BotInactive(dungeon) { // Removes roles const guild = client.guilds.get(process.env.SANCTUM_ID); if (dungeon.players.length > 0) { for (let i = 0; i < dungeon.players.length; i++) { const element = dungeon.players[i]; // Deletes role let role = client.guilds.get(process.env.SANCTUM_ID).roles.find(role => role.name === `${dungeon.room.name}: Raid Party`); await guild.members.get(element.userID).removeRole(role); } } // Removes dungeon by index let removeDungeon = dungeonCollection.findIndex( d => d.room.name === dungeon.room.name ); dungeonCollection.splice(removeDungeon); if (dungeonCollection.length < 1) client.user.setStatus('invisible'); } // Sends timer message function sendTimerInChat(newDungeon) { console.log("[Timer] " + newDungeon.timer + " | " + newDungeon.startTimer); const guild = client.guilds.get(process.env.SANCTUM_ID); var role = guild.roles.find(role => role.name === `${newDungeon.room.name}: Raid Party`); if (newDungeon.state !== DungeonState.WAITING_FOR_USERS) return client.channels.get(newDungeon.room.channel).send(`:alarm_clock: ${role} has ${fmtMSS(newDungeon.timer / 1000)} min. left to complete the dungeon.`); else return client.channels.get(newDungeon.room.channel).send(`:alarm_clock: ${role} has ${fmtMSS(newDungeon.startTimer)} min. left before the dungeon starts.`); } // Ends dialog by pausing then sending your location again. async function endDialog(newCommand, playerDungeon, overrideLastCommand) { if (newCommand.lastCommand || overrideLastCommand) { console.log("[End Dialog] Doing End Dialog Sequence.") playerDungeon.dialogObj = undefined; await sleep(1000); if (newCommand.lastCommand) sendEmbedLocation(playerDungeon); } } // Does typing dialog for the NPC async function typingDialog(playerDungeon, newCommand, author, newMessage) { if (playerDungeon.isTyping) { console.log("isTyping true, cancelling"); return false }; console.log("[Typing Dialog]\n" + JSON.stringify(newCommand, null, 4)); playerDungeon.isTyping = true; var firstTime; var range = 250; var channel = client.channels.get(playerDungeon.room.channel); // Types out all the dialog if (newCommand.descriptions) { for (let index = 0; index < newCommand.descriptions.length; index++) { var temp = ""; const element = newCommand.descriptions[index]; if (element.text !== undefined) { var title = "" if (!firstTime) { title = `**${playerDungeon.dialogObj.name}**\n`; firstTime = true; } temp = element.text.replace("${leader}", author); var waitTimerRange = calcRandom.random(element.waitBegin - range, element.waitBegin + range); // Debug purposes only //waitTimerRange = 10; sendTypingMessage(channel, title + temp, waitTimerRange); await sleep(waitTimerRange + element.waitEnd); endDialog(newCommand, playerDungeon); } } } playerDungeon.isTyping = false; reactionOptions(playerDungeon, newCommand, author, newMessage); } // Does reactions on a message, then uses them as options async function reactionOptions(playerDungeon, newCommand, author, newMessage) { var channel = client.channels.get(playerDungeon.room.channel); var range = 250; var returnElement = false; // Reactions (if they exist via text or have an options JSON field) if (newCommand.optionsDescription || newCommand.options) { var waitTimerRange; var isEmbed = false; waitTimerRange = 10; // Embed and text varients if (!newCommand.embed) { waitTimerRange = calcRandom.random(newCommand.optionsDescription.waitBegin - range, newCommand.optionsDescription.waitBegin + range); newMessage = await sendTypingMessage(channel, newCommand.optionsDescription.text, waitTimerRange) } else { await sleep(newCommand.embed.waitBegin); // Specific formattings if (newCommand.embed.formatting === "author_title_image_desc") { var newImage = client.user.avatarURL; const embed = new Discord.RichEmbed() .setAuthor("Ghost", newImage) .setColor(playerDungeon.room.color) .setTitle(newCommand.embed.setTitle) .setDescription(newCommand.embed.setDescription) newMessage = await channel.send({embed}); channel.stopTyping(true); } else { console.error("[Reaction Options] Embed was tried, but no vaid formatting!"); } await sleep(newCommand.embed.waitEnd) } // Reacts for (let i = 0; i < newCommand.options.length; i++) { const element = newCommand.options[i]; console.log("[Reaction Options] Emote: " + element.emote + " | newMessage: " + newMessage); await newMessage.react(element.emote); } // Collects emotes and reacts upon the reaction (120 seconds) const collector = newMessage.createReactionCollector( (reaction, user) => (getDungeonPlayer(user.id).isLeader || getDungeonPlayer(user.id).isCommander) && (newCommand.options.some(option => option.emote === reaction.emoji.name)), { time: 120 * 1000 }); var endedOnReact = false; // Collect collector.once("collect", async reaction => { // Solely for players if (playerDungeon.dialogObj) { const chosen = reaction.emoji.name; newCommand.options.forEach(element => { if (element.emote === chosen) { returnElement = element; // Grabs the object if it exists if (playerDungeon.dialogObj[element.command] && playerDungeon.dialogObj[element.command].command) { typingDialog(playerDungeon, playerDungeon.dialogObj[element.command], author, newMessage); console.log("Using grab object if exists"); hasAlreadyTriggered = true; } } }); } customCommandEmoteProcessor(playerDungeon, newCommand, author, returnElement, newMessage, reaction); endedOnReact = true; collector.stop(); /* console.log(newMessage.reactions); newMessage.reactions.forEach(element => { if (element.users.id !== client.user.id) newMessage.reactions.remove(element); }); */ // Debug //console.log("[Reaction Options] Return emote from reactionOptions: " + returnElement.emote); //console.log("[Reaction Options] newCommand @ reactions function: " + JSON.stringify(newCommand)); //console.log("[Reaction Options] returnElement: " + JSON.stringify(returnElement)); }); // End trigger collector.once("end", collecter => { console.log("[Reaction Options] Ended collector, clearing emotes and sending timing out message if needed."); newMessage.edit(newMessage.content); newMessage.clearReactions(); if (!endedOnReact) { sendMessage(playerDungeon.room.channel, ":x: **Timed Out** The emote reaction request timed out after 2 minutes."); endDialog(newCommand, playerDungeon, true); } }); } } // Sends messages in periods of time async function sendTypingMessage(channel, message, waitTimer) { channel.startTyping() await sleep(waitTimer); channel.stopTyping(true); return channel.send(message); } // Just here for placeholder, do not use async function sendEmbedMessage(channel, embedData, waitTimer) { channel.startTyping(); await sleep(waitTimer); channel.stopTyping(true); const embed = new Discord.RichEmbed(); return channel.send(embed); } // Send message handler function sendMessage(userID, channelID, message) { // Handle optional first argument (so much for default arugments in node) if (message === undefined) { message = channelID; channelID = userID; userID = null; } // Utility trick (@userID with an optional argument) if (userID != null) { message = "<@" + userID + "> " + message; } // Sends message (needs client var, therefore I think external script won't work) client.channels.get(channelID).send(message); } // You may use cron normally cron.schedule('30 */3 * * *', function() { NewDungeonSequence(process.env.TEST_CHANNEL_ID); }); // Join Message function joinMessage(user, channel, room) { var newDungeon; var firstUserGiveLeader = false; dungeonCollection.forEach(element => { if (element.room === room) newDungeon = element; }); // If dungeon doesn't exist if (!newDungeon) { if (room.undefined != undefined) { if (room.undefined == "") return sendMessage(user.id, channel.id, `:x: You need to specify what dungeon you wish to join.`); return sendMessage(user.id, channel.id, `:x: There is no dungeon named ${room.undefined}.`); } return sendMessage(user.id, channel.id, ":x: This dungeon does not exist."); } else { if (newDungeon.players.length == 0) { firstUserGiveLeader = true; } } if (newDungeon.state !== DungeonState.WAITING_FOR_USERS) { return sendMessage(user.id, channel.id, ":x: This dungeon's closed currently."); } // Find if user is in the Raid Party var userExists = newDungeon.players.find( player => player.userID === user.id ); // If user doesn't exist in the list, join message if (!userExists) { let member = client.guilds.get(process.env.SANCTUM_ID).members.get(user.id); // Adds role to use let role = client.guilds.get(process.env.SANCTUM_ID).roles.find(role => role.name === `${newDungeon.room.name}: Raid Party`); member.addRole(role); // Creates new player for array var playerRole = getFactionID(member); var className = getCombatClassName(member); var newPlayer = new PlayerClass(user.id, playerRole, className, ":new:", firstUserGiveLeader); newDungeon.players.push(newPlayer); // Sends messages confirming it client.channels.get(newDungeon.room.channel).send(`<@${user.id}> welcome to the ${newDungeon.room.name} dungeon raid! Use **!party** to check the members.`); //message.channel.send(`<@${user.id}> you have **!join**ed the ${newDungeon.room.name} dungeon raid! Use **!party** to check the members.`); // Else send error already joined } else { //client.channels.get(newDungeon).send(`<@${user.id}> you already **!join**ed the ${newDungeon.room.name} dungeon raid. Use **!party** to check the members.`); } } // Leave Message async function leaveMessage(message) { var dungeonPlayer = getDungeonPlayer(message.author.id); // Sends messages confirming it if (dungeonPlayer.playerDungeon === undefined) { var confirmMessage = await message.channel.send(`<@${message.author.id}> you aren't in a dungeon raid in my books, but are you sure you want to force leave all dungeon raids?\n*This will remove all dungeon raid roles.*`); // Reacts await confirmMessage.react('✅'); await confirmMessage.react('❌'); const collector = confirmMessage.createReactionCollector( (reaction, user) => (reaction.emoji.name === '✅' || reaction.emoji.name === '❌') && user.id === message.author.id , { time: 15 * 1000 }); var react = ""; // Collect collector.once("collect", async reaction => { endedOnReact = true; react = reaction.emoji.name; collector.stop(); }); // End trigger collector.once("end", async collecter => { console.log("[Reaction Options] Ended collector, clearing emotes and sending timing out message if needed."); confirmMessage.edit(confirmMessage.content); confirmMessage.clearReactions(); if (!endedOnReact) return message.channel.send(":x: **Timed Out**: The emote reaction request timed out after 15 seconds."); if (react === '❌') return confirmMessage.edit(`${message.author} Cancelled removing dungeon raid roles.`); // Force deletes all dungeons that exist for (let i = 0; i < rooms.rooms.length; i++) { const element = rooms.rooms[i]; const name = rooms[element].name; let role = client.guilds.get(process.env.SANCTUM_ID).roles.find(role => role.name === `${name}: Raid Party`); await message.member.removeRole(role); } await confirmMessage.edit(`${message.author} Removed all dungeon raid roles.`) }); } else { var confirmMessage = await message.channel.send(`<@${message.author.id}> are you sure you want to leave the ${dungeonPlayer.playerDungeon.room.name} dungeon raid?`); // Reacts await confirmMessage.react('✅'); await confirmMessage.react('❌'); // Collects emotes and reacts upon the reaction (15 seconds) const collector = confirmMessage.createReactionCollector( (reaction, user) => (reaction.emoji.name === '✅' || reaction.emoji.name === '❌') && user.id === message.author.id , { time: 15 * 1000 }); var endedOnReact = false; var react = ""; // Collect collector.once("collect", async reaction => { // Solely for players endedOnReact = true; react = reaction.emoji.name; collector.stop(); }); // End trigger collector.once("end", collecter => { console.log("[Reaction Options] Ended collector, clearing emotes and sending timing out message if needed."); confirmMessage.edit(confirmMessage.content); confirmMessage.clearReactions(); if (!endedOnReact) return sendMessage(message.channel, ":x: **Timed Out**: The emote reaction request timed out after 15 seconds."); if (react === '❌') return confirmMessage.edit(`${message.author} Cancelled leaving the ${dungeonPlayer.playerDungeon.room.name}.`); // Sends confirmation message message.channel.send(`${message.author} has left the dungeon.`) // Deletes role let role = client.guilds.get(process.env.SANCTUM_ID).roles.find(role => role.name === `${dungeonPlayer.playerDungeon.room.name}: Raid Party`); message.member.removeRole(role); // Removes user from dungeon dungeonPlayer.playerDungeon.players = dungeonPlayer.playerDungeon.players.filter(obj => obj.userID !== message.author.id) // Chooses random leader if leader left console.log("Is there a leader: " + dungeonPlayer.playerDungeon.players.find(l => l.leader) === true); if (dungeonPlayer.playerDungeon.players.find(l => l.leader) === undefined) { for (let i = 0; i < dungeonPlayer.playerDungeon.players.length; i++) { const element = dungeonPlayer.playerDungeon.players[i]; console.log(element); if (element.commander) { element.commander = false; element.leader = true; message.channel.send(`<@${element.userID}> has been promoted to leader!`); break; } } // If there is no commander, promote first person on list for (let i = 0; i < dungeonPlayer.playerDungeon.players.length; i++) { const element = dungeonPlayer.playerDungeon.players[i]; element.commander = false; element.leader = true; message.channel.send(`<@${element.userID}> has been promoted to leader!`); break; } } // Destroy dungeon if last player if (dungeonPlayer.playerDungeon.players.length === 0) { BotInactive(dungeonPlayer.playerDungeon); } }); } } // Checks if direction can be done (skips anything other than Directions.XXXX) function checkValidMovement(direction, dungeon, silent) { var directionFailure; switch (direction) { case Directions.NORTH: if (dungeon.location.connections.north === undefined) { directionFailure = "**north**"; } break; case Directions.EAST: if (dungeon.location.connections.east === undefined) { directionFailure = "**east**"; } break; case Directions.SOUTH: if (dungeon.location.connections.south === undefined) { directionFailure = "**south**"; } break; case Directions.WEST: if (dungeon.location.connections.west === undefined) { directionFailure = "**west**"; } break; } if (directionFailure) { if (!silent) { var deniedMessage = `:x: ${user} You can't go ${directionFailure}, please choose a valid path.`; client.channels.get(dungeon.room.channel).send(deniedMessage); } return false; } return true; } // Moves in the dungeon // TODO ITEM async function move(direction, dungeon, isPartyLeader, user, moveToOverride) { if (dungeon === undefined) return console.log("Dungeon doesn't exist!"); var channel = client.channels.get(dungeon.room.channel); var newRoom; if (!isPartyLeader) return channel.send(`:x: ${user} You need to be a Party Leader or be **!promoted** to Commander in order to move.`); if (!checkValidMovement(direction, dungeon)) return; // Says something if command can be done switch (direction) { case Directions.NORTH: if (dungeon.location.connections.north !== undefined) { newRoom = dungeon.location.connections.north; //channel.send(":white_check_mark: **Command accepted.** Following the party **north**."); } break; case Directions.EAST: if (dungeon.location.connections.east !== undefined) { newRoom = dungeon.location.connections.east; //channel.send(":white_check_mark: **Command accepted.** Following the party **east**."); } break; case Directions.SOUTH: if (dungeon.location.connections.south !== undefined) { newRoom = dungeon.location.connections.south; //channel.send(":white_check_mark: **Command accepted.** Following the party **south**."); } break; case Directions.WEST: if (dungeon.location.connections.west !== undefined) { newRoom = dungeon.location.connections.west; //channel.send(":white_check_mark: **Command accepted.** Following the party **west**."); } break; default: // If uses a custom moveTo statement if (moveToOverride) { newRoom = moveToOverride; // Else if no override } else if (dungeon.dialogObj.moveTo) { newRoom = dungeon.dialogObj.moveTo; } break; } // Reroutes rooms dungeon.reroutedRooms.forEach(element => { console.log("[Move Reroute Rooms] " + element[0] + " " + element[1] + " " + newRoom + " " + (element[0] === newRoom)); if (element[0] === newRoom) newRoom = element[1]; }); console.log("[New Room]: " + newRoom + " | [Old Room]: " + dungeon.location.name); switch (newRoom) { case "__END": await client.channels.get(process.env.TEST_CHANNEL_ID).send(`The **${dungeon.room.name}** dungeon has been completed!`); await channel.send(`Congratulations! The party completed the **${dungeon.room.name}** dungeon with **${fmtMSS(dungeon.timer / 1000)} min. left**!`); BotInactive(dungeon); break; default: dungeon.location = dungeon.room[newRoom]; sendEmbedLocation(dungeon); break; } console.log("[New Location]: " + dungeon.location.name); } // Movement option text function movementOptions(dungeon) { var temp = ""; if (dungeon.location.northtext !== "" && dungeon.location.northtext) temp += `:arrow_up: ${dungeon.location.northtext}\n`; if (dungeon.location.easttext !== "" && dungeon.location.easttext) temp += `:arrow_right: ${dungeon.location.easttext}\n`; if (dungeon.location.southtext !== "" && dungeon.location.southtext) temp += `:arrow_down: ${dungeon.location.southtext}\n`; if (dungeon.location.westtext !== "" && dungeon.location.westtext) temp += `:arrow_left: ${dungeon.location.westtext}\n`; return temp; } async function sendInventory(playerDungeon, pageNum) { var items = ""; // Creates an array that seperates 10 items into seperate arrays each var groupedArr = createGroupedArray(playerDungeon.items, 5); // Sets a default page num, or makes it human readable if (pageNum === undefined) pageNum = 0; else { pageNum--; if (pageNum < 0) pageNum = 0; if (groupedArr.length < pageNum) pageNum = groupedArr.length; } // Checks if page number is valid if (pageNum + 1 > groupedArr.length) { // If it's longer than actual length, but isn't just an empty inventory if (!groupedArr.length === 0) return; } // Grabs item in loop, parses it, then adds it to "items" variable if (groupedArr[pageNum]) { for (let index = 0; index < groupedArr[pageNum].length; index++) { // Grabs an item, from a page index const element = groupedArr[pageNum][index]; // Makes if there is an emote, it'll add an extra space var emoteText = ""; if (element.emote) emoteText = element.emote + " "; // Adds it in items += `> ${emoteText}**${element.name}** [${element.amount}x] ${element.info}\n`; } } // No items message to fill field if (items === "") items = "There are no items in the party's inventory."; // To make the message of "Page 1/0" with no items in !pinventory not happen var moddedLength = groupedArr.length; if (moddedLength < 1) moddedLength = 1; const embed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) .setColor(playerDungeon.room.color) .addField(`Party Inventory (Page ${pageNum + 1}/${moddedLength})`, items, true) //.setDescription(`${items}`) client.channels.get(playerDungeon.room.channel).send({embed}); } // Sends location into chat (non-embed) function sendLocation(dungeon, author) { var temp = `Current Room: **${dungeon.location.name}**\n\n`; temp += location(dungeon); temp += "\n\n"; temp += movementOptions(dungeon); // Sends message var channel = client.channels.get(dungeon.room.channel); channel.send(author + " " + temp); } // Sends location into chat (embed) async function sendEmbedLocation(dungeon) { if (dungeon.directionalMessageID) { dungeon.directionalCollector.stop(); } var channel = client.channels.get(dungeon.room.channel); // Does image for background (future map) const imageEmbed = new Discord.RichEmbed() //.setAuthor("Ghost", client.user.avatarURL) .setColor(dungeon.room.color) .setImage(dungeon.location.image_url) await channel.send({embed: imageEmbed}); // Does text dialog const textEmbed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) .setTitle(`${dungeon.location.name}`) .setColor(dungeon.room.color) .setDescription(`${location(dungeon)}\n\n${movementOptions(dungeon)}`) var newMessage = await channel.send({embed: textEmbed}); // Collects emotes and reacts upon the reaction (120 seconds) var options = ['⬆', '⬇', '⬅', '➡'] const collector = newMessage.createReactionCollector( (reaction, user) => options.includes(reaction.emoji.name) && user.id !== client.user.id); // Reacts for (let i = 0; i < options.length; i++) { const element = options[i]; console.log("[Reaction Options] Emote: " + element + " | newMessage: " + newMessage); await newMessage.react(element); } // Collect collector.once("collect", async reaction => { const user = reaction.users.last(); var moveSuccessful; switch (reaction.emoji.name) { case '⬆': moveSuccessful = move(Directions.NORTH, dungeon, (getDungeonPlayer(user.id).isLeader || getDungeonPlayer(user.id).isCommander), user, false); break; case '⬇': moveSuccessful = move(Directions.SOUTH, dungeon, (getDungeonPlayer(user.id).isLeader || getDungeonPlayer(user.id).isCommander), user, false); break; case '⬅': moveSuccessful = move(Directions.WEST, dungeon, (getDungeonPlayer(user.id).isLeader || getDungeonPlayer(user.id).isCommander), user, false); break; case '➡': moveSuccessful = move(Directions.EAST, dungeon, (getDungeonPlayer(user.id).isLeader || getDungeonPlayer(user.id).isCommander), user, false); break; } collector.stop(); }); // End trigger collector.once("end", collecter => { console.log("[Reaction Options] [sendEmbedLocation] Ended collector, clearing emotes and sending timing out message if needed."); newMessage.edit(newMessage.content); }); } // Sends location descriptions (Text) function location(dungeon) { var temp = ""; for (let index = 0; index < dungeon.location.descriptions.length; index++) { const element = dungeon.location.descriptions[index]; if (element.text !== undefined) { temp += element.text + " "; } } //console.log(temp); return temp; } // Sends party members (Embed) function partyMembers(dungeon, pageNum) { // Updates players updatePlayers(); var playerString = ""; var userInfo = ""; //console.log("0: " + pageNum); if (pageNum === undefined) pageNum = 0; else pageNum--; //console.log("1: " + pageNum) var groupedArr = createGroupedArray(dungeon.players, 9); //console.log(groupedArr); //console.log("\n\nGROUPEDARR[PAGENUM]: " + groupedArr[pageNum] + "\n\n") if (pageNum + 1 > groupedArr.length) return; // If it's longer than actual length if (groupedArr[pageNum] !== undefined) { for (let index = 0; index < groupedArr[pageNum].length; index++) { //const user = dungeon.players[index]; const element = groupedArr[pageNum][index]; playerString += `${element.factionEmote}${element.badge} ${playerName(element.userID)}\n`; userInfo += `${element.combatClassEmote} ${element.combatClass} ${element.isLeaderText}\n`; //console.log("playerString:\n" + playerString); //console.log("userInfo:\n " + userInfo); } } if (playerString == "") playerString = "None."; if (userInfo == "") userInfo = "None."; const embed = new Discord.RichEmbed() .setAuthor("Ghost", client.user.avatarURL) //.setTitle("Hell's Gate: Dungeon Raid") /* * Alternatively, use "#00AE86", [0, 174, 134] or an integer number. */ .setColor(dungeon.room.color) .setTitle(`${dungeon.location.name}`) .setDescription(`The **!party** is currently on floor ${dungeon.location.floor}. The leader can **!promote** users so they can interact with the dungeon.`) //.setImage("https://i.imgur.com/WyI8YuR.png") //.setThumbnail("https://i.imgur.com/BZgLV7w.png") //.setThumbnail("https://i.imgur.com/BlQTi94.jpg") .addField(`Party Members (Page ${pageNum + 1}/${groupedArr.length})`, playerString, true) .addField("User Info", `${userInfo}`, true) client.channels.get(dungeon.room.channel).send({embed}); } // Promote user to become leader function promoteCommander(author, mentionedMember, leader, channelID) { if (mentionedMember !== undefined ) { if (leader) { var dungeonPlayer = getDungeonPlayer(mentionedMember.id); if (dungeonPlayer.player) { if (author.id !== mentionedMember.id) { if (!dungeonPlayer.isCommander) { sendMessage(channelID, `:white_check_mark: ${author} has been **!promote**d ${mentionedMember} to commander status!`); dungeonPlayer.player.commander = true; } else { sendMessage(channelID, `:x: ${author} **Command denied.** User already has been **!promote**d.`); } } else { sendMessage(channelID, `:x: ${author} **Command denied.** You cannot promote yourself.`); } } else sendMessage(channelID, `:x: ${author} **Command denied.** The user being promoted has to be in the party.`); } else sendMessage(channelID, `:x: ${author} **Command denied.** You need to be a leader in order to **!promote** others.`); } else { temp = author + " !promote (!pr)\n```If you're the leader, you can promote others to be a commander with you in order to move around.```" sendMessage(channelID, temp); } } // Promote user to become leader function demoteCommander(author, mentionedMember, leader, channelID) { if (mentionedMember !== undefined ) { if (leader) { var dungeonPlayer = getDungeonPlayer(mentionedMember.id); if (dungeonPlayer.player) { if (author.id !== mentionedMember.id) { if (dungeonPlayer.isCommander) { sendMessage(channelID, `:white_check_mark: ${author} has **!demote**d ${mentionedMember} to normal status.`); dungeonPlayer.player.commander = false; } else { sendMessage(channelID, `:x: ${author} **Command denied.** User already has been **!demote**d.`); } } else { sendMessage(channelID, `:x: ${author} **Command denied.** You cannot promote yourself.`); } } else sendMessage(channelID, `:x: ${author} **Command denied.** The user being demoted has to be in the party.`); } else sendMessage(channelID, `:x: ${author} **Command denied.** You need to be a leader in order to **!demote** others.`); } else { temp = author + " !demote (!de)\n```If you're the leader, you can demote others if needed if they are a commander.```" sendMessage(channelID, temp); } } // Transfer party leadership async function transferLeader(message, toUser) { // From and to user transfer if (toUser === undefined) return message.channel.send(`${message.author} I don't know that user, please try again with someone I can recognize, thanks.`); fromUser = getDungeonPlayer(message.author.id); newUser = getDungeonPlayer(toUser.id); if (newUser.playerDungeon !== undefined && fromUser.playerDungeon !== undefined && newUser.playerDungeon === fromUser.playerDungeon && fromUser.player.leader && message.author.id !== toUser.id) { // Sends messages confirming it var confirmMessage = await message.channel.send(`:warning: <@${message.author.id}> are you sure you want to give leadership to ${toUser}?\n*You'll be given Commander status afterwards.*`); // Reacts await confirmMessage.react('✅'); await confirmMessage.react('❌'); const collector = confirmMessage.createReactionCollector( (reaction, user) => (reaction.emoji.name === '✅' || reaction.emoji.name === '❌') && user.id === message.author.id , { time: 15 * 1000 }); var react = ""; var endedOnReact; // Collect collector.once("collect", async reaction => { endedOnReact = true; react = reaction.emoji.name; collector.stop(); }); // End trigger collector.once("end", async collecter => { console.log("[Reaction Options] Ended collector, clearing emotes and sending timing out message if needed."); confirmMessage.edit(confirmMessage.content); confirmMessage.clearReactions(); if (!endedOnReact) return sendMessage(message.channel, ":x: **Timed Out**: The emote reaction request timed out after 15 seconds."); if (react === '❌') return confirmMessage.edit(`:x: ${message.author} Cancelled giving leadership to ${toUser}.`); fromUser.player.leader = false; fromUser.player.commander = true; newUser.player.leader = true; newUser.player.commander = false; await confirmMessage.edit(`:white_check_mark: ${message.author} has transferred leadership tp ${toUser}!`) }); } else { if (newUser.playerDungeon !== fromUser.playerDungeon) { return message.channel.send(`:x: ${message.author} The traveler must be in the party, or else I can't do anything about it.`); } if (!fromUser.player.leader) { return message.channel.send(`:x: ${message.author} You need to be a leader, in order to transfer your leadership.`) } if (message.author.id === toUser.id) { return message.channel.send(`:x: ${message.author} You cannot give yourself leadership if you are already the leader.`) } } } // Log our bot in (change the token by looking into the .env file) client.login(process.env.GHOST_TOKEN);