diff --git a/Core/src/commands/guild/GuildKickCommand.ts b/Core/src/commands/guild/GuildKickCommand.ts index 53ad51bc2..2cd3ff78b 100644 --- a/Core/src/commands/guild/GuildKickCommand.ts +++ b/Core/src/commands/guild/GuildKickCommand.ts @@ -28,6 +28,7 @@ async function acceptGuildKick(player: Player, kickedPlayer: Player, response: D kickedPlayer.guildId = null; if (guild.elderId === kickedPlayer.id) { + draftBotInstance.logsDatabase.logGuildElderRemove(guild, guild.elderId).then(); guild.elderId = null; } await Promise.all([ diff --git a/Core/src/commands/guild/GuildLeaveCommand.ts b/Core/src/commands/guild/GuildLeaveCommand.ts new file mode 100644 index 000000000..5db564210 --- /dev/null +++ b/Core/src/commands/guild/GuildLeaveCommand.ts @@ -0,0 +1,121 @@ +import {commandRequires, CommandUtils} from "../../core/utils/CommandUtils"; +import {GuildConstants} from "../../../../Lib/src/constants/GuildConstants"; +import {DraftBotPacket, makePacket, PacketContext} from "../../../../Lib/src/packets/DraftBotPacket"; +import Player, {Players} from "../../core/database/game/models/Player"; +import { + CommandGuildLeaveAcceptPacketRes, CommandGuildLeaveNotInAGuildPacketRes, + CommandGuildLeavePacketReq, CommandGuildLeaveRefusePacketRes +} from "../../../../Lib/src/packets/commands/CommandGuildLeavePacket"; +import {Guilds} from "../../core/database/game/models/Guild"; +import {ReactionCollectorGuildLeave} from "../../../../Lib/src/packets/interaction/ReactionCollectorGuildLeave"; +import {EndCallback, ReactionCollectorInstance} from "../../core/utils/ReactionsCollector"; +import {ReactionCollectorAcceptReaction} from "../../../../Lib/src/packets/interaction/ReactionCollectorPacket"; +import {BlockingUtils} from "../../core/utils/BlockingUtils"; +import {BlockingConstants} from "../../../../Lib/src/constants/BlockingConstants"; +import {draftBotInstance} from "../../index"; +import {LogsDatabase} from "../../core/database/logs/LogsDatabase"; + + +/** + * Allow the player to leave its guild + * @param player + * @param response + */ +async function acceptGuildLeave(player: Player, response: DraftBotPacket[]): Promise { + await player.reload(); + // The player is no longer in a guild since the menu + if (player.guildId === null) { + response.push(makePacket(CommandGuildLeaveNotInAGuildPacketRes, {})); + return; + } + const guild = await Guilds.getById(player.guildId); + if (player.id === guild.chiefId) { + // The guild's chief is leaving + if (guild.elderId !== null) { + draftBotInstance.logsDatabase.logGuildElderRemove(guild, guild.elderId).then(); + draftBotInstance.logsDatabase.logGuildChiefChange(guild, guild.elderId).then(); + // An elder can recover the guild + player.guildId = null; + const elder = await Players.getById(guild.elderId); + guild.elderId = null; + guild.chiefId = elder.id; + response.push(makePacket(CommandGuildLeaveAcceptPacketRes, { + newChiefKeycloakId: elder.keycloakId, + guildName: guild.name + })); + + await Promise.all([ + elder.save(), + guild.save(), + player.save() + ]); + return; + } + // No elder => the guild will be destroyed + await guild.completelyDestroyAndDeleteFromTheDatabase(); + response.push(makePacket(CommandGuildLeaveAcceptPacketRes, { + guildName: guild.name, + isGuildDestroyed: true + })); + return; + } + if (guild.elderId === player.id) { + // The guild's elder is leaving + draftBotInstance.logsDatabase.logGuildElderRemove(guild, guild.elderId).then(); + guild.elderId = null; + } + LogsDatabase.logGuildLeave(guild, player.keycloakId).then(); + player.guildId = null; + response.push(makePacket(CommandGuildLeaveAcceptPacketRes, { + guildName: guild.name + })); + await Promise.all([ + player.save(), + guild.save() + ]); +} + +function endCallback(player: Player): EndCallback { + return async (collector, response): Promise => { + const reaction = collector.getFirstReaction(); + if (reaction && reaction.reaction.type === ReactionCollectorAcceptReaction.name) { + await acceptGuildLeave(player, response); + } + else { + response.push(makePacket(CommandGuildLeaveRefusePacketRes, {})); + } + BlockingUtils.unblockPlayer(player.id, BlockingConstants.REASONS.GUILD_LEAVE); + }; +} +export default class GuildLeaveCommand { + @commandRequires(CommandGuildLeavePacketReq, { + notBlocked: true, + disallowedEffects: CommandUtils.DISALLOWED_EFFECTS.NOT_STARTED_OR_DEAD, + level: GuildConstants.REQUIRED_LEVEL, + guildNeeded: true + }) + async execute(response: DraftBotPacket[], player: Player, packet: CommandGuildLeavePacketReq, context: PacketContext): Promise { + const guild = await Guilds.getById(player.guildId); + const newChief = guild.chiefId === player.id && guild.elderId ? await Players.getById(guild.elderId) : null; + + const collector = new ReactionCollectorGuildLeave( + guild.name, + guild.chiefId === player.id && guild.elderId === null, + newChief?.keycloakId + ); + + const collectorPacket = new ReactionCollectorInstance( + collector, + context, + { + allowedPlayerKeycloakIds: [player.keycloakId], + reactionLimit: 1 + }, + endCallback(player) + ) + .block(player.id, BlockingConstants.REASONS.GUILD_LEAVE) + .build(); + + response.push(collectorPacket); + } +} \ No newline at end of file diff --git a/Discord/src/commands/guild/GuildElderCommand.ts b/Discord/src/commands/guild/GuildElderCommand.ts index 78a60a25f..8b173b7f4 100644 --- a/Discord/src/commands/guild/GuildElderCommand.ts +++ b/Discord/src/commands/guild/GuildElderCommand.ts @@ -74,6 +74,7 @@ export async function handleCommandGuildElderRefusePacketRes(packet: CommandGuil }); } + /** * Handle the response of the server after a guild elder, * this packet is only sent if the promotion is accepted diff --git a/Discord/src/commands/guild/GuildLeaveCommand.ts b/Discord/src/commands/guild/GuildLeaveCommand.ts new file mode 100644 index 000000000..9c5585138 --- /dev/null +++ b/Discord/src/commands/guild/GuildLeaveCommand.ts @@ -0,0 +1,109 @@ +import {makePacket, PacketContext} from "../../../../Lib/src/packets/DraftBotPacket"; +import {ICommand} from "../ICommand"; +import {SlashCommandBuilderGenerator} from "../SlashCommandBuilderGenerator"; +import { + CommandGuildLeaveAcceptPacketRes, + CommandGuildLeavePacketReq, CommandGuildLeaveRefusePacketRes +} from "../../../../Lib/src/packets/commands/CommandGuildLeavePacket"; +import {ReactionCollectorCreationPacket} from "../../../../Lib/src/packets/interaction/ReactionCollectorPacket"; +import {DiscordCache} from "../../bot/DiscordCache"; +import {DraftBotEmbed} from "../../messages/DraftBotEmbed"; +import i18n from "../../translations/i18n"; +import {DiscordCollectorUtils} from "../../utils/DiscordCollectorUtils"; +import {ReactionCollectorGuildLeaveData} from "../../../../Lib/src/packets/interaction/ReactionCollectorGuildLeave"; +import {KeycloakUtils} from "../../../../Lib/src/keycloak/KeycloakUtils"; +import {keycloakConfig} from "../../bot/DraftBotShard"; + +/** + * Create a collector to accept/refuse to leave the guild + * @param packet + * @param context + */ +export async function createGuildLeaveCollector(packet: ReactionCollectorCreationPacket, context: PacketContext): Promise { + const interaction = DiscordCache.getInteraction(context.discord!.interaction)!; + await interaction.deferReply(); + const data = packet.data.data as ReactionCollectorGuildLeaveData; + const newChiefPseudo = data.newChiefKeycloakId ? (await KeycloakUtils.getUserByKeycloakId(keycloakConfig, data.newChiefKeycloakId))!.attributes.gameUsername : ""; + const keyDesc = data.isGuildDestroyed ? "confirmChiefDesc" : data.newChiefKeycloakId ? "confirmChiefDescWithElder" : "confirmDesc"; + const embed = new DraftBotEmbed().formatAuthor(i18n.t("commands:guildLeave.title", { + lng: interaction.userLanguage, + pseudo: interaction.user.displayName + }), interaction.user) + .setDescription( + i18n.t(`commands:guildLeave.${keyDesc}`, { + lng: interaction.userLanguage, + newChiefPseudo, + guildName: data.guildName + }) + ); + + await DiscordCollectorUtils.createAcceptRefuseCollector(interaction, embed, packet, context); +} + +/** + * Handle the response when the player leave its guild + * @param packet + * @param context + */ +export async function handleCommandGuildLeaveAcceptPacketRes(packet: CommandGuildLeaveAcceptPacketRes, context: PacketContext): Promise { + const originalInteraction = DiscordCache.getInteraction(context.discord!.interaction!); + const buttonInteraction = DiscordCache.getButtonInteraction(context.discord!.buttonInteraction!); + const keyTitle = packet.newChiefKeycloakId ? "newChiefTitle" : "successTitle"; + const keyDesc = packet.isGuildDestroyed ? "destroySuccess" : "leavingSuccess"; + const newChiefPseudo = packet.newChiefKeycloakId ? (await KeycloakUtils.getUserByKeycloakId(keycloakConfig, packet.newChiefKeycloakId))!.attributes.gameUsername : ""; + if (buttonInteraction && originalInteraction) { + await buttonInteraction.editReply({ + embeds: [ + new DraftBotEmbed().formatAuthor(i18n.t(`commands:guildLeave.${keyTitle}`, { + lng: originalInteraction.userLanguage, + pseudo: originalInteraction.user.displayName, + newChiefPseudo, + guildName: packet.guildName + }), originalInteraction.user) + .setDescription( + i18n.t(`commands:guildLeave.${keyDesc}`, {lng: originalInteraction.userLanguage, guildName: packet.guildName}) + ) + ] + }); + } +} + +/** + * Handle the response when the player don't leave its guild + * @param packet + * @param context + */ +export async function handleCommandGuildLeaveRefusePacketRes(packet: CommandGuildLeaveRefusePacketRes, context: PacketContext): Promise { + const originalInteraction = DiscordCache.getInteraction(context.discord!.interaction!); + if (!originalInteraction) { + return; + } + const buttonInteraction = DiscordCache.getButtonInteraction(context.discord!.buttonInteraction!); + await buttonInteraction?.editReply({ + embeds: [ + new DraftBotEmbed().formatAuthor(i18n.t("commands:guildLeave.canceledTitle", { + lng: originalInteraction.userLanguage, + pseudo: originalInteraction.user.displayName + }), originalInteraction.user) + .setDescription( + i18n.t("commands:guildLeave.canceledDesc", { + lng: originalInteraction.userLanguage + }) + ) + .setErrorColor() + ] + }); +} + +/** + * Allow the player to leave its guild + */ +function getPacket(): CommandGuildLeavePacketReq { + return makePacket(CommandGuildLeavePacketReq, {}); +} + +export const commandInfo: ICommand = { + slashCommandBuilder: SlashCommandBuilderGenerator.generateBaseCommand("guildLeave"), + getPacket, + mainGuildCommand: false +}; \ No newline at end of file diff --git a/Discord/src/packetHandlers/handlers/CommandHandlers.ts b/Discord/src/packetHandlers/handlers/CommandHandlers.ts index 1c24c439f..d3244d433 100644 --- a/Discord/src/packetHandlers/handlers/CommandHandlers.ts +++ b/Discord/src/packetHandlers/handlers/CommandHandlers.ts @@ -139,6 +139,14 @@ import { CommandGuildElderRefusePacketRes, CommandGuildElderSameGuildPacketRes } from "../../../../Lib/src/packets/commands/CommandGuildElderPacket"; +import { + CommandGuildLeaveAcceptPacketRes, CommandGuildLeaveNotInAGuildPacketRes, + CommandGuildLeaveRefusePacketRes +} from "../../../../Lib/src/packets/commands/CommandGuildLeavePacket"; +import { + handleCommandGuildLeaveAcceptPacketRes, + handleCommandGuildLeaveRefusePacketRes +} from "../../commands/guild/GuildLeaveCommand"; import {handleCommandGuildElderAcceptPacketRes, handleCommandGuildElderRefusePacketRes} from "../../commands/guild/GuildElderCommand"; import {CommandSwitchCancelled, CommandSwitchErrorNoItemToSwitch, CommandSwitchSuccess} from "../../../../Lib/src/packets/commands/CommandSwitchPacket"; import {handleItemSwitch} from "../../commands/player/SwitchCommand"; @@ -274,22 +282,22 @@ export default class CommandHandlers { } @packetHandler(CommandGuildElderSameGuildPacketRes) - async guildElderSameGuildRes(packet: CommandGuildElderSameGuildPacketRes, context: PacketContext): Promise { + async guildElderSameGuildRes(_packet: CommandGuildElderSameGuildPacketRes, context: PacketContext): Promise { await handleClassicError(context, "commands:guildElder.notSameGuild"); } @packetHandler(CommandGuildElderHimselfPacketRes) - async guildElderHimselfRes(packet: CommandGuildElderHimselfPacketRes, context: PacketContext): Promise { + async guildElderHimselfRes(_packet: CommandGuildElderHimselfPacketRes, context: PacketContext): Promise { await handleClassicError(context, "commands:guildElder.chiefError"); } @packetHandler(CommandGuildElderAlreadyElderPacketRes) - async guildElderAlreadyElderRes(packet: CommandGuildElderAlreadyElderPacketRes, context: PacketContext): Promise { + async guildElderAlreadyElderRes(_packet: CommandGuildElderAlreadyElderPacketRes, context: PacketContext): Promise { await handleClassicError(context, "commands:guildElder.alreadyElder"); } @packetHandler(CommandGuildElderFoundPlayerPacketRes) - async guildElderFoundPlayerRes(packet: CommandGuildElderFoundPlayerPacketRes, context: PacketContext): Promise { + async guildElderFoundPlayerRes(_packet: CommandGuildElderFoundPlayerPacketRes, context: PacketContext): Promise { await handleClassicError(context, "commands:guildElder.playerNotFound"); } @@ -303,6 +311,21 @@ export default class CommandHandlers { await handleCommandGuildElderAcceptPacketRes(packet, context); } + @packetHandler(CommandGuildLeaveNotInAGuildPacketRes) + async guildLeaveNotInAGuildRes(_packet: CommandGuildLeaveNotInAGuildPacketRes, context: PacketContext): Promise { + await handleClassicError(context, "commands:guildLeave.notInAGuild"); + } + + @packetHandler(CommandGuildLeaveRefusePacketRes) + async guildLeaveRefuseRes(packet: CommandGuildLeaveRefusePacketRes, context: PacketContext): Promise { + await handleCommandGuildLeaveRefusePacketRes(packet, context); + } + + @packetHandler(CommandGuildLeaveAcceptPacketRes) + async guildLeaveAcceptRes(packet: CommandGuildLeaveAcceptPacketRes, context: PacketContext): Promise { + await handleCommandGuildLeaveAcceptPacketRes(packet,context); + } + @packetHandler(CommandInventoryPacketRes) async inventoryRes(packet: CommandInventoryPacketRes, context: PacketContext): Promise { diff --git a/Discord/src/packetHandlers/handlers/ReactionCollectorHandlers.ts b/Discord/src/packetHandlers/handlers/ReactionCollectorHandlers.ts index c6aee577a..94f1e7ff6 100644 --- a/Discord/src/packetHandlers/handlers/ReactionCollectorHandlers.ts +++ b/Discord/src/packetHandlers/handlers/ReactionCollectorHandlers.ts @@ -43,6 +43,8 @@ import {ReactionCollectorSkipMissionShopItemData} from "../../../../Lib/src/pack import {skipMissionShopItemCollector} from "../../commands/mission/MissionShop"; import {createGuildElderCollector} from "../../commands/guild/GuildElderCommand"; import {ReactionCollectorGuildElderData} from "../../../../Lib/src/packets/interaction/ReactionCollectorGuildElder"; +import {ReactionCollectorGuildLeaveData} from "../../../../Lib/src/packets/interaction/ReactionCollectorGuildLeave"; +import {createGuildLeaveCollector} from "../../commands/guild/GuildLeaveCommand"; import {ReactionCollectorSwitchItemData} from "../../../../Lib/src/packets/interaction/ReactionCollectorSwitchItem"; import {switchItemCollector} from "../../commands/player/SwitchCommand"; @@ -59,6 +61,7 @@ export default class ReactionCollectorHandler { ReactionCollectorHandler.collectorMap.set(ReactionCollectorGuildCreateData.name, createGuildCreateCollector); ReactionCollectorHandler.collectorMap.set(ReactionCollectorGuildKickData.name, createGuildKickCollector); ReactionCollectorHandler.collectorMap.set(ReactionCollectorGuildElderData.name, createGuildElderCollector); + ReactionCollectorHandler.collectorMap.set(ReactionCollectorGuildLeaveData.name, createGuildLeaveCollector); ReactionCollectorHandler.collectorMap.set(ReactionCollectorLotteryData.name, lotteryCollector); ReactionCollectorHandler.collectorMap.set(ReactionCollectorInteractOtherPlayersPoorData.name, interactOtherPlayersCollector); ReactionCollectorHandler.collectorMap.set(ReactionCollectorWitchData.name, witchCollector); diff --git a/Lang/en/discordBuilder.json b/Lang/en/discordBuilder.json index cd7668282..ace47ec53 100644 --- a/Lang/en/discordBuilder.json +++ b/Lang/en/discordBuilder.json @@ -109,6 +109,20 @@ } } }, + "guildElder": { + "description": "Promote a guild member to elder.", + "name": "guildelder", + "options": { + "user": { + "description": "The user who will be promoted as an elder.", + "name": "user" + } + } + }, + "guildLeave": { + "description": "Leave your guild", + "name": "guildleave" + }, "profile": { "description": "Displays the profile of a player.", "name": "profile", diff --git a/Lang/fr/commands.json b/Lang/fr/commands.json index 8af59226e..227eb4293 100644 --- a/Lang/fr/commands.json +++ b/Lang/fr/commands.json @@ -89,12 +89,24 @@ "chiefError": "En tant que chef de la guilde, vous ne pouvez pas être l'aîné.", "alreadyElder": "Le joueur est déjà aîné de votre guilde.", "title": "{{pseudo}}, confirmez-vous votre choix ?", - "confirmDesc": ":question: {{elderPseudo}} deviendra l'aîné de la guilde `{{guildName}}`.", + "confirmDesc": "{emote:collectors.question} {{elderPseudo}} deviendra l'aîné de la guilde `{{guildName}}`.", "successElderAddTitle": "{{elderPseudo}} est le nouvel aîné de la guilde {{guildName}} !", "acceptedDesc": "L'aîné sert à aider le chef dans la gestion de la guilde. Il faut donc le choisir avec prudence ! Mais rassurez-vous, si vous n'êtes pas satisfait de votre aîné, vous pouvez le remplacer avec la commande {command:guildelder}, ou si vous avez une âme de dictateur, vous pouvez définitivement le supprimer avec {command:guildelderremove}.", "canceledDesc": "L'aîné n'a pas été promu.", - "canceledTitle": "Annulation prise en compte.", - "problemWhilePromoting": "Une erreur est survenue lors de la promotion du joueur." + "canceledTitle": "Annulation prise en compte." + }, + "guildLeave": { + "title": "{{pseudo}}, confirmation :", + "confirmDesc": "{emote:collectors.question} Souhaitez vous vraiment quitter la guilde `{{guildName}}` ?", + "confirmChiefDesc": "{emote:collectors.question} Souhaitez vous vraiment **détruire définitivement** la guilde `{{guildName}}` ?", + "confirmChiefDescWithElder": "{emote:collectors.question} Souhaitez vous vraiment quitter la guilde `{{guildName}}` ? {{newChiefPseudo}} en deviendra le chef.", + "canceledDesc": "Votre départ a été annulé.", + "successTitle": "{{pseudo}} a quitté la guilde {{guildName}} !", + "leavingSuccess": "Vous êtes un pauvre cow-boy solitaire et vous êtes très loin de chez vous...", + "destroySuccess": "La guilde {{guildName}} a correctement été dissoute.", + "newChiefTitle": "{{newChiefPseudo}} est le nouveau chef de la guilde {{guildName}} !", + "notInAGuild": "Il semblerait que vous ne fassiez déjà plus parti d'une guilde.", + "canceledTitle": "Annulation prise en compte" }, "help": { "aliasFieldTitle": "Alias", diff --git a/Lib/src/packets/commands/CommandGuildLeavePacket.ts b/Lib/src/packets/commands/CommandGuildLeavePacket.ts new file mode 100644 index 000000000..3f9c0b2aa --- /dev/null +++ b/Lib/src/packets/commands/CommandGuildLeavePacket.ts @@ -0,0 +1,22 @@ +import {DraftBotPacket, PacketDirection, sendablePacket} from "../DraftBotPacket"; + +@sendablePacket(PacketDirection.FRONT_TO_BACK) +export class CommandGuildLeavePacketReq extends DraftBotPacket { +} + +@sendablePacket(PacketDirection.BACK_TO_FRONT) +export class CommandGuildLeaveRefusePacketRes extends DraftBotPacket { +} + +@sendablePacket(PacketDirection.BACK_TO_FRONT) +export class CommandGuildLeaveAcceptPacketRes extends DraftBotPacket { + newChiefKeycloakId?: string; + + guildName!: string; + + isGuildDestroyed?: boolean; +} + +@sendablePacket(PacketDirection.BACK_TO_FRONT) +export class CommandGuildLeaveNotInAGuildPacketRes extends DraftBotPacket { +} \ No newline at end of file diff --git a/Lib/src/packets/interaction/ReactionCollectorGuildLeave.ts b/Lib/src/packets/interaction/ReactionCollectorGuildLeave.ts new file mode 100644 index 000000000..f58d2f5e2 --- /dev/null +++ b/Lib/src/packets/interaction/ReactionCollectorGuildLeave.ts @@ -0,0 +1,46 @@ +import { + ReactionCollector, + ReactionCollectorAcceptReaction, + ReactionCollectorCreationPacket, + ReactionCollectorData, + ReactionCollectorRefuseReaction +} from "./ReactionCollectorPacket"; + +export class ReactionCollectorGuildLeaveData extends ReactionCollectorData { + guildName!: string; + + isGuildDestroyed!: boolean; + + newChiefKeycloakId!: string; +} + +export class ReactionCollectorGuildLeave extends ReactionCollector { + private readonly guildName: string; + + private readonly newChiefKeycloakId: string; + + private readonly isGuildDestroyed: boolean; + + constructor(guildName: string, isGuildDestroyed: boolean, newChiefKeycloakId: string) { + super(); + this.guildName = guildName; + this.newChiefKeycloakId = newChiefKeycloakId; + this.isGuildDestroyed = isGuildDestroyed; + } + + creationPacket(id: string, endTime: number): ReactionCollectorCreationPacket { + return { + id, + endTime, + reactions: [ + this.buildReaction(ReactionCollectorAcceptReaction, {}), + this.buildReaction(ReactionCollectorRefuseReaction, {}) + ], + data: this.buildData(ReactionCollectorGuildLeaveData, { + guildName: this.guildName, + newChiefKeycloakId: this.newChiefKeycloakId, + isGuildDestroyed: this.isGuildDestroyed + }) + }; + } +} \ No newline at end of file