diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 6df56f2..47a2374 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,22 +1,33 @@ name: Claude Code Review on: - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] jobs: test: + # Only run if comment contains "claude" and is on a PR (not an issue) + # Exclude bot comments to prevent infinite loops + if: | + github.event.issue.pull_request != null && + github.event.comment.author.type != 'Bot' && + contains(github.event.comment.body, 'claude-review') runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read steps: - name: Checkout code uses: actions/checkout@v4 + - name: Checkout PR branch + run: | + gh pr checkout ${{ github.event.issue.number }} + env: + GH_TOKEN: ${{ github.token }} + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -40,15 +51,16 @@ jobs: claude-review: needs: test - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + # Only run if comment contains "claude" and is on a PR (not an issue) + # Exclude bot comments to prevent infinite loops + if: | + github.event.issue.pull_request != null && + github.event.comment.author.type != 'Bot' && + contains(github.event.comment.body, 'claude-review') runs-on: ubuntu-latest permissions: - contents: read + contents: write pull-requests: read issues: read id-token: write @@ -59,6 +71,12 @@ jobs: with: fetch-depth: 1 + - name: Checkout PR branch + run: | + gh pr checkout ${{ github.event.issue.number }} + env: + GH_TOKEN: ${{ github.token }} + - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@v1 @@ -66,7 +84,7 @@ jobs: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: | REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} + PR NUMBER: ${{ github.event.issue.number }} Please review this pull request and provide feedback on: - Code quality and best practices diff --git a/CLAUDE.md b/CLAUDE.md index 07e90a0..40c0e12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,3 +176,9 @@ Required environment variables (typically in `.env`): - OpenAI API key (for AI features) - fal.ai API key (for image generation) - Bot developer Discord user ID(s) + + +## Reminders + +- When wanting to create custom migration, use the following command to generate a migration file: `drizzle-kit generate --custom --name=` +- Always ensure you're running linting, type checks, builds, and tests after completing features \ No newline at end of file diff --git a/FUTURE_PLANS.md b/FUTURE_PLANS.md index 544b18a..1ab8927 100644 --- a/FUTURE_PLANS.md +++ b/FUTURE_PLANS.md @@ -234,9 +234,9 @@ interface GuildWeather { -#### 1.3 Time-Based Fishing (Day/Night Cycle) ⭐⭐⭐ +#### 1.3 Time-Based Fishing (Day/Night Cycle) ⭐⭐⭐ ✅ -**Priority: MEDIUM** | **Complexity: Low** | **Impact: Medium** +**Priority: MEDIUM** | **Complexity: Low** | **Impact: Medium** | **Status: IMPLEMENTED** @@ -2112,7 +2112,7 @@ Fish values fluctuate based on supply and demand. 14. ⚠️ **Prestige System** - Endgame content -15. ⚠️ **Time-Based Fishing** - Day/night cycle +15. ✅ **Time-Based Fishing** - Day/night cycle 16. ⚠️ **Boats/Vessels** - Cooldown/location unlocks diff --git a/drizzle/0007_minor_diamondback.sql b/drizzle/0007_minor_diamondback.sql new file mode 100644 index 0000000..909c941 --- /dev/null +++ b/drizzle/0007_minor_diamondback.sql @@ -0,0 +1,2 @@ +CREATE TYPE "public"."time_of_day_enum" AS ENUM('DAY', 'NIGHT', 'DAWN', 'DUSK', 'ANY');--> statement-breakpoint +ALTER TABLE "catchables" ADD COLUMN "time_of_day" time_of_day_enum DEFAULT 'ANY'; diff --git a/drizzle/0007_minor_diamondback_rollback.sql b/drizzle/0007_minor_diamondback_rollback.sql new file mode 100644 index 0000000..d55d4f0 --- /dev/null +++ b/drizzle/0007_minor_diamondback_rollback.sql @@ -0,0 +1,6 @@ +-- Rollback migration for 0007_minor_diamondback +-- This removes the time_of_day column and enum type +-- WARNING: This will delete all time-based fishing configuration data + +ALTER TABLE "catchables" DROP COLUMN "time_of_day";--> statement-breakpoint +DROP TYPE "public"."time_of_day_enum"; diff --git a/drizzle/0008_update-null-time-of-day.sql b/drizzle/0008_update-null-time-of-day.sql new file mode 100644 index 0000000..96fec08 --- /dev/null +++ b/drizzle/0008_update-null-time-of-day.sql @@ -0,0 +1,7 @@ +-- Custom SQL migration file, put your code below! -- + +-- Data migration: Convert NULL time_of_day values to 'ANY' for consistency +-- This handles legacy catchables that existed before the time-based fishing feature +-- Ensures all catchables have explicit time_of_day values instead of NULL + +UPDATE catchables SET time_of_day = 'ANY' WHERE time_of_day IS NULL; diff --git a/drizzle/0009_fantastic_punisher.sql b/drizzle/0009_fantastic_punisher.sql new file mode 100644 index 0000000..b547735 --- /dev/null +++ b/drizzle/0009_fantastic_punisher.sql @@ -0,0 +1 @@ +ALTER TABLE "catchables" ALTER COLUMN "time_of_day" SET NOT NULL; diff --git a/drizzle/0010_add-catchables-index.sql b/drizzle/0010_add-catchables-index.sql new file mode 100644 index 0000000..6ad724b --- /dev/null +++ b/drizzle/0010_add-catchables-index.sql @@ -0,0 +1,6 @@ +-- Custom SQL migration file, put your code below! -- + +-- Add index on (rarity, time_of_day) for optimized fishing queries +-- This improves performance of the pickCatchableByRarity query +-- which filters by both rarity and time_of_day frequently +CREATE INDEX IF NOT EXISTS idx_catchables_rarity_time ON catchables(rarity, time_of_day); diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..1405455 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,723 @@ +{ + "id": "d8f8375a-f1a3-42fb-bb19-514ab36e6c1a", + "prevId": "64781da7-192b-46dd-ad28-80e8877a7208", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catchables": { + "name": "catchables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "worth": { + "name": "worth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "time_of_day": { + "name": "time_of_day", + "type": "time_of_day_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'ANY'" + }, + "first_caught_by": { + "name": "first_caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catchables_first_caught_by_users_id_fk": { + "name": "catchables_first_caught_by_users_id_fk", + "tableFrom": "catchables", + "tableTo": "users", + "columnsFrom": [ + "first_caught_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catches": { + "name": "catches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catchable_id": { + "name": "catchable_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "caught_by": { + "name": "caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catches_catchable_id_catchables_id_fk": { + "name": "catches_catchable_id_catchables_id_fk", + "tableFrom": "catches", + "tableTo": "catchables", + "columnsFrom": [ + "catchable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "catches_caught_by_users_id_fk": { + "name": "catches_caught_by_users_id_fk", + "tableFrom": "catches", + "tableTo": "users", + "columnsFrom": [ + "caught_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fishing_attempts": { + "name": "fishing_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "guild_id": { + "name": "guild_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fishing_attempts_user_id_users_id_fk": { + "name": "fishing_attempts_user_id_users_id_fk", + "tableFrom": "fishing_attempts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fishing_attempts_guild_id_guilds_id_fk": { + "name": "fishing_attempts_guild_id_guilds_id_fk", + "tableFrom": "fishing_attempts", + "tableTo": "guilds", + "columnsFrom": [ + "guild_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guilds": { + "name": "guilds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "fishing_cooldown_limit": { + "name": "fishing_cooldown_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "fishing_cooldown_window_seconds": { + "name": "fishing_cooldown_window_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guilds_discord_snowflake_unique": { + "name": "guilds_discord_snowflake_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_snowflake" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory": { + "name": "inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inventory_user_id_users_id_fk": { + "name": "inventory_user_id_users_id_fk", + "tableFrom": "inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventory_item_id_items_id_fk": { + "name": "inventory_item_id_items_id_fk", + "tableFrom": "inventory", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "effect_type": { + "name": "effect_type", + "type": "effect_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "effect_value": { + "name": "effect_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_consumable": { + "name": "is_consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_passive": { + "name": "is_passive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "items_slug_unique": { + "name": "items_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchases": { + "name": "purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "shop_id": { + "name": "shop_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "purchases_user_id_users_id_fk": { + "name": "purchases_user_id_users_id_fk", + "tableFrom": "purchases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "purchases_item_id_items_id_fk": { + "name": "purchases_item_id_items_id_fk", + "tableFrom": "purchases", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "purchases_shop_id_shop_id_fk": { + "name": "purchases_shop_id_shop_id_fk", + "tableFrom": "purchases", + "tableTo": "shop", + "columnsFrom": [ + "shop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop": { + "name": "shop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_item_id_items_id_fk": { + "name": "shop_item_id_items_id_fk", + "tableFrom": "shop", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "discord_tag": { + "name": "discord_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "money": { + "name": "money", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_fishing": { + "name": "auto_fishing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_discord_snowflake_unique": { + "name": "users_discord_snowflake_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_snowflake" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.effect_type_enum": { + "name": "effect_type_enum", + "schema": "public", + "values": [ + "RARITY_BOOST", + "WORTH_MULTIPLIER" + ] + }, + "public.time_of_day_enum": { + "name": "time_of_day_enum", + "schema": "public", + "values": [ + "DAY", + "NIGHT", + "DAWN", + "DUSK", + "ANY" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..fa94938 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,723 @@ +{ + "id": "116aff4f-1a52-45c6-9805-c13928e3b5c5", + "prevId": "d8f8375a-f1a3-42fb-bb19-514ab36e6c1a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catchables": { + "name": "catchables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "worth": { + "name": "worth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "time_of_day": { + "name": "time_of_day", + "type": "time_of_day_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'ANY'" + }, + "first_caught_by": { + "name": "first_caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catchables_first_caught_by_users_id_fk": { + "name": "catchables_first_caught_by_users_id_fk", + "tableFrom": "catchables", + "columnsFrom": [ + "first_caught_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catches": { + "name": "catches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catchable_id": { + "name": "catchable_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "caught_by": { + "name": "caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catches_catchable_id_catchables_id_fk": { + "name": "catches_catchable_id_catchables_id_fk", + "tableFrom": "catches", + "columnsFrom": [ + "catchable_id" + ], + "tableTo": "catchables", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "catches_caught_by_users_id_fk": { + "name": "catches_caught_by_users_id_fk", + "tableFrom": "catches", + "columnsFrom": [ + "caught_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fishing_attempts": { + "name": "fishing_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "guild_id": { + "name": "guild_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fishing_attempts_user_id_users_id_fk": { + "name": "fishing_attempts_user_id_users_id_fk", + "tableFrom": "fishing_attempts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "fishing_attempts_guild_id_guilds_id_fk": { + "name": "fishing_attempts_guild_id_guilds_id_fk", + "tableFrom": "fishing_attempts", + "columnsFrom": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guilds": { + "name": "guilds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "fishing_cooldown_limit": { + "name": "fishing_cooldown_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "fishing_cooldown_window_seconds": { + "name": "fishing_cooldown_window_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guilds_discord_snowflake_unique": { + "name": "guilds_discord_snowflake_unique", + "columns": [ + "discord_snowflake" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory": { + "name": "inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inventory_user_id_users_id_fk": { + "name": "inventory_user_id_users_id_fk", + "tableFrom": "inventory", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "inventory_item_id_items_id_fk": { + "name": "inventory_item_id_items_id_fk", + "tableFrom": "inventory", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "effect_type": { + "name": "effect_type", + "type": "effect_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "effect_value": { + "name": "effect_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_consumable": { + "name": "is_consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_passive": { + "name": "is_passive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "items_slug_unique": { + "name": "items_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchases": { + "name": "purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "shop_id": { + "name": "shop_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "purchases_user_id_users_id_fk": { + "name": "purchases_user_id_users_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "purchases_item_id_items_id_fk": { + "name": "purchases_item_id_items_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "purchases_shop_id_shop_id_fk": { + "name": "purchases_shop_id_shop_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "shop_id" + ], + "tableTo": "shop", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop": { + "name": "shop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_item_id_items_id_fk": { + "name": "shop_item_id_items_id_fk", + "tableFrom": "shop", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "discord_tag": { + "name": "discord_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "money": { + "name": "money", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_fishing": { + "name": "auto_fishing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_discord_snowflake_unique": { + "name": "users_discord_snowflake_unique", + "columns": [ + "discord_snowflake" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.effect_type_enum": { + "name": "effect_type_enum", + "schema": "public", + "values": [ + "RARITY_BOOST", + "WORTH_MULTIPLIER" + ] + }, + "public.time_of_day_enum": { + "name": "time_of_day_enum", + "schema": "public", + "values": [ + "DAY", + "NIGHT", + "DAWN", + "DUSK", + "ANY" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..0b567d5 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,723 @@ +{ + "id": "f1d627e2-fc38-405b-84f7-0b7497a159f7", + "prevId": "116aff4f-1a52-45c6-9805-c13928e3b5c5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catchables": { + "name": "catchables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "worth": { + "name": "worth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "time_of_day": { + "name": "time_of_day", + "type": "time_of_day_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ANY'" + }, + "first_caught_by": { + "name": "first_caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catchables_first_caught_by_users_id_fk": { + "name": "catchables_first_caught_by_users_id_fk", + "tableFrom": "catchables", + "tableTo": "users", + "columnsFrom": [ + "first_caught_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catches": { + "name": "catches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catchable_id": { + "name": "catchable_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "caught_by": { + "name": "caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catches_catchable_id_catchables_id_fk": { + "name": "catches_catchable_id_catchables_id_fk", + "tableFrom": "catches", + "tableTo": "catchables", + "columnsFrom": [ + "catchable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "catches_caught_by_users_id_fk": { + "name": "catches_caught_by_users_id_fk", + "tableFrom": "catches", + "tableTo": "users", + "columnsFrom": [ + "caught_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fishing_attempts": { + "name": "fishing_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "guild_id": { + "name": "guild_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fishing_attempts_user_id_users_id_fk": { + "name": "fishing_attempts_user_id_users_id_fk", + "tableFrom": "fishing_attempts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fishing_attempts_guild_id_guilds_id_fk": { + "name": "fishing_attempts_guild_id_guilds_id_fk", + "tableFrom": "fishing_attempts", + "tableTo": "guilds", + "columnsFrom": [ + "guild_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guilds": { + "name": "guilds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "fishing_cooldown_limit": { + "name": "fishing_cooldown_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "fishing_cooldown_window_seconds": { + "name": "fishing_cooldown_window_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guilds_discord_snowflake_unique": { + "name": "guilds_discord_snowflake_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_snowflake" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory": { + "name": "inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inventory_user_id_users_id_fk": { + "name": "inventory_user_id_users_id_fk", + "tableFrom": "inventory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventory_item_id_items_id_fk": { + "name": "inventory_item_id_items_id_fk", + "tableFrom": "inventory", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "effect_type": { + "name": "effect_type", + "type": "effect_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "effect_value": { + "name": "effect_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_consumable": { + "name": "is_consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_passive": { + "name": "is_passive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "items_slug_unique": { + "name": "items_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchases": { + "name": "purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "shop_id": { + "name": "shop_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "purchases_user_id_users_id_fk": { + "name": "purchases_user_id_users_id_fk", + "tableFrom": "purchases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "purchases_item_id_items_id_fk": { + "name": "purchases_item_id_items_id_fk", + "tableFrom": "purchases", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "purchases_shop_id_shop_id_fk": { + "name": "purchases_shop_id_shop_id_fk", + "tableFrom": "purchases", + "tableTo": "shop", + "columnsFrom": [ + "shop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop": { + "name": "shop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_item_id_items_id_fk": { + "name": "shop_item_id_items_id_fk", + "tableFrom": "shop", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "discord_tag": { + "name": "discord_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "money": { + "name": "money", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_fishing": { + "name": "auto_fishing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_discord_snowflake_unique": { + "name": "users_discord_snowflake_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_snowflake" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.effect_type_enum": { + "name": "effect_type_enum", + "schema": "public", + "values": [ + "RARITY_BOOST", + "WORTH_MULTIPLIER" + ] + }, + "public.time_of_day_enum": { + "name": "time_of_day_enum", + "schema": "public", + "values": [ + "DAY", + "NIGHT", + "DAWN", + "DUSK", + "ANY" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..14c0ec7 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,723 @@ +{ + "id": "677b322b-e37d-4482-8c76-c2e982e1f31e", + "prevId": "f1d627e2-fc38-405b-84f7-0b7497a159f7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catchables": { + "name": "catchables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "worth": { + "name": "worth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "time_of_day": { + "name": "time_of_day", + "type": "time_of_day_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ANY'" + }, + "first_caught_by": { + "name": "first_caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catchables_first_caught_by_users_id_fk": { + "name": "catchables_first_caught_by_users_id_fk", + "tableFrom": "catchables", + "columnsFrom": [ + "first_caught_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catches": { + "name": "catches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catchable_id": { + "name": "catchable_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "caught_by": { + "name": "caught_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catches_catchable_id_catchables_id_fk": { + "name": "catches_catchable_id_catchables_id_fk", + "tableFrom": "catches", + "columnsFrom": [ + "catchable_id" + ], + "tableTo": "catchables", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "catches_caught_by_users_id_fk": { + "name": "catches_caught_by_users_id_fk", + "tableFrom": "catches", + "columnsFrom": [ + "caught_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fishing_attempts": { + "name": "fishing_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "guild_id": { + "name": "guild_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fishing_attempts_user_id_users_id_fk": { + "name": "fishing_attempts_user_id_users_id_fk", + "tableFrom": "fishing_attempts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "fishing_attempts_guild_id_guilds_id_fk": { + "name": "fishing_attempts_guild_id_guilds_id_fk", + "tableFrom": "fishing_attempts", + "columnsFrom": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guilds": { + "name": "guilds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "fishing_cooldown_limit": { + "name": "fishing_cooldown_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "fishing_cooldown_window_seconds": { + "name": "fishing_cooldown_window_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guilds_discord_snowflake_unique": { + "name": "guilds_discord_snowflake_unique", + "columns": [ + "discord_snowflake" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory": { + "name": "inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inventory_user_id_users_id_fk": { + "name": "inventory_user_id_users_id_fk", + "tableFrom": "inventory", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "inventory_item_id_items_id_fk": { + "name": "inventory_item_id_items_id_fk", + "tableFrom": "inventory", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "effect_type": { + "name": "effect_type", + "type": "effect_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "effect_value": { + "name": "effect_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_consumable": { + "name": "is_consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_passive": { + "name": "is_passive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "items_slug_unique": { + "name": "items_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchases": { + "name": "purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "shop_id": { + "name": "shop_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "purchases_user_id_users_id_fk": { + "name": "purchases_user_id_users_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "purchases_item_id_items_id_fk": { + "name": "purchases_item_id_items_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "purchases_shop_id_shop_id_fk": { + "name": "purchases_shop_id_shop_id_fk", + "tableFrom": "purchases", + "columnsFrom": [ + "shop_id" + ], + "tableTo": "shop", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop": { + "name": "shop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_item_id_items_id_fk": { + "name": "shop_item_id_items_id_fk", + "tableFrom": "shop", + "columnsFrom": [ + "item_id" + ], + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "discord_snowflake": { + "name": "discord_snowflake", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "discord_tag": { + "name": "discord_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "money": { + "name": "money", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_fishing": { + "name": "auto_fishing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_discord_snowflake_unique": { + "name": "users_discord_snowflake_unique", + "columns": [ + "discord_snowflake" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.effect_type_enum": { + "name": "effect_type_enum", + "schema": "public", + "values": [ + "RARITY_BOOST", + "WORTH_MULTIPLIER" + ] + }, + "public.time_of_day_enum": { + "name": "time_of_day_enum", + "schema": "public", + "values": [ + "DAY", + "NIGHT", + "DAWN", + "DUSK", + "ANY" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index ea0475a..589a1a7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,34 @@ "when": 1763250726594, "tag": "0006_silly_richard_fisk", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1763263984645, + "tag": "0007_minor_diamondback", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1763265878223, + "tag": "0008_update-null-time-of-day", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1763266519702, + "tag": "0009_fantastic_punisher", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1763266620864, + "tag": "0010_add-catchables-index", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/commands/chat/fish-command.ts b/src/commands/chat/fish-command.ts index fa99982..2ce927a 100644 --- a/src/commands/chat/fish-command.ts +++ b/src/commands/chat/fish-command.ts @@ -2,6 +2,7 @@ import { ChatInputCommandInteraction, EmbedBuilder, PermissionsString } from 'di import { RateLimiter } from 'discord.js-rate-limiter'; import { Rarity } from '../../enums/rarity.js'; +import { TimeOfDay } from '../../enums/time-of-day.js'; import { Language } from '../../models/enum-helpers/index.js'; import { EventData } from '../../models/internal-models.js'; import { FishingCooldownService } from '../../services/fishing-cooldown.service.js'; @@ -143,6 +144,11 @@ export class FishCommand implements Command { const rarityColor = this.fishingService.getRarityColor(caught.rarity as Rarity); const newBalance = user.money + finalWorth; + // Get time of day information + const currentTimeOfDay = this.fishingService.getCurrentTimeOfDay(); + const timeOfDayName = this.fishingService.getTimeOfDayName(currentTimeOfDay); + const timeOfDayEmoji = this.fishingService.getTimeOfDayEmoji(currentTimeOfDay); + // Show worth with multiplier indicator if different from base const worthDisplay = finalWorth !== caught.worth ? `${caught.worth} → ${finalWorth}` : `${finalWorth}`; @@ -154,6 +160,14 @@ export class FishCommand implements Command { description += `\n\n🎣 Used **${usedConsumable.name}** (+${(usedConsumable.boost * 100).toFixed(0)}% rarity boost)`; } + // Show time of day info if fish is time-specific + // Database ensures timeOfDay is always set (NOT NULL with DEFAULT 'ANY') + if (caught.timeOfDay !== 'ANY') { + const fishTimeOfDayName = this.fishingService.getTimeOfDayName(caught.timeOfDay as TimeOfDay); + const fishTimeOfDayEmoji = this.fishingService.getTimeOfDayEmoji(caught.timeOfDay as TimeOfDay); + description += `\n\n${fishTimeOfDayEmoji} *Only appears during ${fishTimeOfDayName}*`; + } + // Show remaining attempts if under limit if (remainingAttempts > 0) { description += `\n\n⏰ **${remainingAttempts} attempt${remainingAttempts !== 1 ? 's' : ''} remaining**`; @@ -167,6 +181,7 @@ export class FishCommand implements Command { { name: 'New Balance', value: `${newBalance} coins`, inline: true }, { name: 'Rarity', value: rarityName, inline: true } ) + .setFooter({ text: `${timeOfDayEmoji} Current time: ${timeOfDayName}` }) .setColor(rarityColor); // Add image if available diff --git a/src/db/schema.ts b/src/db/schema.ts index 60cad22..0e22292 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,6 +1,12 @@ import { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import { boolean, integer, numeric, pgEnum, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +// Enum for item effect types +export const effectTypeEnum = pgEnum('effect_type_enum', ['RARITY_BOOST', 'WORTH_MULTIPLIER']); + +// Enum for time of day +export const timeOfDayEnum = pgEnum('time_of_day_enum', ['DAY', 'NIGHT', 'DAWN', 'DUSK', 'ANY']); + export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), discordSnowflake: varchar('discord_snowflake', { length: 255 }).unique().notNull(), @@ -20,6 +26,7 @@ export const catchables = pgTable('catchables', { rarity: integer('rarity').default(0).notNull(), worth: integer('worth').default(0).notNull(), image: varchar('image', { length: 255 }), + timeOfDay: timeOfDayEnum('time_of_day').default('ANY').notNull(), firstCaughtBy: uuid('first_caught_by').references(() => users.id), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), @@ -39,9 +46,6 @@ export const catches = pgTable('catches', { export type Catch = InferSelectModel; export type CatchInsert = InferInsertModel; -// Enum for item effect types -export const effectTypeEnum = pgEnum('effect_type_enum', ['RARITY_BOOST', 'WORTH_MULTIPLIER']); - export const items = pgTable('items', { id: uuid('id').defaultRandom().primaryKey(), name: varchar('name', { length: 255 }).notNull(), diff --git a/src/enums/index.ts b/src/enums/index.ts index 091893a..46bda9f 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -3,3 +3,4 @@ export { HelpOption } from './help-option.js'; export { InfoOption } from './info-option.js'; export { FishingOption } from './fishing-option.js'; export { Rarity } from './rarity.js'; +export { TimeOfDay } from './time-of-day.js'; diff --git a/src/enums/time-of-day.ts b/src/enums/time-of-day.ts new file mode 100644 index 0000000..c04ebff --- /dev/null +++ b/src/enums/time-of-day.ts @@ -0,0 +1,10 @@ +/** + * Time of day for fishing mechanics + */ +export enum TimeOfDay { + DAY = 'DAY', + NIGHT = 'NIGHT', + DAWN = 'DAWN', + DUSK = 'DUSK', + ANY = 'ANY', +} diff --git a/src/services/fishing.service.ts b/src/services/fishing.service.ts index 305abdd..8951b0c 100644 --- a/src/services/fishing.service.ts +++ b/src/services/fishing.service.ts @@ -1,10 +1,11 @@ -import { and, eq, isNull, sql } from 'drizzle-orm'; +import { and, eq, isNull, or, sql } from 'drizzle-orm'; import { getDb } from './database.service.js'; import { ItemEffectsService } from './item-effects.service.js'; import { Logger } from './logger.js'; import { Catchable, catchables, catches, CatchInsert } from '../db/schema.js'; import { Rarity } from '../enums/rarity.js'; +import { TimeOfDay } from '../enums/time-of-day.js'; /** * Rarity weights for random selection @@ -17,6 +18,17 @@ const RARITY_WEIGHTS = { [Rarity.LEGENDARY]: 2, // 2% }; +/** + * Time of day hour boundaries (24-hour format, UTC) + * These define when each time period starts/ends + */ +const TIME_BOUNDARIES = { + DAWN_START: 5, + DAWN_END: 7, + DAY_END: 18, + DUSK_END: 20, +} as const; + /** * Service for managing fishing operations */ @@ -27,6 +39,82 @@ export class FishingService { this.itemEffectsService = new ItemEffectsService(); } + /** + * Determine the current time of day based on the hour (24-hour format) + * Uses UTC timezone for consistency across all users. + * + * Time periods (exclusive upper bounds): + * - Dawn: 5-7 UTC (5:00-6:59) - hours 5, 6 + * - Day: 7-18 UTC (7:00-17:59) - hours 7-17 + * - Dusk: 18-20 UTC (18:00-19:59) - hours 18, 19 + * - Night: 20-5 UTC (20:00-4:59) - hours 20-23, 0-4 + * + * @param hour - Optional hour to check (defaults to current hour in UTC) + * @returns The time of day enum value + */ + public getCurrentTimeOfDay(hour?: number): TimeOfDay { + const currentHour = hour ?? new Date().getUTCHours(); + + // Dawn: 5-7 UTC (5:00-6:59) + if (currentHour >= TIME_BOUNDARIES.DAWN_START && currentHour < TIME_BOUNDARIES.DAWN_END) { + return TimeOfDay.DAWN; + } + // Day: 7-18 UTC (7:00-17:59) + if (currentHour >= TIME_BOUNDARIES.DAWN_END && currentHour < TIME_BOUNDARIES.DAY_END) { + return TimeOfDay.DAY; + } + // Dusk: 18-20 UTC (18:00-19:59) + if (currentHour >= TIME_BOUNDARIES.DAY_END && currentHour < TIME_BOUNDARIES.DUSK_END) { + return TimeOfDay.DUSK; + } + // Night: 20-5 UTC (20:00-4:59) + return TimeOfDay.NIGHT; + } + + /** + * Get a human-readable name for the time of day + * @param timeOfDay - The time of day enum value + * @returns Human-readable name + */ + public getTimeOfDayName(timeOfDay: TimeOfDay): string { + switch (timeOfDay) { + case TimeOfDay.DAY: + return 'Day'; + case TimeOfDay.NIGHT: + return 'Night'; + case TimeOfDay.DAWN: + return 'Dawn'; + case TimeOfDay.DUSK: + return 'Dusk'; + case TimeOfDay.ANY: + return 'Any Time'; + default: + return 'Unknown'; + } + } + + /** + * Get an emoji for the time of day + * @param timeOfDay - The time of day enum value + * @returns Emoji representing the time of day + */ + public getTimeOfDayEmoji(timeOfDay: TimeOfDay): string { + switch (timeOfDay) { + case TimeOfDay.DAY: + return '☀️'; + case TimeOfDay.NIGHT: + return '🌙'; + case TimeOfDay.DAWN: + return '🌅'; + case TimeOfDay.DUSK: + return '🌆'; + case TimeOfDay.ANY: + return '🕐'; + default: + return '❓'; + } + } + /** * Determine rarity based on weighted random selection * @param userId - Optional user ID to apply item effects @@ -72,26 +160,41 @@ export class FishingService { /** * Pick a random catchable by rarity * @param rarity - The rarity level to pick from + * @param timeOfDay - Optional time of day to filter by (defaults to current time) * @returns A random catchable of the specified rarity, or null if none found */ - public async pickCatchableByRarity(rarity: Rarity): Promise { + public async pickCatchableByRarity(rarity: Rarity, timeOfDay?: TimeOfDay): Promise { const db = getDb(); try { - // Get all catchables of this rarity - const availableCatchables = await db + // Determine current time of day if not provided + const currentTimeOfDay = timeOfDay ?? this.getCurrentTimeOfDay(); + + // Get a random catchable of this rarity that is available at this time of day + // Fish are available if their timeOfDay is null (legacy), ANY, or matches the current time + // Using ORDER BY RANDOM() LIMIT 1 for better performance with large datasets + const result = await db .select() .from(catchables) - .where(eq(catchables.rarity, rarity)); + .where( + and( + eq(catchables.rarity, rarity), + or( + isNull(catchables.timeOfDay), + eq(catchables.timeOfDay, TimeOfDay.ANY), + eq(catchables.timeOfDay, currentTimeOfDay) + ) + ) + ) + .orderBy(sql`RANDOM()`) + .limit(1); - if (availableCatchables.length === 0) { - Logger.warn(`[FishingService] No catchables found for rarity ${rarity}`); + if (result.length === 0) { + Logger.warn(`[FishingService] No catchables found for rarity ${rarity} at time ${currentTimeOfDay}`); return null; } - // Pick a random one - const randomIndex = Math.floor(Math.random() * availableCatchables.length); - return availableCatchables[randomIndex]; + return result[0]; } catch (error) { Logger.error(`[FishingService] Failed to pick catchable by rarity ${rarity}:`, error); throw new Error(`Failed to pick catchable: ${error instanceof Error ? error.message : 'Unknown error'}`); diff --git a/tests/integration/fishing-service.integration.test.ts b/tests/integration/fishing-service.integration.test.ts index 7fe5ad3..c95bfc0 100644 --- a/tests/integration/fishing-service.integration.test.ts +++ b/tests/integration/fishing-service.integration.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Catchable } from '../../src/db/schema.js'; import { Rarity } from '../../src/enums/rarity.js'; +import { TimeOfDay } from '../../src/enums/time-of-day.js'; import { FishingService } from '../../src/services/fishing.service.js'; // Mock the database service @@ -32,6 +33,7 @@ describe('FishingService Integration Tests', () => { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), }; @@ -44,42 +46,33 @@ describe('FishingService Integration Tests', () => { describe('pickCatchableByRarity', () => { it('should pick a random catchable from available options', async () => { - const mockCatchables: Catchable[] = [ - { - id: '1', - name: 'Common Fish 1', - rarity: Rarity.COMMON, - worth: 10, - image: '🐟', - firstCaughtBy: null, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: '2', - name: 'Common Fish 2', - rarity: Rarity.COMMON, - worth: 15, - image: '🐠', - firstCaughtBy: null, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; + const mockCatchable: Catchable = { + id: '1', + name: 'Common Fish 1', + rarity: Rarity.COMMON, + worth: 10, + image: '🐟', + firstCaughtBy: null, + createdAt: new Date(), + updatedAt: new Date(), + timeOfDay: null, + }; - // Mock the database query to return our test catchables - mockDb.where.mockResolvedValue(mockCatchables); + // Mock the database query to return a single catchable (LIMIT 1) + // The query chain is: select().from().where().orderBy().limit() + mockDb.limit.mockResolvedValue([mockCatchable]); const result = await fishingService.pickCatchableByRarity(Rarity.COMMON); expect(result).toBeDefined(); - expect(mockCatchables).toContainEqual(result); + expect(result).toEqual(mockCatchable); expect(result?.rarity).toBe(Rarity.COMMON); }); it('should return null when no catchables are found', async () => { // Mock the database query to return empty array - mockDb.where.mockResolvedValue([]); + // The query chain is: select().from().where().orderBy().limit() + mockDb.limit.mockResolvedValue([]); const result = await fishingService.pickCatchableByRarity(Rarity.LEGENDARY); @@ -94,11 +87,17 @@ describe('FishingService Integration Tests', () => { worth: (i + 1) * 10, image: '🐟', firstCaughtBy: null, + timeOfDay: null, createdAt: new Date(), updatedAt: new Date(), })); - mockDb.where.mockResolvedValue(mockCatchables); + // Mock to return one random catchable per call (simulating RANDOM() LIMIT 1) + // The query chain is: select().from().where().orderBy().limit() + mockDb.limit.mockImplementation(() => { + const randomIndex = Math.floor(Math.random() * mockCatchables.length); + return Promise.resolve([mockCatchables[randomIndex]]); + }); // Pick multiple catchables const picks = await Promise.all( @@ -119,8 +118,8 @@ describe('FishingService Integration Tests', () => { }); it('should handle database errors gracefully', async () => { - // Mock a database error - mockDb.where.mockRejectedValue(new Error('Database connection failed')); + // Mock a database error - can occur at any point in the query chain + mockDb.limit.mockRejectedValue(new Error('Database connection failed')); await expect( fishingService.pickCatchableByRarity(Rarity.RARE) @@ -224,4 +223,124 @@ describe('FishingService Integration Tests', () => { expect(result).toBe(baseWorth); }); }); + + describe('Time-Based Fishing', () => { + it('should pick DAY fish during DAY time', async () => { + const dayFish: Catchable = { + id: '1', + name: 'Day Fish', + rarity: Rarity.COMMON, + worth: 10, + image: '🐟', + firstCaughtBy: null, + timeOfDay: 'DAY', + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockDb.limit.mockResolvedValue([dayFish]); + + const result = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.DAY); + + expect(result).toBeDefined(); + expect(result?.name).toBe('Day Fish'); + expect(result?.timeOfDay).toBe('DAY'); + }); + + it('should not pick NIGHT fish during DAY time', async () => { + // Mock database to return empty array (no matching fish) + mockDb.limit.mockResolvedValue([]); + + const result = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.DAY); + + expect(result).toBeNull(); + }); + + it('should always pick ANY time fish regardless of current time', async () => { + const anyTimeFish: Catchable = { + id: '1', + name: 'Any Time Fish', + rarity: Rarity.COMMON, + worth: 10, + image: '🐟', + firstCaughtBy: null, + timeOfDay: 'ANY', + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockDb.limit.mockResolvedValue([anyTimeFish]); + + // Test during different times of day + const dayResult = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.DAY); + expect(dayResult?.timeOfDay).toBe('ANY'); + + const nightResult = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.NIGHT); + expect(nightResult?.timeOfDay).toBe('ANY'); + + const dawnResult = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.DAWN); + expect(dawnResult?.timeOfDay).toBe('ANY'); + + const duskResult = await fishingService.pickCatchableByRarity(Rarity.COMMON, TimeOfDay.DUSK); + expect(duskResult?.timeOfDay).toBe('ANY'); + }); + + it('should pick time-specific fish during their designated time', async () => { + const nightFish: Catchable = { + id: '1', + name: 'Night Fish', + rarity: Rarity.RARE, + worth: 50, + image: '🦈', + firstCaughtBy: null, + timeOfDay: 'NIGHT', + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockDb.limit.mockResolvedValue([nightFish]); + + const result = await fishingService.pickCatchableByRarity(Rarity.RARE, TimeOfDay.NIGHT); + + expect(result).toBeDefined(); + expect(result?.name).toBe('Night Fish'); + expect(result?.timeOfDay).toBe('NIGHT'); + }); + + it('should handle DAWN and DUSK time periods correctly', async () => { + const dawnFish: Catchable = { + id: '1', + name: 'Dawn Fish', + rarity: Rarity.UNCOMMON, + worth: 25, + image: '🐠', + firstCaughtBy: null, + timeOfDay: 'DAWN', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const duskFish: Catchable = { + id: '2', + name: 'Dusk Fish', + rarity: Rarity.UNCOMMON, + worth: 30, + image: '🐡', + firstCaughtBy: null, + timeOfDay: 'DUSK', + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Test DAWN + mockDb.limit.mockResolvedValue([dawnFish]); + const dawnResult = await fishingService.pickCatchableByRarity(Rarity.UNCOMMON, TimeOfDay.DAWN); + expect(dawnResult?.timeOfDay).toBe('DAWN'); + + // Test DUSK + mockDb.limit.mockResolvedValue([duskFish]); + const duskResult = await fishingService.pickCatchableByRarity(Rarity.UNCOMMON, TimeOfDay.DUSK); + expect(duskResult?.timeOfDay).toBe('DUSK'); + }); + }); }); diff --git a/tests/unit/services/fishing.service.test.ts b/tests/unit/services/fishing.service.test.ts index 1d2c70d..8274566 100644 --- a/tests/unit/services/fishing.service.test.ts +++ b/tests/unit/services/fishing.service.test.ts @@ -11,6 +11,7 @@ vi.mock('../../../src/services/logger.js', () => ({ })); import { Rarity } from '../../../src/enums/rarity.js'; +import { TimeOfDay } from '../../../src/enums/time-of-day.js'; import { FishingService } from '../../../src/services/fishing.service.js'; describe('FishingService', () => { @@ -133,4 +134,185 @@ describe('FishingService', () => { }); }); }); + + describe('getCurrentTimeOfDay', () => { + // Test Dawn period (5:00-6:59) + it('should return DAWN for hour 5', () => { + const result = fishingService.getCurrentTimeOfDay(5); + expect(result).toBe(TimeOfDay.DAWN); + }); + + it('should return DAWN for hour 6', () => { + const result = fishingService.getCurrentTimeOfDay(6); + expect(result).toBe(TimeOfDay.DAWN); + }); + + // Test Day period (7:00-17:59) + it('should return DAY for hour 7 (edge case)', () => { + const result = fishingService.getCurrentTimeOfDay(7); + expect(result).toBe(TimeOfDay.DAY); + }); + + it('should return DAY for hour 12 (midday)', () => { + const result = fishingService.getCurrentTimeOfDay(12); + expect(result).toBe(TimeOfDay.DAY); + }); + + it('should return DAY for hour 17', () => { + const result = fishingService.getCurrentTimeOfDay(17); + expect(result).toBe(TimeOfDay.DAY); + }); + + // Test Dusk period (18:00-19:59) + it('should return DUSK for hour 18 (edge case)', () => { + const result = fishingService.getCurrentTimeOfDay(18); + expect(result).toBe(TimeOfDay.DUSK); + }); + + it('should return DUSK for hour 19', () => { + const result = fishingService.getCurrentTimeOfDay(19); + expect(result).toBe(TimeOfDay.DUSK); + }); + + // Test Night period (20:00-4:59) + it('should return NIGHT for hour 20 (edge case)', () => { + const result = fishingService.getCurrentTimeOfDay(20); + expect(result).toBe(TimeOfDay.NIGHT); + }); + + it('should return NIGHT for hour 23', () => { + const result = fishingService.getCurrentTimeOfDay(23); + expect(result).toBe(TimeOfDay.NIGHT); + }); + + it('should return NIGHT for hour 0 (midnight)', () => { + const result = fishingService.getCurrentTimeOfDay(0); + expect(result).toBe(TimeOfDay.NIGHT); + }); + + it('should return NIGHT for hour 4', () => { + const result = fishingService.getCurrentTimeOfDay(4); + expect(result).toBe(TimeOfDay.NIGHT); + }); + + // Test boundary conditions + it('should handle all 24 hours correctly', () => { + const expected = [ + TimeOfDay.NIGHT, // 0 + TimeOfDay.NIGHT, // 1 + TimeOfDay.NIGHT, // 2 + TimeOfDay.NIGHT, // 3 + TimeOfDay.NIGHT, // 4 + TimeOfDay.DAWN, // 5 + TimeOfDay.DAWN, // 6 + TimeOfDay.DAY, // 7 + TimeOfDay.DAY, // 8 + TimeOfDay.DAY, // 9 + TimeOfDay.DAY, // 10 + TimeOfDay.DAY, // 11 + TimeOfDay.DAY, // 12 + TimeOfDay.DAY, // 13 + TimeOfDay.DAY, // 14 + TimeOfDay.DAY, // 15 + TimeOfDay.DAY, // 16 + TimeOfDay.DAY, // 17 + TimeOfDay.DUSK, // 18 + TimeOfDay.DUSK, // 19 + TimeOfDay.NIGHT, // 20 + TimeOfDay.NIGHT, // 21 + TimeOfDay.NIGHT, // 22 + TimeOfDay.NIGHT, // 23 + ]; + + for (let hour = 0; hour < 24; hour++) { + expect(fishingService.getCurrentTimeOfDay(hour)).toBe(expected[hour]); + } + }); + + it('should use current UTC hour when no parameter provided', () => { + // Just verify it returns a valid TimeOfDay value + const result = fishingService.getCurrentTimeOfDay(); + expect(Object.values(TimeOfDay)).toContain(result); + }); + }); + + describe('getTimeOfDayName', () => { + it('should return "Day" for DAY', () => { + const result = fishingService.getTimeOfDayName(TimeOfDay.DAY); + expect(result).toBe('Day'); + }); + + it('should return "Night" for NIGHT', () => { + const result = fishingService.getTimeOfDayName(TimeOfDay.NIGHT); + expect(result).toBe('Night'); + }); + + it('should return "Dawn" for DAWN', () => { + const result = fishingService.getTimeOfDayName(TimeOfDay.DAWN); + expect(result).toBe('Dawn'); + }); + + it('should return "Dusk" for DUSK', () => { + const result = fishingService.getTimeOfDayName(TimeOfDay.DUSK); + expect(result).toBe('Dusk'); + }); + + it('should return "Any Time" for ANY', () => { + const result = fishingService.getTimeOfDayName(TimeOfDay.ANY); + expect(result).toBe('Any Time'); + }); + + it('should return "Unknown" for invalid value', () => { + const result = fishingService.getTimeOfDayName('INVALID' as TimeOfDay); + expect(result).toBe('Unknown'); + }); + }); + + describe('getTimeOfDayEmoji', () => { + it('should return sun emoji for DAY', () => { + const result = fishingService.getTimeOfDayEmoji(TimeOfDay.DAY); + expect(result).toBe('☀️'); + }); + + it('should return moon emoji for NIGHT', () => { + const result = fishingService.getTimeOfDayEmoji(TimeOfDay.NIGHT); + expect(result).toBe('🌙'); + }); + + it('should return sunrise emoji for DAWN', () => { + const result = fishingService.getTimeOfDayEmoji(TimeOfDay.DAWN); + expect(result).toBe('🌅'); + }); + + it('should return sunset emoji for DUSK', () => { + const result = fishingService.getTimeOfDayEmoji(TimeOfDay.DUSK); + expect(result).toBe('🌆'); + }); + + it('should return clock emoji for ANY', () => { + const result = fishingService.getTimeOfDayEmoji(TimeOfDay.ANY); + expect(result).toBe('🕐'); + }); + + it('should return question mark emoji for invalid value', () => { + const result = fishingService.getTimeOfDayEmoji('INVALID' as TimeOfDay); + expect(result).toBe('❓'); + }); + + it('should return valid emoji strings for all TimeOfDay values', () => { + const emojis = [ + fishingService.getTimeOfDayEmoji(TimeOfDay.DAY), + fishingService.getTimeOfDayEmoji(TimeOfDay.NIGHT), + fishingService.getTimeOfDayEmoji(TimeOfDay.DAWN), + fishingService.getTimeOfDayEmoji(TimeOfDay.DUSK), + fishingService.getTimeOfDayEmoji(TimeOfDay.ANY), + ]; + + emojis.forEach(emoji => { + expect(emoji).toBeTruthy(); + expect(typeof emoji).toBe('string'); + expect(emoji.length).toBeGreaterThan(0); + }); + }); + }); });