discord-multipurpose-bot/commands/games/spyfall.js

402 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2024-10-28 23:14:58 +00:00
const {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
} = require("discord.js");
const SpyfallGame = require("../../models/SpyfallGame");
const SpyfallLocation = require("../../models/SpyfallLocation");
const { v4: uuidv4 } = require("uuid");
module.exports = {
data: new SlashCommandBuilder()
.setName("spyfall")
.setDescription("Start a game of Spyfall."),
async execute(interaction) {
const guildId = interaction.guild.id;
const gameId = uuidv4();
const existingGame = await SpyfallGame.findOne({ gameId });
if (existingGame && existingGame.status === "ongoing") {
return interaction.reply(
"A game is already in progress! Please finish the current game first."
);
}
try {
const locations = await SpyfallLocation.find({});
if (!locations || locations.length === 0) {
return interaction.reply("No locations found. Please try again later.");
}
const selectedLocation =
locations[Math.floor(Math.random() * locations.length)].name;
const players = new Set();
const joinEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Join the Spyfall Game!")
.setDescription("Click the button below to join the game.")
.addFields({ name: "Current Players:", value: "None yet." })
.setFooter({ text: "You need at least 2 players to start!" })
.setTimestamp();
const joinButton = new ButtonBuilder()
.setCustomId("join_game")
.setLabel("Join Game")
.setStyle(ButtonStyle.Success);
const leaveButton = new ButtonBuilder()
.setCustomId("leave_game")
.setLabel("Leave Game")
.setStyle(ButtonStyle.Danger);
const startButton = new ButtonBuilder()
.setCustomId("start_game")
.setLabel("Start Game")
.setStyle(ButtonStyle.Primary)
.setDisabled(true);
const buttonRow = new ActionRowBuilder().addComponents(
joinButton,
leaveButton,
startButton
);
const joinMessage = await interaction.reply({
embeds: [joinEmbed],
components: [buttonRow],
fetchReply: true,
});
const filter = (i) =>
i.customId === "join_game" ||
i.customId === "leave_game" ||
i.customId === "start_game";
const collector = joinMessage.createMessageComponentCollector({ filter });
collector.on("collect", async (i) => {
try {
if (!players.has(i.user.id) && i.customId !== "join_game") {
return i.reply({
content: "You cannot interact with this button.",
ephemeral: true,
});
}
if (i.customId === "join_game") {
if (players.has(i.user.id)) {
return i.reply({
content: "You are already in the game!",
ephemeral: true,
});
}
players.add(i.user.id);
const currentPlayers =
Array.from(players)
.map(
(id) => interaction.guild.members.cache.get(id).user.username
)
.join(", ") || "None yet.";
joinEmbed
.setDescription("Click the button below to join the game.")
.setFields([{ name: "Current Players:", value: currentPlayers }]);
if (players.size >= 2) {
startButton.setDisabled(false);
}
await i.update({ embeds: [joinEmbed], components: [buttonRow] });
} else if (i.customId === "leave_game") {
if (players.has(i.user.id)) {
players.delete(i.user.id);
const currentPlayers =
Array.from(players)
.map(
(id) =>
interaction.guild.members.cache.get(id).user.username
)
.join(", ") || "None yet.";
joinEmbed
.setDescription("Click the button below to join the game.")
.setFields([
{ name: "Current Players:", value: currentPlayers },
]);
await i.update({ embeds: [joinEmbed], components: [buttonRow] });
} else {
await i.reply({
content: "You are not part of the game.",
ephemeral: true,
});
}
} else if (i.customId === "start_game") {
if (players.size < 2) {
return i.reply({
content: "You need at least 2 players to start the game.",
ephemeral: true,
});
}
const spyIndex = Math.floor(Math.random() * players.size);
const spy = Array.from(players)[spyIndex];
const newGame = new SpyfallGame({
gameId,
guildId,
location: selectedLocation,
spy,
players: Array.from(players),
status: "ongoing",
});
await newGame.save();
const locationMessage = `The game has started! The location is **${selectedLocation}**.`;
for (const playerId of players) {
const player = await interaction.guild.members.cache.get(playerId)
.user;
if (playerId !== spy) {
await player.send(locationMessage);
} else {
await player.send(
"The game has started! You are the spy! Try to blend in!"
);
}
}
const embed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Spyfall Game Started!")
.setDescription("The game has started! One of you is the spy!")
.setFooter({
text: "The spy must figure out the location without revealing themselves!",
})
.setTimestamp();
await interaction.followUp({ embeds: [embed], components: [] });
collector.stop();
await interaction.channel.send({
content: "The game has started! Use `/stopspyfall` to end it.",
});
await startQuestioningPhase(
interaction,
spy,
players,
selectedLocation,
gameId
);
return;
}
} catch (error) {
console.error("Error during button interaction:", error);
await i.reply({
content:
"There was an error processing your request. Please try again later.",
ephemeral: true,
});
}
});
} catch (error) {
console.error("Error fetching locations or starting the game:", error);
await interaction.reply(
"There was an error fetching locations or starting the game. Please try again later."
);
}
},
};
async function startQuestioningPhase(
interaction,
spy,
players,
selectedLocation,
gameId
) {
const ongoingGame = await SpyfallGame.findOne({ gameId });
if (!ongoingGame || ongoingGame.status !== "ongoing") {
return;
}
const playerArray = Array.from(players);
let currentIndex = 0;
async function askQuestion() {
if (currentIndex >= playerArray.length) {
return startVotingPhase(interaction, spy, playerArray, gameId);
}
const currentPlayerId = playerArray[currentIndex];
const currentPlayer =
interaction.guild.members.cache.get(currentPlayerId).user;
const questionEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Your Turn to Ask a Question!")
.setDescription(
`It's **${currentPlayer.username}**'s turn to ask a question about the location.`
)
.setFooter({ text: "Click 'Finish Turn' when you're done." });
const finishButton = new ButtonBuilder()
.setCustomId(`finish_turn_${currentPlayerId}`)
.setLabel("Finish Turn")
.setStyle(ButtonStyle.Primary);
const buttonRow = new ActionRowBuilder().addComponents(finishButton);
await interaction.channel.send({
embeds: [questionEmbed],
components: [buttonRow],
});
const filter = (i) =>
i.customId === `finish_turn_${currentPlayerId}` &&
i.user.id === currentPlayerId;
const collector = interaction.channel.createMessageComponentCollector({
filter,
time: 30000,
});
collector.on("collect", async (i) => {
await i.reply({ content: "Your turn has ended!", ephemeral: true });
collector.stop();
});
collector.on("end", async () => {
currentIndex++;
await askQuestion();
});
}
askQuestion();
}
async function startVotingPhase(
interaction,
spy,
players,
gameId,
selectedLocation
) {
// Ensure players is an array
if (!Array.isArray(players)) {
console.error("Players is not an array:", players);
return;
}
const ongoingGame = await SpyfallGame.findOne({ gameId });
if (!ongoingGame || ongoingGame.status !== "ongoing") {
return;
}
const votes = new Map();
const voteEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Voting Phase")
.setDescription(
"Vote for who you think the spy is! (You cannot vote for yourself)"
)
.setFooter({ text: "Click the buttons below to vote." });
const voteButtons = players.map((playerId) =>
new ButtonBuilder()
.setCustomId(`vote_${playerId}`)
.setLabel(interaction.guild.members.cache.get(playerId).user.username)
.setStyle(ButtonStyle.Primary)
);
const voteRow = new ActionRowBuilder().addComponents(voteButtons);
const voteMessage = await interaction.channel.send({
embeds: [voteEmbed],
components: [voteRow],
});
const voteFilter = (i) =>
i.customId.startsWith("vote_") && players.includes(i.user.id);
const voteCollector = voteMessage.createMessageComponentCollector({
filter: voteFilter,
});
voteCollector.on("collect", async (i) => {
const votedPlayerId = i.customId.split("_")[1];
if (i.user.id === votedPlayerId) {
await i.reply({
content: "You cannot vote for yourself.",
ephemeral: true,
});
return;
}
// Add the vote
votes.set(votedPlayerId, (votes.get(votedPlayerId) || 0) + 1);
await i.reply({
content: `You voted for ${
interaction.guild.members.cache.get(votedPlayerId).user.username
}.`,
ephemeral: true,
});
if (votes.size === players.length) {
voteCollector.stop();
await revealSpy(interaction, spy, votes, selectedLocation, gameId);
}
});
voteCollector.on("end", async () => {
if (votes.size > 0) {
await revealSpy(interaction, spy, votes, selectedLocation, gameId);
} else {
await interaction.channel.send("Voting timed out! No votes were cast.");
}
});
}
async function revealSpy(interaction, spy, votes, selectedLocation, gameId) {
const voteEntries = Array.from(votes.entries());
const highestVote = Math.max(...voteEntries.map(([_, v]) => v));
const suspectedSpy = voteEntries.find(([_, v]) => v === highestVote)[0];
const resultEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Voting Results")
.addFields(
{
name: "Suspected Spy:",
value: interaction.guild.members.cache.get(suspectedSpy).user.username,
},
{
name: "Actual Spy:",
value: interaction.guild.members.cache.get(spy).user.username,
}
)
.setFooter({
text:
suspectedSpy === spy
? "The spy has been caught!"
: "The spy has escaped!",
})
.setTimestamp();
const result = await SpyfallGame.updateOne({ gameId }, { status: "ended" });
if (result.modifiedCount === 0) {
console.log("No game found or status was already 'ended'.");
}
await interaction.channel.send({ embeds: [resultEmbed] });
}