diff --git a/.dev.env b/.dev.env index 1e90070..0089c5e 100644 --- a/.dev.env +++ b/.dev.env @@ -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 diff --git a/.stage.env b/.stage.env index 389c429..19590a2 100644 --- a/.stage.env +++ b/.stage.env @@ -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 diff --git a/src/Functions/CardSetupFunctions.ts b/src/Functions/CardSetupFunction.ts similarity index 87% rename from src/Functions/CardSetupFunctions.ts rename to src/Functions/CardSetupFunction.ts index 8e7fbf4..a9098ec 100644 --- a/src/Functions/CardSetupFunctions.ts +++ b/src/Functions/CardSetupFunction.ts @@ -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); } diff --git a/src/bot.ts b/src/bot.ts index baca574..e8c1dd3 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -32,5 +32,6 @@ const client = new CoreClient([ Registry.RegisterCommands(); Registry.RegisterEvents(); +Registry.RegisterButtonEvents(); client.start(); \ No newline at end of file diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts new file mode 100644 index 0000000..5412db8 --- /dev/null +++ b/src/buttonEvents/Claim.ts @@ -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'); + } +} \ No newline at end of file diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts new file mode 100644 index 0000000..f1c4bcc --- /dev/null +++ b/src/buttonEvents/Reroll.ts @@ -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(); + + 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; + } +} \ No newline at end of file diff --git a/src/client/client.ts b/src/client/client.ts index a928d3b..b517a28 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -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); + } } diff --git a/src/client/events.ts b/src/client/events.ts index bfbe7ec..db6cfc4 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -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 diff --git a/src/client/interactionCreate/Button.ts b/src/client/interactionCreate/Button.ts new file mode 100644 index 0000000..165e426 --- /dev/null +++ b/src/client/interactionCreate/Button.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/client/interactionCreate/ChatInputCommand.ts b/src/client/interactionCreate/ChatInputCommand.ts new file mode 100644 index 0000000..d483f1d --- /dev/null +++ b/src/client/interactionCreate/ChatInputCommand.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/commands/drop.ts b/src/commands/drop.ts new file mode 100644 index 0000000..861fb18 --- /dev/null +++ b/src/commands/drop.ts @@ -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(); + + 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; + } +} \ No newline at end of file diff --git a/src/constants/CardRarity.ts b/src/constants/CardRarity.ts index 541ff36..92a5141 100644 --- a/src/constants/CardRarity.ts +++ b/src/constants/CardRarity.ts @@ -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; + } } \ No newline at end of file diff --git a/src/constants/EmbedColours.ts b/src/constants/EmbedColours.ts index 023c77a..d889f5d 100644 --- a/src/constants/EmbedColours.ts +++ b/src/constants/EmbedColours.ts @@ -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; } \ No newline at end of file diff --git a/src/contracts/IButtonEventItem.ts b/src/contracts/IButtonEventItem.ts new file mode 100644 index 0000000..6be9f6e --- /dev/null +++ b/src/contracts/IButtonEventItem.ts @@ -0,0 +1,6 @@ +import { ButtonEvent } from "../type/buttonEvent"; + +export default interface IButtonEventItem { + ButtonId: string, + Event: ButtonEvent, +} \ No newline at end of file diff --git a/src/database/entities/app/.gitkeep b/src/database/entities/app/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/database/entities/app/Inventory.ts b/src/database/entities/app/Inventory.ts new file mode 100644 index 0000000..c6319a9 --- /dev/null +++ b/src/database/entities/app/Inventory.ts @@ -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 { + const repository = AppDataSource.getRepository(Inventory); + + const single = await repository.findOne({ where: { UserId: userId, CardNumber: cardNumber }}); + + return single; + } + + public static async FetchOneByClaimId(claimId: string): Promise { + const repository = AppDataSource.getRepository(Inventory); + + const single = await repository.findOne({ where: { ClaimId: claimId }}); + + return single; + } +} \ No newline at end of file diff --git a/src/database/entities/card/Card.ts b/src/database/entities/card/Card.ts index 0a368f9..f354fd6 100644 --- a/src/database/entities/card/Card.ts +++ b/src/database/entities/card/Card.ts @@ -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 { - const repository = CardDataSource.getRepository(Card); - - const all = await repository.find({ where: { Rarity: rarity }}); - - return all; - } } \ No newline at end of file diff --git a/src/database/entities/card/Series.ts b/src/database/entities/card/Series.ts index 5a8dca3..eb990cc 100644 --- a/src/database/entities/card/Series.ts +++ b/src/database/entities/card/Series.ts @@ -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); - } } \ No newline at end of file diff --git a/src/helpers/CardDropHelper.ts b/src/helpers/CardDropHelper.ts new file mode 100644 index 0000000..9ab63ca --- /dev/null +++ b/src/helpers/CardDropHelper.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/src/registry.ts b/src/registry.ts index 774015a..c517af3 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -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()); + } } \ No newline at end of file diff --git a/src/type/buttonEvent.ts b/src/type/buttonEvent.ts new file mode 100644 index 0000000..3a691cf --- /dev/null +++ b/src/type/buttonEvent.ts @@ -0,0 +1,7 @@ +import { ButtonInteraction } from "discord.js"; + +export class ButtonEvent { + public execute(interaction: ButtonInteraction) { + + } +} \ No newline at end of file