Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-159: Request a duel using buttons #164

Merged
merged 2 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"allowedRoleIds": [],
"requestExpireTime": 60,
"requestCooldownTime": 0,
"requestReactions": false,
"simultaneousGames": false,
"gameExpireTime": 30,
"gameBoardReactions": false,
Expand Down
6 changes: 5 additions & 1 deletion config/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Zum Annehmen reagieren.",
"expire": ":x: `{invited}` ist nicht zum Match angetreten.",
"reject": ":x: `{invited}` hat das Match abgelehnt.",
"unknown-user": "Du kannst diesen User nicht herausfordern."
"unknown-user": "Du kannst diesen User nicht herausfordern.",
"button": {
"accept": "Akzeptieren",
"decline": "Ablehnen"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "React to this message to accept or decline the duel.",
"expire": ":x: `{invited}` did not rise up to the challenge.",
"reject": ":x: `{invited}` has rejected the duel.",
"unknown-user": "you cannot challenge that user."
"unknown-user": "you cannot challenge that user.",
"button": {
"accept": "Accept",
"decline": "Decline"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Reacciona a este mensaje para aceptar o rechazar el duelo.",
"expire": ":x: `{invited}` no se puso a la altura del desafío.",
"reject": ":x: `{invited}` ha rechazado el duelo.",
"unknown-user": "no pudiste desafiar a ese usuario."
"unknown-user": "no pudiste desafiar a ese usuario.",
"button": {
"accept": "Aceptar",
"decline": "Rechazar"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Réagis à ce message pour accepter ou décliner le duel.",
"expire": ":x: `{invited}` n'a pas répondu à la demande dans les temps.",
"reject": ":x: `{invited}` a rejeté votre proposition de duel.",
"unknown-user": "vous ne pouvez pas défier ce membre."
"unknown-user": "vous ne pouvez pas défier ce membre.",
"button": {
"accept": "Accepter",
"decline": "Décliner"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Reagisci a questo messaggio per accettare o no la sfida.",
"expire": ":x: `{invited}` non ha risposto.",
"reject": ":x: `{invited}` non ha accettato.",
"unknown-user": "non puoi sfidare questo utente."
"unknown-user": "non puoi sfidare questo utente.",
"button": {
"accept": "Accettare",
"decline": "Rifiutare"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Reageer op dit bericht om het spel te accepteren of te weigeren.",
"expire": ":x: `{invited}` heeft te laat gereageerd!",
"reject": ":x: `{invited}` heeft het spel geweigerd!",
"unknown-user": "Je kan dit lid niet uitdagen!"
"unknown-user": "Je kan dit lid niet uitdagen!",
"button": {
"accept": "Accepteren",
"decline": "Refuse"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Zareaguj odpowiednią emotką do tej wiadomości, aby zaakceptować lub odrzucić zaproszenie.",
"expire": ":x: `{invited}` nie odpowiedział na zaproszenie do pojedynku.",
"reject": ":x: `{invited}` odrzucił zaproszenie do pojedynku.",
"unknown-user": "nie możesz wyzwać na pojedynek tego użytkownika."
"unknown-user": "nie możesz wyzwać na pojedynek tego użytkownika.",
"button": {
"accept": "Akceptuj",
"decline": "Odmawiający"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Reaja esta mensagem para aceitar ou recusar este duelo.",
"expire": ":x: `{invited}` não apareceu para o duelo.",
"reject": ":x: `{invited}` rejeitou o duelo.",
"unknown-user": "você não pode desafiar este usuário."
"unknown-user": "você não pode desafiar este usuário.",
"button": {
"accept": "Accept",
"decline": "Decline"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "Нажмите на реакции ниже для принятия или отклонения приглашения!",
"expire": ":x: `{invited}` Не ответил на Ваше предложение.",
"reject": ":x: `{invited}` Отклонил вызов на игру.",
"unknown-user": "Вы не можете вызвать данного игрока **{username}**. Пожалуйста линканите другого пользователя."
"unknown-user": "Вы не можете вызвать данного игрока **{username}**. Пожалуйста линканите другого пользователя.",
"button": {
"accept": "Принять",
"decline": "отказывать"
}
},
"game": {
"title": ":game_die: `{player1}` **Против** `{player2}`",
Expand Down
6 changes: 5 additions & 1 deletion config/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"action": "React vào tin nhắn này để chấp nhận hoặc từ chối lời mời",
"expire": ":x: `{invited}` đã không phản hồi gì về lời mời của bạn",
"reject": ":x: `{invited}` đã từ chối lời mời của bạn.",
"unknown-user": "Bạn không thể thách đấu **{username}**. Hãy mention một người khác."
"unknown-user": "Bạn không thể thách đấu **{username}**. Hãy mention một người khác.",
"button": {
"accept": "Accept",
"decline": "Decline"
}
},
"game": {
"title": ":game_die: `{player1}` **VS** `{player2}`",
Expand Down
4 changes: 2 additions & 2 deletions src/bot/command/GameCommand.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import InteractionMessagingTunnel from '@bot/messaging/InteractionMessagingTunnel';
import CommandInteractionMessagingTunnel from '@bot/messaging/CommandInteractionMessagingTunnel';
import MessagingTunnel from '@bot/messaging/MessagingTunnel';
import TextMessagingTunnel from '@bot/messaging/TextMessagingTunnel';
import GameStateManager from '@bot/state/GameStateManager';
Expand Down Expand Up @@ -69,7 +69,7 @@ export default class GameCommand {
(noTrigger || interaction.commandName === this.config.command)
) {
// Retrieve the inviter and create an interaction tunnel
const tunnnel = new InteractionMessagingTunnel(interaction);
const tunnnel = new CommandInteractionMessagingTunnel(interaction);

// Retrieve invited user from options if provided
const mentionned = interaction.options.getMember(
Expand Down
122 changes: 95 additions & 27 deletions src/bot/entity/DuelRequest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import ComponentInteractionMessagingTunnel from '@bot/messaging/ComponentInteractionMessagingTunnel';
import MessagingTunnel from '@bot/messaging/MessagingTunnel';
import GameStateManager from '@bot/state/GameStateManager';
import localize from '@i18n/localize';
import {
Collection,
GuildMember,
Message,
MessageActionRow,
MessageButton,
MessageComponentInteraction,
MessageOptions,
MessageReaction,
Snowflake
Expand All @@ -26,10 +30,6 @@ export default class DuelRequest {
* Global game state manager.
*/
private readonly manager: GameStateManager;
/**
* Tunnel that initiated the duel request.
*/
private readonly tunnel: MessagingTunnel;
/**
* Member who has been invited to play
*/
Expand All @@ -38,6 +38,14 @@ export default class DuelRequest {
* Expiration time of a request message
*/
private readonly expireTime: number;
/**
* Interact with reactions instead of buttons
*/
private readonly useReactions: boolean;
/**
* Tunnel that initiated the duel request.
*/
private tunnel: MessagingTunnel;

/**
* Constructs a new duel request based on a message.
Expand All @@ -46,17 +54,20 @@ export default class DuelRequest {
* @param tunnel messaging tunnel that created the request
* @param invited invited member object
* @param expireTime expiration time of the mesage, undefined for default
* @param useReactions interact with reactions instead of buttons
*/
constructor(
manager: GameStateManager,
tunnel: MessagingTunnel,
invited: GuildMember,
expireTime?: number
expireTime?: number,
useReactions?: boolean
) {
this.manager = manager;
this.tunnel = tunnel;
this.invited = invited;
this.expireTime = expireTime ?? 60;
this.useReactions = useReactions ?? false;
}

/**
Expand All @@ -75,6 +86,22 @@ export default class DuelRequest {

return {
allowedMentions: { parse: [] },
components: !this.useReactions
? [
new MessageActionRow().addComponents(
new MessageButton({
style: 'SUCCESS',
customId: 'yes',
label: localize.__('duel.button.accept')
}),
new MessageButton({
style: 'DANGER',
customId: 'no',
label: localize.__('duel.button.decline')
})
)
]
: [],
embeds: [
{
color: 2719929, // #2980B9
Expand All @@ -92,39 +119,78 @@ export default class DuelRequest {
* @param message discord.js message object to attach
*/
public async attachTo(message: Message): Promise<void> {
for (const reaction of DuelRequest.REACTIONS) {
await message.react(reaction);
if (this.useReactions) {
for (const reaction of DuelRequest.REACTIONS) {
await message.react(reaction);
}

message
.awaitReactions({
filter: (reaction, user) =>
reaction.emoji.name != null &&
DuelRequest.REACTIONS.includes(reaction.emoji.name) &&
user.id === this.invited.id,
max: 1,
time: this.expireTime * 1000,
errors: ['time']
})
.then(this.challengeEmojiAnswered.bind(this))
.catch(this.challengeExpired.bind(this));
} else {
message
.createMessageComponentCollector({
filter: interaction => interaction.user.id === this.invited.id,
max: 1,
time: this.expireTime * 1000
})
.on('collect', this.challengeButtonAnswered.bind(this))
.on('end', async (_, reason) => {
if (reason !== 'limit') {
await this.challengeExpired();
}
});
}
}

message
.awaitReactions({
filter: (reaction, user) =>
reaction.emoji.name != null &&
DuelRequest.REACTIONS.includes(reaction.emoji.name) &&
user.id === this.invited.id,
max: 1,
time: this.expireTime * 1000,
errors: ['time']
})
.then(this.challengeAnswered.bind(this))
.catch(this.challengeExpired.bind(this));
/**
* Called when the invited user answered to the request using a button.
*
* @param interaction interaction that has operated challenge answer
* @private
*/
private async challengeButtonAnswered(interaction: MessageComponentInteraction): Promise<void> {
// now that an interaction using buttons has been operated on message, use it
this.tunnel = new ComponentInteractionMessagingTunnel(interaction);
return this.challengeAnswered(interaction.customId === 'yes');
}

/**
* Called when the invited user answered to the request.
* Called when the invited user answered to the request using an emoji.
*
* @param collected collection with all reactions added
*/
private async challengeAnswered(
private async challengeEmojiAnswered(
collected: Collection<Snowflake, MessageReaction>
): Promise<void> {
if (collected.first()!.emoji.name === DuelRequest.REACTIONS[0]) {
return this.challengeAnswered(collected.first()!.emoji.name === DuelRequest.REACTIONS[0]);
}

/**
* Called when the invited user answered to the request.
*
* @param accepted true if user accepted the request, false otherwise
* @param rejectFunc function called to reject the duel request
*/
private async challengeAnswered(accepted: boolean): Promise<void> {
if (accepted) {
await this.tunnel.end();
await this.manager.createGame(this.tunnel, this.invited);
return this.manager.createGame(this.tunnel, this.invited);
} else {
await this.tunnel.end({
return this.tunnel.end({
allowedMentions: { parse: [] },
content: localize.__('duel.reject', { invited: this.invited.displayName })
components: [],
content: localize.__('duel.reject', { invited: this.invited.displayName }),
embeds: []
});
}
}
Expand All @@ -133,9 +199,11 @@ export default class DuelRequest {
* Called if the challenge has expired without answer.
*/
private async challengeExpired(): Promise<void> {
await this.tunnel.end({
return this.tunnel.end({
allowedMentions: { parse: [] },
content: localize.__('duel.expire', { invited: this.invited.displayName })
components: [],
content: localize.__('duel.expire', { invited: this.invited.displayName }),
embeds: []
});
}
}
2 changes: 1 addition & 1 deletion src/bot/entity/GameBoard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export default class GameBoard {
/**
* Called when a player has selected a valid move button.
*
* @param collected collected data from discordjs
* @param interaction interaction that has operated move request
* @private
*/
private async onButtonMoveSelected(interaction: ButtonInteraction): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CommandInteraction, GuildMember, Message, TextChannel } from 'discord.j
* @author Utarwyn
* @since 2.2.0
*/
export default class InteractionMessagingTunnel extends MessagingTunnel {
export default class CommandInteractionMessagingTunnel extends MessagingTunnel {
/**
* Interaction object retrieved from the Discord API
* @private
Expand Down Expand Up @@ -85,8 +85,7 @@ export default class InteractionMessagingTunnel extends MessagingTunnel {
public async end(reason?: MessagingAnswer): Promise<void> {
if (this.reply) {
try {
await this.editReply(reason ?? { content: '.' });
await this.reply.suppressEmbeds(true);
await this.editReply(reason ?? { content: '.', components: [], embeds: [] });
await this.reply.reactions.removeAll();
} catch {
// ignore api error
Expand Down