diff --git a/migrations/committed/000015-daily-and-vgnyj-error-functions.sql b/migrations/committed/000015-daily-and-vgnyj-error-functions.sql new file mode 100644 index 0000000..d3e43d8 --- /dev/null +++ b/migrations/committed/000015-daily-and-vgnyj-error-functions.sql @@ -0,0 +1,137 @@ +--! Previous: sha1:2a39b3b006a8d5ec5d30ac1e5c2158d282b99acb +--! Hash: sha1:1390238abcc8e013a87b670fbcb3ae127a51ece8 +--! Message: daily and VGNYJ error functions + +--- Calculates the date at which /daily can be used again after the given timestamp. +CREATE OR REPLACE FUNCTION NextDailyCommand(IN lastDaily timestamp with time zone) + RETURNS timestamp with time zone + CALLED ON NULL INPUT + IMMUTABLE +AS +$$ +SELECT (COALESCE(lastDaily, TIMESTAMP '-infinity' AT TIME ZONE 'UTC')::date + 1)::timestamp AT TIME ZONE 'UTC' +$$ LANGUAGE 'sql'; + +--- Raises the VGNYJ error. +CREATE OR REPLACE PROCEDURE Error_NotYetJoined() AS +$$ +BEGIN + RAISE EXCEPTION USING + ERRCODE = 'VGNYJ', + MESSAGE = 'Not yet joined', + DETAIL = 'You haven''t joined the game yet, and can''t use this command until you do.', + HINT = 'Use the /join command to join the game!'; +END; +$$ LANGUAGE 'plpgsql'; + +--- Runs the full /daily command. +--- Error codes: +--- VGBCG: Bad channel (game). This is not a valid channel to send game commands in. +--- VGBGG: Bad guild (game). This is not a valid guild to send game commands in. +--- VGNYJ: Not yet joined. The Discord user using has not joined the game yet. +--- VGDLY: Already used today. +CREATE OR REPLACE FUNCTION Command_Daily( + IN requestedChannel DiscordChannel.discordId%TYPE, + IN requestedGuild DiscordChannel.guildId%TYPE, + IN forId DiscordUser.discordId%TYPE, + IN newUsername DiscordUser.username%TYPE, + IN newDiscriminator DiscordUser.discriminator%TYPE, + OUT newLastDaily Player.lastDaily%TYPE, + OUT newNextDaily Player.lastDaily%TYPE, + OUT newCurrency Player.currency%TYPE, + OUT bonus Player.currency%TYPE) + STRICT + VOLATILE +AS +$$ +DECLARE + playerId Player.id%TYPE; + playerLastDaily Player.lastDaily%TYPE; + playerNextDaily Player.lastDaily%TYPE; +BEGIN + CALL CheckGameCommandIn(requestedChannel, requestedGuild); + SELECT InvokingPlayer.id, InvokingPlayer.lastDaily, NextDailyCommand(InvokingPlayer.lastdaily) + INTO playerId, playerLastDaily, playerNextDaily + FROM GetInvokingPlayer(forId, newUsername, newDiscriminator) AS InvokingPlayer; + IF playerId IS NULL THEN + CALL Error_NotYetJoined(); + END IF; + bonus = 100; + IF playerNextDaily > NOW() THEN + RAISE EXCEPTION USING + ERRCODE = 'VGDLY', + MESSAGE = 'Already used today', + DETAIL = format('You last used your daily command at . You can use it again at .', + extract(EPOCH FROM playerLastDaily)::INT, + extract(EPOCH FROM playerNextDaily)::INT), + HINT = format('Wait and you can use the /daily command again to get some more currency!', + extract(EPOCH FROM playerNextDaily)::INT); + ELSE + UPDATE Player + SET currency = currency + bonus, + lastDaily = NOW() + WHERE id = playerId + RETURNING lastDaily, currency INTO newLastDaily, newCurrency; + newNextDaily = NextDailyCommand(newLastDaily); + END IF; +END; +$$ LANGUAGE 'plpgsql'; + +--- Runs the full /pull command. +--- Error codes: +--- VGBCG: Bad channel (game). This is not a valid channel to send game commands in. +--- VGBGG: Bad guild (game). This is not a valid guild to send game commands in. +--- VGNYJ: Not yet joined. The Discord user using has not joined the game yet. +--- VGNEC: Not enough currency. +CREATE OR REPLACE FUNCTION Command_Pull( + IN requestedChannel DiscordChannel.discordId%TYPE, + IN requestedGuild DiscordChannel.guildId%TYPE, + IN forId DiscordUser.discordId%TYPE, + IN newUsername DiscordUser.username%TYPE, + IN newDiscriminator DiscordUser.discriminator%TYPE, + IN count INT +) + RETURNS TABLE + ( + summonedUnitInstanceId SummonedUnit.instanceId%TYPE, + summonedUnitId Unit.id%TYPE, + summonedUnitName Unit.name%TYPE, + summonedUnitSubtitle Unit.subtitle%TYPE, + summonedUnitTierName UnitTier.name%TYPE, + firstTimePull BOOLEAN, + wasAlreadySummoned BOOLEAN + ) + STRICT + VOLATILE + ROWS 10 +AS +$$ +DECLARE + playerId Player.id%TYPE; + cost Player.currency%TYPE; + oldCurrency Player.currency%TYPE; + playerLastDaily Player.lastDaily%TYPE; +BEGIN + CALL CheckGameCommandIn(requestedChannel, requestedGuild); + SELECT InvokingPlayer.id, InvokingPlayer.currency, InvokingPlayer.lastDaily + INTO playerId, oldCurrency, playerLastDaily + FROM GetInvokingPlayer(forId, newUsername, newDiscriminator) AS InvokingPlayer; + IF playerId IS NULL THEN + CALL Error_NotYetJoined(); + END IF; + cost = 10 * count; + UPDATE Player SET currency = currency - cost WHERE id = playerId AND currency >= cost; + IF NOT FOUND THEN + RAISE EXCEPTION USING + ERRCODE = 'VGNEC', + MESSAGE = 'Not enough currency', + DETAIL = format('Pulling %s heroines would cost %s currency, but you only have %s currency.', + count, cost, oldCurrency), + HINT = CASE NextDailyCommand(playerLastDaily) > NOW() + WHEN FALSE THEN 'You can use the /daily command to get some more currency for today!' + ELSE format('You can use the /daily command again to get some more currency!', + extract(EPOCH FROM NextDailyCommand(playerLastDaily))::INT) + END; + END IF; +END; +$$ LANGUAGE 'plpgsql'; diff --git a/src/GameServer.ts b/src/GameServer.ts index cb9df67..c0ae335 100644 --- a/src/GameServer.ts +++ b/src/GameServer.ts @@ -7,6 +7,7 @@ import {PullCommand} from "./commands/game/PullCommand.js"; import {JoinCommand} from "./commands/game/JoinCommand.js"; import {singleColumnQueryResult} from "./queries/QueryHelpers.js"; import {UnjoinCommand} from "./commands/debug/UnjoinCommand.js"; +import {DailyCommand} from "./commands/game/DailyCommand.js"; export class GameServer extends BaseServer { @@ -33,6 +34,10 @@ export class GameServer extends BaseServer { pool: this.pool, gameGuildIds: singleColumnQueryResult(await promisedGameGuildIds), })) + this.slashcmd.registerCommand(new DailyCommand(this.slashcmd, { + pool: this.pool, + gameGuildIds: singleColumnQueryResult(await promisedGameGuildIds), + })) this.server.get("/game/started", async (req, res) => { const token = await generateXSRFCookie(res) res.code(200) diff --git a/src/commands/game/DailyCommand.ts b/src/commands/game/DailyCommand.ts new file mode 100644 index 0000000..9c608fd --- /dev/null +++ b/src/commands/game/DailyCommand.ts @@ -0,0 +1,47 @@ +import {CommandContext, SlashCommand, SlashCreator} from "slash-create"; +import {Snowflake} from "discord-api-types"; +import {Pool} from "pg"; +import {sendErrorMessage} from "../../queries/ErrorCodes.js"; +import {singleRowQueryResult} from "../../queries/QueryHelpers.js"; + +export class DailyCommand extends SlashCommand { + readonly pool: Pool + + constructor(creator: SlashCreator, {pool, gameGuildIds}: { + pool: Pool, + gameGuildIds: Snowflake[] + }) { + super(creator, { + name: "daily", + guildIDs: gameGuildIds, + description: "Gathers your daily currency once per day, resetting at midnight UTC.", + options: [] + }); + + this.pool = pool + } + + async run(ctx: CommandContext): Promise { + try { + const result = singleRowQueryResult(await this.pool.query<{ newlastdaily: Date, newnextdaily: Date, newcurrency: number, bonus: number }>({ + text: `SELECT * + FROM Command_Daily($1, $2, $3, $4, $5)`, + values: [ctx.channelID, ctx.guildID, ctx.user.id, ctx.user.username, ctx.user.discriminator], + })) + if (result === undefined) { + await ctx.send({ + content: "Unexpectedly got no results!!", + ephemeral: true, + }) + console.log("Unexpectedly empty Command_Join result!") + return; + } + return ctx.send({ + content: `Nice! You reap ${result.bonus} currency from the void, leaving you with a total of ${result.newcurrency}!\n\nYou collected your currency now at and can gather it again in at .`, + ephemeral: true, + }) + } catch (e) { + await sendErrorMessage(ctx, e) + } + } +} \ No newline at end of file diff --git a/src/queries/ErrorCodes.ts b/src/queries/ErrorCodes.ts index 015402b..974e756 100644 --- a/src/queries/ErrorCodes.ts +++ b/src/queries/ErrorCodes.ts @@ -8,6 +8,7 @@ export enum ErrorCodes { BAD_GUILD_GAME = "VGBGG", NOT_YET_JOINED = "VGNYJ", NOT_ENOUGH_CURRENCY = "VGNEC", + DAILY_ALREADY_USED = "VGDLY", } /** Checks if the error is a database error. */ @@ -26,6 +27,7 @@ export async function sendErrorMessage(ctx: CommandContext, err: unknown): Promi case ErrorCodes.BAD_GUILD_GAME: case ErrorCodes.NOT_YET_JOINED: case ErrorCodes.NOT_ENOUGH_CURRENCY: + case ErrorCodes.DAILY_ALREADY_USED: await ctx.send({ content: `**${err.message}**\n${err.detail}\n\n**Tip**: ${err.hint}`, ephemeral: true,