feature/5-drop-command (#17)

#5

Reviewed-on: https://gitea.vylpes.xyz/External/card-drop/pulls/17
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
This commit is contained in:
Ethan Lane 2023-09-03 20:27:29 +01:00 committed by Vylpes
parent 51d97bacd5
commit 58d1541e47
21 changed files with 382 additions and 48 deletions

View file

@ -7,7 +7,7 @@
# any secret values.
BOT_TOKEN=
BOT_VER=0.1.0
BOT_VER=0.1.0 DEV
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=682942374040961060

View file

@ -7,7 +7,7 @@
# any secret values.
BOT_TOKEN=
BOT_VER=0.1.0
BOT_VER=0.1.0 BETA
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=1016767908740857949

View file

@ -5,7 +5,7 @@ import Series from "../database/entities/card/Series";
import path from "path";
import { CardRarity } from "../constants/CardRarity";
export default class CardSetupFunctions {
export default class CardSetupFunction {
public async Execute() {
await this.ClearDatabase();
await this.ReadSeries();
@ -42,7 +42,7 @@ export default class CardSetupFunctions {
}
private async ReadCards() {
const loadedSeries = await Series.FetchAll(Series);
const loadedSeries = await Series.FetchAll(Series, [ "Cards", "Cards.Series" ]);
const cardRepository = CardDataSource.getRepository(Card);
@ -65,7 +65,7 @@ export default class CardSetupFunctions {
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Bronze);
const card = new Card(cardId, cardName, CardRarity.Bronze, path.join(path.join(process.cwd(), 'cards', series.Path, 'BRONZE', file)), series);
cardsToSave.push(card);
}
@ -76,7 +76,7 @@ export default class CardSetupFunctions {
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Gold);
const card = new Card(cardId, cardName, CardRarity.Gold, path.join(path.join(process.cwd(), 'cards', series.Path, 'GOLD', file)), series);
cardsToSave.push(card);
}
@ -87,7 +87,7 @@ export default class CardSetupFunctions {
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Legendary);
const card = new Card(cardId, cardName, CardRarity.Legendary, path.join(path.join(process.cwd(), 'cards', series.Path, 'LEGENDARY', file)), series);
cardsToSave.push(card);
}
@ -98,7 +98,7 @@ export default class CardSetupFunctions {
const cardId = filePart[0];
const cardName = filePart[0];
const card = new Card(cardId, cardName, CardRarity.Silver);
const card = new Card(cardId, cardName, CardRarity.Silver, path.join(path.join(process.cwd(), 'cards', series.Path, 'SILVER', file)), series);
cardsToSave.push(card);
}

View file

@ -32,5 +32,6 @@ const client = new CoreClient([
Registry.RegisterCommands();
Registry.RegisterEvents();
Registry.RegisterButtonEvents();
client.start();

38
src/buttonEvents/Claim.ts Normal file
View file

@ -0,0 +1,38 @@
import { ButtonInteraction } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import Inventory from "../database/entities/app/Inventory";
import { CoreClient } from "../client/client";
export default class Claim extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
const cardNumber = interaction.customId.split(' ')[1];
const claimId = interaction.customId.split(' ')[2];
const userId = interaction.user.id;
const claimed = await Inventory.FetchOneByClaimId(claimId);
if (claimed) {
await interaction.reply('This card has already been claimed');
return;
}
if (claimId != CoreClient.ClaimId) {
await interaction.reply('This card has expired');
return;
}
let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!inventory) {
inventory = new Inventory(userId, cardNumber, 1, claimId);
} else {
inventory.SetQuantity(inventory.Quantity + 1);
}
await inventory.Save(Inventory, inventory);
await interaction.reply('Card claimed');
}
}

View file

@ -0,0 +1,48 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, CacheType, EmbedBuilder } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import CardDropHelper from "../helpers/CardDropHelper";
import { readFileSync } from "fs";
import { CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import { v4 } from "uuid";
import { CoreClient } from "../client/client";
export default class Reroll extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
const randomCard = await CardDropHelper.GetRandomCard();
const image = readFileSync(randomCard.Path);
const attachment = new AttachmentBuilder(image, { name: `${randomCard.Id}.png` });
const embed = new EmbedBuilder()
.setTitle(randomCard.Name)
.setDescription(randomCard.Series.Name)
.setFooter({ text: CardRarityToString(randomCard.Rarity) })
.setColor(CardRarityToColour(randomCard.Rarity))
.setImage(`attachment://${randomCard.Id}.png`);
const row = new ActionRowBuilder<ButtonBuilder>();
const claimId = v4();
row.addComponents(
new ButtonBuilder()
.setCustomId(`claim ${randomCard.CardNumber} ${claimId}`)
.setLabel("Claim")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`reroll`)
.setLabel("Reroll")
.setStyle(ButtonStyle.Secondary));
await interaction.reply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
CoreClient.ClaimId = claimId;
}
}

View file

@ -7,14 +7,23 @@ import { Command } from "../type/command";
import { Events } from "./events";
import { Util } from "./util";
import CardSetupFunction from "../Functions/CardSetupFunction";
import CardDataSource from "../database/dataSources/cardDataSource";
import CardDropHelper from "../helpers/CardDropHelper";
import IButtonEventItem from "../contracts/IButtonEventItem";
import { ButtonEvent } from "../type/buttonEvent";
import AppDataSource from "../database/dataSources/appDataSource";
export class CoreClient extends Client {
private static _commandItems: ICommandItem[];
private static _eventItems: IEventItem[];
private static _buttonEvents: IButtonEventItem[];
private _events: Events;
private _util: Util;
private _cardSetupFunc: CardSetupFunction;
public static ClaimId: string;
public static get commandItems(): ICommandItem[] {
return this._commandItems;
@ -24,15 +33,21 @@ export class CoreClient extends Client {
return this._eventItems;
}
public static get buttonEvents(): IButtonEventItem[] {
return this._buttonEvents;
}
constructor(intents: number[]) {
super({ intents: intents });
dotenv.config();
CoreClient._commandItems = [];
CoreClient._eventItems = [];
CoreClient._buttonEvents = [];
this._events = new Events();
this._util = new Util();
this._cardSetupFunc = new CardSetupFunction();
}
public async start() {
@ -42,12 +57,18 @@ export class CoreClient extends Client {
}
await AppDataSource.initialize()
.then(() => console.log("Data Source Initialized"))
.catch((err) => console.error("Error Initialising Data Source", err));
.then(() => console.log("App Data Source Initialised"))
.catch(err => console.error("Error initialising App Data Source", err));
await CardDataSource.initialize()
.then(() => console.log("Card Data Source Initialised"))
.catch(err => console.error("Error initialising Card Data Source", err));
super.on("interactionCreate", this._events.onInteractionCreate);
super.on("ready", this._events.onReady);
await this._cardSetupFunc.Execute();
await super.login(process.env.BOT_TOKEN);
this._util.loadEvents(this, CoreClient._eventItems);
@ -72,4 +93,13 @@ export class CoreClient extends Client {
CoreClient._eventItems.push(item);
}
public static RegisterButtonEvent(buttonId: string, event: ButtonEvent) {
const item: IButtonEventItem = {
ButtonId: buttonId,
Event: event,
};
CoreClient._buttonEvents.push(item);
}
}

View file

@ -1,29 +1,18 @@
import { Interaction } from "discord.js";
import ICommandItem from "../contracts/ICommandItem";
import { CoreClient } from "./client";
import ChatInputCommand from "./interactionCreate/ChatInputCommand";
import Button from "./interactionCreate/Button";
export class Events {
public async onInteractionCreate(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return;
if (!interaction.guildId) return;
const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId);
const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId);
let itemToUse: ICommandItem;
if (!itemForServer) {
if (!item) {
await interaction.reply('Command not found');
return;
}
itemToUse = item;
} else {
itemToUse = itemForServer;
if (interaction.isChatInputCommand()) {
ChatInputCommand.onChatInput(interaction);
}
itemToUse.Command.execute(interaction);
if (interaction.isButton()) {
Button.onButtonClicked(interaction);
}
}
// Emit when bot is logged in and ready to use

View file

@ -0,0 +1,17 @@
import { ButtonInteraction, Interaction } from "discord.js";
import { CoreClient } from "../client";
export default class Button {
public static async onButtonClicked(interaction: ButtonInteraction) {
if (!interaction.isButton) return;
const item = CoreClient.buttonEvents.find(x => x.ButtonId == interaction.customId.split(' ')[0]);
if (!item) {
await interaction.reply('Event not found');
return;
}
item.Event.execute(interaction);
}
}

View file

@ -0,0 +1,27 @@
import { Interaction } from "discord.js";
import { CoreClient } from "../client";
import ICommandItem from "../../contracts/ICommandItem";
export default class ChatInputCommand {
public static async onChatInput(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return;
const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId);
const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId);
let itemToUse: ICommandItem;
if (!itemForServer) {
if (!item) {
await interaction.reply('Command not found');
return;
}
itemToUse = item;
} else {
itemToUse = itemForServer;
}
itemToUse.Command.execute(interaction);
}
}

54
src/commands/drop.ts Normal file
View file

@ -0,0 +1,54 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import CardDropHelper from "../helpers/CardDropHelper";
import { CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import { readFileSync } from "fs";
import { CoreClient } from "../client/client";
import { v4 } from "uuid";
export default class Drop extends Command {
constructor() {
super();
super.CommandBuilder = new SlashCommandBuilder()
.setName('drop')
.setDescription('Summon a new card drop');
}
public override async execute(interaction: CommandInteraction) {
const randomCard = await CardDropHelper.GetRandomCard();
const image = readFileSync(randomCard.Path);
const attachment = new AttachmentBuilder(image, { name: `${randomCard.Id}.png` });
const embed = new EmbedBuilder()
.setTitle(randomCard.Name)
.setDescription(randomCard.Series.Name)
.setFooter({ text: CardRarityToString(randomCard.Rarity) })
.setColor(CardRarityToColour(randomCard.Rarity))
.setImage(`attachment://${randomCard.Id}.png`);
const row = new ActionRowBuilder<ButtonBuilder>();
const claimId = v4();
row.addComponents(
new ButtonBuilder()
.setCustomId(`claim ${randomCard.CardNumber} ${claimId}`)
.setLabel("Claim")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(`reroll`)
.setLabel("Reroll")
.setStyle(ButtonStyle.Secondary));
await interaction.reply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
CoreClient.ClaimId = claimId;
}
}

View file

@ -1,6 +1,34 @@
import EmbedColours from "./EmbedColours";
export enum CardRarity {
Bronze,
Silver,
Gold,
Legendary,
}
export function CardRarityToString(rarity: CardRarity): string {
switch (rarity) {
case CardRarity.Bronze:
return "Bronze";
case CardRarity.Silver:
return "Silver";
case CardRarity.Gold:
return "Gold";
case CardRarity.Legendary:
return "Legendary";
}
}
export function CardRarityToColour(rarity: CardRarity): number {
switch (rarity) {
case CardRarity.Bronze:
return EmbedColours.BronzeCard;
case CardRarity.Silver:
return EmbedColours.SilverCard;
case CardRarity.Gold:
return EmbedColours.GoldCard;
case CardRarity.Legendary:
return EmbedColours.LegendaryCard;
}
}

View file

@ -1,3 +1,7 @@
export default class EmbedColours {
public static readonly Ok = 0x3050ba;
public static readonly BronzeCard = 0xcd7f32;
public static readonly SilverCard = 0xc0c0c0;
public static readonly GoldCard = 0xffd700;
public static readonly LegendaryCard = 0x50c878;
}

View file

@ -0,0 +1,6 @@
import { ButtonEvent } from "../type/buttonEvent";
export default interface IButtonEventItem {
ButtonId: string,
Event: ButtonEvent,
}

View file

@ -0,0 +1,47 @@
import { Column, Entity } from "typeorm";
import AppBaseEntity from "../../../contracts/AppBaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity()
export default class Inventory extends AppBaseEntity {
constructor(userId: string, cardNumber: string, quantity: number, claimId: string) {
super();
this.UserId = userId;
this.CardNumber = cardNumber;
this.Quantity = quantity;
this.ClaimId = claimId;
}
@Column()
UserId: string;
@Column()
CardNumber: string;
@Column()
Quantity: number;
@Column()
ClaimId: string;
public SetQuantity(quantity: number) {
this.Quantity = quantity;
}
public static async FetchOneByCardNumberAndUserId(userId: string, cardNumber: string): Promise<Inventory | null> {
const repository = AppDataSource.getRepository(Inventory);
const single = await repository.findOne({ where: { UserId: userId, CardNumber: cardNumber }});
return single;
}
public static async FetchOneByClaimId(claimId: string): Promise<Inventory | null> {
const repository = AppDataSource.getRepository(Inventory);
const single = await repository.findOne({ where: { ClaimId: claimId }});
return single;
}
}

View file

@ -1,17 +1,18 @@
import { Column, Entity, OneToMany } from "typeorm";
import { Column, Entity, ManyToOne } from "typeorm";
import CardBaseEntity from "../../../contracts/CardBaseEntity";
import { CardRarity } from "../../../constants/CardRarity";
import Series from "./Series";
import CardDataSource from "../../dataSources/cardDataSource";
@Entity()
export default class Card extends CardBaseEntity {
constructor(cardNumber: string, name: string, rarity: CardRarity) {
constructor(cardNumber: string, name: string, rarity: CardRarity, path: string, series: Series) {
super();
this.CardNumber = cardNumber;
this.Name = name;
this.Rarity = rarity;
this.Path = path;
this.Series = series;
}
@Column()
@ -23,14 +24,9 @@ export default class Card extends CardBaseEntity {
@Column()
Rarity: CardRarity;
@OneToMany(() => Series, x => x.Cards)
@Column()
Path: string
@ManyToOne(() => Series, x => x.Cards)
Series: Series;
public static async FetchAllByRarity(rarity: CardRarity): Promise<Card[]> {
const repository = CardDataSource.getRepository(Card);
const all = await repository.find({ where: { Rarity: rarity }});
return all;
}
}

View file

@ -1,4 +1,4 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { Column, Entity, OneToMany } from "typeorm";
import CardBaseEntity from "../../../contracts/CardBaseEntity";
import Card from "./Card";
@ -18,12 +18,6 @@ export default class Series extends CardBaseEntity {
@Column()
Path: string;
@ManyToOne(() => Card, x => x.Series)
@OneToMany(() => Card, x => x.Series)
Cards: Card[];
public async AddCard(card: Card) {
if (!this.Cards) return;
this.Cards.push(card);
}
}

View file

@ -0,0 +1,38 @@
import { CardRarity } from "../constants/CardRarity";
import CardDataSource from "../database/dataSources/cardDataSource";
import Card from "../database/entities/card/Card";
import Series from "../database/entities/card/Series";
export default class CardDropHelper {
public static async GetRandomCard(): Promise<Card> {
const seriesRepository = CardDataSource.getRepository(Series);
const allSeries = await Series.FetchAll(Series, [ "Cards", "Cards.Series" ]);
const allSeriesWithCards = allSeries.filter(x => x.Cards.length > 0);
const randomSeriesIndex = Math.floor(Math.random() * allSeriesWithCards.length);
const randomSeries = allSeriesWithCards[randomSeriesIndex];
const randomRarity = Math.random() * 100;
let cardRarity: CardRarity;
const bronzeChance = 62;
const silverChance = bronzeChance + 31;
const goldChance = silverChance + 6.4;
if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze;
else if (randomRarity < silverChance) cardRarity = CardRarity.Silver;
else if (randomRarity < goldChance) cardRarity = CardRarity.Gold;
else cardRarity = CardRarity.Legendary;
const allCards = randomSeries.Cards.filter(x => x.Rarity == cardRarity);
const randomCardIndex = Math.floor(Math.random() * allCards.length);
const randomCard = allCards[randomCardIndex];
return randomCard;
}
}

View file

@ -1,13 +1,23 @@
import { CoreClient } from "./client/client";
import About from "./commands/about";
import Drop from "./commands/drop";
import Claim from "./buttonEvents/Claim";
import Reroll from "./buttonEvents/Reroll";
export default class Registry {
public static RegisterCommands() {
CoreClient.RegisterCommand('about', new About());
CoreClient.RegisterCommand('drop', new Drop());
}
public static RegisterEvents() {
}
public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent('claim', new Claim());
CoreClient.RegisterButtonEvent('reroll', new Reroll());
}
}

7
src/type/buttonEvent.ts Normal file
View file

@ -0,0 +1,7 @@
import { ButtonInteraction } from "discord.js";
export class ButtonEvent {
public execute(interaction: ButtonInteraction) {
}
}