const { SlashCommandBuilder, Interaction, EmbedBuilder, PermissionFlagsBits, ButtonBuilder, ButtonStyle, ActionRowBuilder, StringSelectMenuBuilder, } = require("discord.js"); const ProfileModel = require("../../models/profileSchema"); const NexusGame = require("../../models/nexusGame"); const GuildConfigs = require("../../models/guildConfiguration"); const handleCooldowns = require("../../utils/handleCooldowns"); const mapTypes = { plains: 0, lake: 1, mansion: 2, forest: 3, treasureChest: 4, cave: 5, tower: 6, portal: 7, healingFountain: 8, cartographerDesk: 9, enchantedGarden: 10, }; const items = [ { name: "sword", value: 0 }, { name: "bow", value: 1 }, { name: "shield", value: 2 }, { name: "binoculors", value: 3 }, { name: "trap disarm kit", value: 4 }, { name: "medical kit", value: 5 }, { name: "terrain changer orb", value: 6 }, { name: "terrain destroyer crystal", value: 7 }, { name: "terrain swapper amulet", value: 8 }, { name: "map", value: 9 }, { name: "divine intervention scroll", value: 10 }, { name: "serenity pearl", value: 11 }, { name: "revenge rune", value: 12 }, { name: "divine shield", value: 13 }, { name: "lifesteal dagger", value: 14 }, { name: "fate shifter amulet", value: 15 }, { name: "celestial beacon", value: 16 }, { name: "phoenix feather", value: 17 }, ]; const BUTTON_IDS = { JOIN: "nexusarena_join", START: "nexusarena_start", PLAYERS: "nexusarena_players", }; const BUTTON_EMOJIS = { JOIN: "🎟️", START: "🗺️", PLAYERS: "🙋", }; var colors = require("colors"); colors.enable(); module.exports = { data: new SlashCommandBuilder() .setName("nexus-arena") .setDescription("Play a survival battlefield game with your friends!") .addSubcommand((command) => command .setName("new") .setDescription("Start a new game!") .addIntegerOption((option) => option .setName("map-size") .setDescription( "Minimum size is 2 by 2, maximum is 8 by 8, default is 4 by 4" ) .setMinValue(2) .setMaxValue(8) ) ) .addSubcommand((command) => command.setName("cancel").setDescription("Cancel a game you started") ) .addSubcommand((command) => command.setName("help").setDescription("Get more information of how the game works!") ), /** * * * @param {Interaction} interaction */ async execute(interaction, client) { const { options, guild, user } = interaction; const config = await client.configs.get(guild.id); let cooldown = 0; if (config.cooldowns.filter((c) => c.name === interaction.commandName).length > 0) { cooldown = config.cooldowns.find((c) => c.name === interaction.commandName).value; } else cooldown = 0; const cd = await handleCooldowns(interaction, cooldown); if (cd === false) return; const sub = options.getSubcommand(); const guildconfig = await GuildConfigs.findOne({ guildId: guild.id }).select( "-_id gamesCount" ); const number = config.gamesCount; let players = []; switch (sub) { case `new`: const size = options.getInteger("map-size"); let length = 4; if (size) length = size; let board = []; let playerArray = []; // initialze the grid for (let y = 0; y < length; y++) { for (let x = 0; x < length; x++) { board[y * length + x] = { type: 0, traps: [], items: [], players: [], visible: false, }; } } let game = await NexusGame.findOne({ guildId: guild.id, playerIds: user.id }); if (game) return interaction.reply({ content: "You are already participating in a game", ephemeral: true, }); game = await NexusGame.create({ userId: user.id, guildId: guild.id, playerIds: [`${user.id}`], }); players.push(user.id); //functions const buildTerrain = () => { let portalCount = 0; let enchantedGardenCount = 0; for (let y = 0; y < length; y++) { for (let x = 0; x < length; x++) { const rarityValue = Math.random() * 15; let selectedTerrain = "plains"; for (const type in mapTypes) { if (rarityValue <= mapTypes[type]) { selectedTerrain = type; if (selectedTerrain === "portal" && portalCount >= 2) { selectedTerrain = "plains"; } else if (selectedTerrain === "portal") { portalCount++; } if ( selectedTerrain === "enchantedGarden" && enchantedGardenCount >= 1 ) { selectedTerrain = "treasureChest"; } else if (selectedTerrain === "enchantedGarden") { enchantedGardenCount++; } break; } } board[y * length + x].type = mapTypes[selectedTerrain]; } } }; const getRandomItem = (min, max) => { const eligibleItems = items.filter( (item) => item.value >= min && item.value <= max ); const randomIndex = Math.floor(Math.random() * eligibleItems.length); return eligibleItems[randomIndex]; }; const addItems = () => { for (let y = 0; y < length; y++) { for (let x = 0; x < length; x++) { const terrainType = board[y * length + x].type; const chance = Math.random(); switch (terrainType) { case 1: if (chance <= 0.2) { board[y * length + x].items.push(getRandomItem(0, 2)); } break; case 2: if (chance <= 0.5) { board[y * length + x].items.push(getRandomItem(0, 5)); } break; case 3: if (chance <= 0.35) { board[y * length + x].items.push(getRandomItem(0, 5)); } break; case 4: board[y * length + x].items.push(getRandomItem(0, 5)); if (chance <= 0.4) { const newChance = Math.random(); if (newChance <= 0.02) { board[y * length + x].items.push(getRandomItem(10, 17)); } else if (newChance <= 0.33) { board[y * length + x].items.push(getRandomItem(6, 9)); } else { board[y * length + x].items.push(getRandomItem(0, 5)); } } break; case 9: board[y * length + x].items.push( items.find((item) => item.value === 9) ); break; case 10: const newChance = Math.random(); if (newChance <= 30) { board[y * length + x].items.push(getRandomItem(6, 9)); } else { board[y * length + x].items.push(getRandomItem(10, 17)); } if (chance <= 0.01) { for (let i = 0; i < 3; i++) { const newChance = Math.random(); if (newChance <= 30) { board[y * length + x].items.push( getRandomItem(6, 9) ); } else { board[y * length + x].items.push( getRandomItem(10, 17) ); } } } else if (chance <= 0.1) { for (let i = 0; i < 2; i++) { const newChance = Math.random(); if (newChance <= 30) { board[y * length + x].items.push( getRandomItem(6, 9) ); } else { board[y * length + x].items.push( getRandomItem(10, 17) ); } } } else if (chance <= 0.5) { const newChance = Math.random(); if (newChance <= 30) { board[y * length + x].items.push(getRandomItem(6, 9)); } else { board[y * length + x].items.push(getRandomItem(10, 17)); } } break; } } } }; const initializePlayers = async () => { await Promise.all( players.map(async (playerId) => { const randomY = Math.floor(Math.random() * length); const randomX = Math.floor(Math.random() * length); const data = await ProfileModel.findOne({ guildId: guild.id, userId: playerId, }); const playerItems = data.gameItems; const player = { userId: playerId, hp: 5, position: { x: randomX, y: randomY }, visionRange: 1, knownCells: [], items: playerItems, activeItemEffects: [], }; playerArray.push(player); board[randomY * length + randomX].players.push(player); }) ); }; const resetMapVisibility = () => { board.forEach((cell) => { cell.visible = false; }); }; const updateMap = (currentPlayer) => { resetMapVisibility(); const knownCells = currentPlayer.knownCells; const visionRange = currentPlayer.visionRange; for (let y = 0; y < length; y++) { for (let x = 0; x < length; x++) { const cellIndex = y * length + x; const horizontalDistance = Math.abs(currentPlayer.position.x - x); const verticalDistance = Math.abs(currentPlayer.position.y - y); if (knownCells.includes(cellIndex)) { board[cellIndex].visible = true; } else if ( horizontalDistance <= visionRange && verticalDistance <= visionRange && horizontalDistance + verticalDistance <= visionRange ) { board[cellIndex].visible = true; knownCells.push(cellIndex); } } } currentPlayer.knownCells = knownCells; }; const displayMap = (currentPlayer) => { updateMap(currentPlayer); let map = ""; let rowString = "-----"; for (let i = 0; i < length - 1; i++) { rowString += "----"; } map += `${rowString}\n`; for (let y = 0; y < length; y++) { for (let x = 0; x < length; x++) { const cell = board[y * length + x]; map += "|"; if (currentPlayer.position.x === x && currentPlayer.position.y === y) { map += " P "; } else { map += cell.visible ? " V " : " O "; } } map += `|\n${rowString}\n`; } console.log(map); return map; }; const createActionButtonRows = (currentPlayer) => { const hasItems = currentPlayer.items.length > 0; const actionRows = []; const firstRow = [ new ButtonBuilder() .setCustomId("nexusarena_attack") .setLabel("Atk") .setStyle(ButtonStyle.Secondary), // Move Top Left Button new ButtonBuilder() .setCustomId("nexusarena_move_tl") .setEmoji("↖️") .setStyle(ButtonStyle.Secondary) .setDisabled( currentPlayer.position.y === 0 || currentPlayer.position.x === 0 ), // Move Top Button new ButtonBuilder() .setCustomId("nexusarena_move_t") .setEmoji("⬆️") .setStyle(ButtonStyle.Secondary) .setDisabled(currentPlayer.position.y === 0), // Move Top Right Button new ButtonBuilder() .setCustomId("nexusarena_move_tr") .setEmoji("↗️") .setStyle(ButtonStyle.Secondary) .setDisabled( currentPlayer.position.y === 0 || currentPlayer.position.x === length - 1 ), // Hide Button new ButtonBuilder() .setCustomId("nexusarena_hide") .setLabel("Hide") .setStyle(ButtonStyle.Secondary), ]; actionRows.push(new ActionRowBuilder().addComponents(...firstRow)); const secondRow = [ // Decorative Disabled Button new ButtonBuilder() .setCustomId("nexusarena_decorative_disabled1") .setLabel("\u200b") .setStyle(ButtonStyle.Secondary) .setDisabled(true), // Move Left Button new ButtonBuilder() .setCustomId("nexusarena_move_l") .setEmoji("⬅️") .setStyle(ButtonStyle.Secondary) .setDisabled(currentPlayer.position.x === 0), // Disable if on the left edge // Scout Button new ButtonBuilder() .setCustomId("nexusarena_scout") .setEmoji("🔭") .setStyle(ButtonStyle.Secondary), // Move Right Button new ButtonBuilder() .setCustomId("nexusarena_move_r") .setEmoji("➡️") .setStyle(ButtonStyle.Secondary) .setDisabled(currentPlayer.position.x === length - 1), // Disable if on the right edge // Decorative Disabled Button new ButtonBuilder() .setCustomId("nexusarena_decorative_disabled2") .setLabel("\u200b") .setStyle(ButtonStyle.Secondary) .setDisabled(true), ]; actionRows.push(new ActionRowBuilder().addComponents(...secondRow)); const thirdRow = [ // Decorative Disabled Button new ButtonBuilder() .setCustomId("nexusarena_decorative_disabled3") .setLabel("\u200b") .setStyle(ButtonStyle.Secondary) .setDisabled(true), // Move Bottom Left Button new ButtonBuilder() .setCustomId("nexusarena_move_bf") .setEmoji("↙️") .setStyle(ButtonStyle.Secondary) .setDisabled( currentPlayer.position.y === length - 1 || currentPlayer.position.x === 0 ), // Disable if on the bottom or left edge // Move Bottom Button new ButtonBuilder() .setCustomId("nexusarena_move_b") .setEmoji("⬇️") .setStyle(ButtonStyle.Secondary) .setDisabled(currentPlayer.position.y === length - 1), // Disable if on the bottom edge // Move Bottom Right Button new ButtonBuilder() .setCustomId("nexusarena_move_br") .setEmoji("↘️") .setStyle(ButtonStyle.Secondary) .setDisabled( currentPlayer.position.y === length - 1 || currentPlayer.position.x === length - 1 ), // Disable if on the bottom or right edge // Decorative Disabled Button new ButtonBuilder() .setCustomId("decorative_disabled4") .setLabel("\u200b") .setStyle(ButtonStyle.Secondary) .setDisabled(true), ]; actionRows.push(new ActionRowBuilder().addComponents(...thirdRow)); const fourthRow = [ new StringSelectMenuBuilder() .setCustomId("nexusarena_use_item") .setPlaceholder("Use an item") .addOptions( hasItems ? currentPlayer.items.map((item) => ({ label: item.name, value: item.value.toString(), })) : [{ label: "No Items", value: "-1" }] ) .setDisabled(!hasItems), ]; actionRows.push(new ActionRowBuilder().addComponents(...fourthRow)); return actionRows; }; const movePlayer = (currentPlayer, direction) => { console.log(direction); const { x, y } = currentPlayer.position; let newX = x; let newY = y; switch (direction) { case "tl": newX -= 1; newY -= 1; break; case "t": newY -= 1; break; case "tr": newX += 1; newY -= 1; break; case "l": newX -= 1; break; case "r": newX += 1; break; case "bl": newX -= 1; newY += 1; break; case "b": newY += 1; break; case "br": newX += 1; newY += 1; break; } const currentIndex = y * length + x; board[currentIndex].players = board[currentIndex].players.filter( (p) => p.userId !== currentPlayer.userId ); currentPlayer.position = { x: newX, y: newY }; const newIndex = newY * length + newX; board[newIndex].players.push(currentPlayer); }; //embeds and buttons for the start menu let end = "th"; if (number % 10 === 1) end = "st"; if (number % 10 === 2) end = "nd"; if (number % 10 === 3) end = "rd"; const embed = new EmbedBuilder() .setTitle(`${guild.name}'s ${number}${end} Nexus Arena!`) .setColor("Purple") .setDescription( "At least 3 players must join before the battle can begin!\n🎟️ Join battle\n🗺️ Start battle\n🙋 View players" ); const playerEmbed = new EmbedBuilder() .setColor("Purple") .setDescription(`1 player has joined`); const joinButton = new ButtonBuilder() .setCustomId(BUTTON_IDS.JOIN) .setEmoji(BUTTON_EMOJIS.JOIN) .setLabel("Join") .setStyle(ButtonStyle.Success); const startButton = new ButtonBuilder() .setCustomId(BUTTON_IDS.START) .setEmoji(BUTTON_EMOJIS.START) .setStyle(ButtonStyle.Secondary); const playersButton = new ButtonBuilder() .setCustomId(BUTTON_IDS.PLAYERS) .setEmoji(BUTTON_EMOJIS.PLAYERS) .setLabel("Players") .setStyle(ButtonStyle.Secondary); const row = new ActionRowBuilder().addComponents( joinButton, startButton, playersButton ); const message = await interaction.reply({ embeds: [embed, playerEmbed], components: [row], }); // start menu buttons const collectorPromise = new Promise((reject, resolve) => { const collector = message.createMessageComponentCollector(); collector.on("collect", async (i) => { console.log("Line 558, collector created"); try { const btn = i.customId.split("_")[1]; switch (btn) { case "join": game = await NexusGame.findOne({ guildId: guild.id, playerIds: user.id, }); if (!game) return i.reply({ content: "This battle has been canceled.", ephemeral: true, }); if (players.includes(i.user.id)) { return i.reply({ content: `You have already joined this game.`, ephemeral: true, }); } else { players.push(i.user.id); game.playerIds = players; await game.save(); playerEmbed.setDescription( `${players.length} players have joined` ); message.edit({ embeds: [embed, playerEmbed] }); return i.reply({ content: `You have joined the Arena!`, ephemeral: true, }); } case "players": let string = ""; players.map((p) => { string += `<@${p}>\n`; }); return i.reply({ content: string, ephemeral: true, }); case "start": game = await NexusGame.findOne({ guildId: guild.id, playerIds: user.id, }); if (!game) return i.reply({ content: "This battle has been canceled.", ephemeral: true, }); if (!players.includes(i.user.id)) { return i.reply({ content: "You are not a player of this battle. You cannot influence it.", ephemeral: true, }); } else if (players.length < 1) { return i.reply({ content: "The minimum number of players required has not been met.", ephemeral: true, }); } else { console.log("line 622, start button pressed."); await i.reply({ content: "You have started the battle!", ephemeral: true, }); collector.stop(); } break; } } catch (error) { console.log( `[NEXUS ARENA] Error handling buttons. ${error.stack}\n${new Date( Date.now() )}`.red ); } }); collector.on("end", () => { console.log("line 640, collector ended"); resolve(); console.log("line 642, promise resolved"); }); }); console.log("line 645, waiting for collector to end"); try { await collectorPromise; console.log("line 648, waiting done 1"); } catch (error) { console.log(error.stack); } console.log("line 652, waiting done 2"); game.playerIds = players; game.state = true; await game.save(); row.setComponents( joinButton.setDisabled(true), startButton.setDisabled(true), playersButton.setDisabled(true) ); message.edit({ components: [row] }); buildTerrain(); addItems(); initializePlayers(); let currentTurnIndex = 0; let currentTurnUserId = players[currentTurnIndex]; let nextTurnIndex = (currentTurnIndex + 1) % players.length; let nextTurnUserId = players[nextTurnIndex]; const currentTurnEmbed = new EmbedBuilder() .setTitle("Current Turn") .setDescription(`It's currently <@${currentTurnUserId}>'s turn`) .addFields({ name: "Next turn", value: `Next up: <@${nextTurnUserId}>`, }) .setFooter({ text: `Click the button to start your turn.` }) .setColor("Purple"); const turnButton = new ButtonBuilder() .setCustomId("nexusarena_turn") .setStyle(ButtonStyle.Primary) .setLabel(`Click Me`); const turnRow = new ActionRowBuilder().addComponents(turnButton); const gameStartMessage = await i.channel.send({ content: `The battle has begun! Good luck players!\n<@${currentTurnUserId}>`, embeds: [currentTurnEmbed], components: [turnRow], }); const outerCollector = gameStartMessage.createMessageComponentCollector(); outerCollector.on("collect", async (buttonI) => { if (buttonI.user.id === currentTurnUserId) { const playerObj = playerArray.find((p) => p.userId === currentTurnUserId); const map = displayMap(playerObj); const actionRows = createActionButtonRows(playerObj); const playerMessage = new EmbedBuilder() .setTitle("Your Turn!") .setDescription(`${map}`) .setColor("Blue"); // Send ephemeral message with buttons to the specific player within the server const msg = await buttonI.reply({ content: `It's your turn, <@${currentTurnUserId}>!`, embeds: [playerMessage], components: actionRows, ephemeral: true, }); const innerCollector = msg.createMessageComponentCollector(); innerCollector.on("collect", async (pI) => { const btn = pI.customId.split("_"); console.log(btn); //handle movement if (btn[1] === "move") { console.log("Move button has been clicked"); movePlayer(playerObj, btn[2]); } //update turns and embeds currentTurnIndex = (currentTurnIndex + 1) % players.length; currentTurnUserId = players[currentTurnIndex]; nextTurnIndex = (currentTurnIndex + 1) % players.length; nextTurnUserId = players[nextTurnIndex]; const newTurnEmbed = new EmbedBuilder() .setTitle("Current Turn") .setDescription(`It's currently <@${currentTurnUserId}>'s turn`) .addFields({ name: "Next turn", value: `Next up: <@${nextTurnUserId}>`, }) .setFooter({ text: `Click the button to start your turn.`, }) .setColor("Purple"); await gameStartMessage.edit({ content: `The battle has begun! Good luck players!\n<@${currentTurnUserId}>`, embeds: [newTurnEmbed], }); }); } else { await i.reply({ content: "It's not your turn yet!", ephemeral: true, }); } }); break; case "cancel": let data = await NexusGame.findOne({ guildId: guild.id, playerIds: user.id }); if (!data) { return interaction.reply({ content: "You are not in an Arena.", ephemeral: true, }); } else if (data.userId !== user.id) { return interaction.reply({ content: `Only <@${data.userId}> can cancel this battle.`, ephemeral: true, }); } else { if (data.state === true) return interaction.reply({ content: "You cannot cancel an ongoing battle.", ephemeral: true, }); await NexusGame.deleteOne({ userId: user.id, guildId: guild.id }); return interaction.reply({ content: "You have canceled the battle.", }); } case "help": interaction.reply({ content: "Still writing this", ephemeral: true }); break; } return; }, };