From 7183b176788dc78d4b5aaeb07b72aa24b4233798 Mon Sep 17 00:00:00 2001 From: Whidix Date: Mon, 22 Jun 2026 20:56:31 +0200 Subject: [PATCH] feat: add daily fruit feature and related components - Introduced a new daily fruit selection mechanism with associated database schema. - Created routes and components for displaying and interacting with daily fruit. - Implemented logic for tracking wins related to daily fruit and character guesses. - Added localization support for new daily fruit titles in English and French. - Enhanced the user experience with history tracking for guesses and wins. - Refactored existing character-related logic to accommodate new fruit functionality. --- drizzle/0004_magenta_screwball.sql | 11 + drizzle/meta/0004_snapshot.json | 1260 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/lib/components/SimpleGuessHistory.svelte | 30 + src/lib/i18n/en.json | 3 +- src/lib/i18n/fr.json | 3 +- src/lib/server/daily-character.ts | 9 - src/lib/server/daily-fruit.ts | 138 ++ src/lib/server/db/schema.ts | 14 + src/routes/(game)/+page.svelte | 2 +- .../daily/{ => character}/+page.server.ts | 0 .../(game)/daily/{ => character}/+page.svelte | 2 +- .../(game)/daily/{ => character}/+server.ts | 0 src/routes/(game)/daily/fruit/+page.server.ts | 32 + src/routes/(game)/daily/fruit/+page.svelte | 143 ++ src/routes/(game)/daily/fruit/+server.ts | 32 + 16 files changed, 1673 insertions(+), 13 deletions(-) create mode 100644 drizzle/0004_magenta_screwball.sql create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 src/lib/components/SimpleGuessHistory.svelte create mode 100644 src/lib/server/daily-fruit.ts rename src/routes/(game)/daily/{ => character}/+page.server.ts (100%) rename src/routes/(game)/daily/{ => character}/+page.svelte (99%) rename src/routes/(game)/daily/{ => character}/+server.ts (100%) create mode 100644 src/routes/(game)/daily/fruit/+page.server.ts create mode 100644 src/routes/(game)/daily/fruit/+page.svelte create mode 100644 src/routes/(game)/daily/fruit/+server.ts diff --git a/drizzle/0004_magenta_screwball.sql b/drizzle/0004_magenta_screwball.sql new file mode 100644 index 0000000..0425b16 --- /dev/null +++ b/drizzle/0004_magenta_screwball.sql @@ -0,0 +1,11 @@ +CREATE TABLE `devil_fruit_history` ( + `id` text PRIMARY KEY NOT NULL, + `devil_fruit_id` text, + `date` integer NOT NULL, + `won` integer DEFAULT 0 NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`devil_fruit_id`) REFERENCES `devil_fruit`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `devil_fruit_history_date_unique` ON `devil_fruit_history` (`date`); \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..735afa1 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,1260 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5d8f2d8f-e66d-4b6f-8216-9e37d2fbff59", + "prevId": "736137e1-d840-4f5b-a1b4-d50648839073", + "tables": { + "arc": { + "name": "arc", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fr_name": { + "name": "fr_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_chapter": { + "name": "start_chapter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_chapter": { + "name": "end_chapter", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character": { + "name": "character", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fr_name": { + "name": "fr_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affiliation": { + "name": "affiliation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_affiliation": { + "name": "fr_affiliation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "devil_fruit_id": { + "name": "devil_fruit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haki_observation": { + "name": "haki_observation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "haki_armament": { + "name": "haki_armament", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "haki_conqueror": { + "name": "haki_conqueror", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "bounty": { + "name": "bounty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_origin": { + "name": "fr_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_appearance": { + "name": "first_appearance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "picture_url": { + "name": "picture_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epithets": { + "name": "epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_epithets": { + "name": "fr_epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arc_id": { + "name": "arc_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_url": { + "name": "fr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_in_daily_mode": { + "name": "is_in_daily_mode", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_devil_fruit_id_devil_fruit_id_fk": { + "name": "character_devil_fruit_id_devil_fruit_id_fk", + "tableFrom": "character", + "tableTo": "devil_fruit", + "columnsFrom": [ + "devil_fruit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "character_arc_id_arc_id_fk": { + "name": "character_arc_id_arc_id_fk", + "tableFrom": "character", + "tableTo": "arc", + "columnsFrom": [ + "arc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_history": { + "name": "character_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "won": { + "name": "won", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "character_history_date_unique": { + "name": "character_history_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": { + "character_history_character_id_character_id_fk": { + "name": "character_history_character_id_character_id_fk", + "tableFrom": "character_history", + "tableTo": "character", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_scrape_validation": { + "name": "character_scrape_validation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fr_name": { + "name": "fr_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affiliation": { + "name": "affiliation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_affiliation": { + "name": "fr_affiliation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "devil_fruit_id": { + "name": "devil_fruit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haki_observation": { + "name": "haki_observation", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "haki_armament": { + "name": "haki_armament", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "haki_conqueror": { + "name": "haki_conqueror", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "bounty": { + "name": "bounty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_origin": { + "name": "fr_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_appearance": { + "name": "first_appearance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "picture_url": { + "name": "picture_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epithets": { + "name": "epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_epithets": { + "name": "fr_epithets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arc_id": { + "name": "arc_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fr_url": { + "name": "fr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_deleted": { + "name": "is_deleted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk": { + "name": "character_scrape_validation_devil_fruit_id_devil_fruit_id_fk", + "tableFrom": "character_scrape_validation", + "tableTo": "devil_fruit", + "columnsFrom": [ + "devil_fruit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "character_scrape_validation_arc_id_arc_id_fk": { + "name": "character_scrape_validation_arc_id_arc_id_fk", + "tableFrom": "character_scrape_validation", + "tableTo": "arc", + "columnsFrom": [ + "arc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "devil_fruit": { + "name": "devil_fruit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "devil_fruit_name_unique": { + "name": "devil_fruit_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "devil_fruit_history": { + "name": "devil_fruit_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "devil_fruit_id": { + "name": "devil_fruit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "won": { + "name": "won", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "devil_fruit_history_date_unique": { + "name": "devil_fruit_history_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": { + "devil_fruit_history_devil_fruit_id_devil_fruit_id_fk": { + "name": "devil_fruit_history_devil_fruit_id_devil_fruit_id_fk", + "tableFrom": "devil_fruit_history", + "tableTo": "devil_fruit", + "columnsFrom": [ + "devil_fruit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "friendship": { + "name": "friendship", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addressee_id": { + "name": "addressee_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "friendship_requester_id_addressee_id_unique": { + "name": "friendship_requester_id_addressee_id_unique", + "columns": [ + "requester_id", + "addressee_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "friendship_requester_id_user_id_fk": { + "name": "friendship_requester_id_user_id_fk", + "tableFrom": "friendship", + "tableTo": "user", + "columnsFrom": [ + "requester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friendship_addressee_id_user_id_fk": { + "name": "friendship_addressee_id_user_id_fk", + "tableFrom": "friendship", + "tableTo": "user", + "columnsFrom": [ + "addressee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_character_history": { + "name": "user_character_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "character_history_id": { + "name": "character_history_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "try_count": { + "name": "try_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tried_character_ids": { + "name": "tried_character_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_character_history_user_id_character_history_id_unique": { + "name": "user_character_history_user_id_character_history_id_unique", + "columns": [ + "user_id", + "character_history_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_character_history_user_id_user_id_fk": { + "name": "user_character_history_user_id_user_id_fk", + "tableFrom": "user_character_history", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_character_history_character_history_id_character_history_id_fk": { + "name": "user_character_history_character_history_id_character_history_id_fk", + "tableFrom": "user_character_history", + "tableTo": "character_history", + "columnsFrom": [ + "character_history_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 5008b3a..50a4698 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1776195681488, "tag": "0003_mixed_ben_grimm", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1782152097346, + "tag": "0004_magenta_screwball", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/SimpleGuessHistory.svelte b/src/lib/components/SimpleGuessHistory.svelte new file mode 100644 index 0000000..0f2b5ef --- /dev/null +++ b/src/lib/components/SimpleGuessHistory.svelte @@ -0,0 +1,30 @@ + + +
+

Historique

+ + {#if selectedCharacters.length === 0} +

Aucun personnage sélectionné

+ {:else} +
    + {#each selectedCharacters as char, index (char.id)} +
  1. + + {index + 1} + + + {#if char.pictureUrl} + {char.name} + {:else} +
    ?
    + {/if} + + {char.name} +
  2. + {/each} +
+ {/if} +
diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json index e05c352..5e67fb7 100644 --- a/src/lib/i18n/en.json +++ b/src/lib/i18n/en.json @@ -104,7 +104,8 @@ }, "daily": { "metaTitle": "OnePieceDle - Daily Mode", - "title": "Daily Character", + "characterTitle": "daily - character", + "fruitTitle": "daily - fruit", "winsPeopleSingular": "person", "winsPeoplePlural": "people", "winsVerbSingular": "has", diff --git a/src/lib/i18n/fr.json b/src/lib/i18n/fr.json index b95fc3f..3c6cec6 100644 --- a/src/lib/i18n/fr.json +++ b/src/lib/i18n/fr.json @@ -104,7 +104,8 @@ }, "daily": { "metaTitle": "OnePieceDle - Mode du jour", - "title": "Personnage du jour", + "characterTitle": "daily - personnage", + "fruitTitle": "daily - fruit", "winsPeopleSingular": "personne", "winsPeoplePlural": "personnes", "winsVerbSingular": "a", diff --git a/src/lib/server/daily-character.ts b/src/lib/server/daily-character.ts index 5a6e3e9..5d7c5e8 100644 --- a/src/lib/server/daily-character.ts +++ b/src/lib/server/daily-character.ts @@ -42,15 +42,6 @@ export type CharacterWithRelations = Character & { frArcName: string | null; }; -type RelationMaps = { - arcNameById: Map; - devilFruitById: Map; -}; - -function isNotNullish(value: T | null | undefined): value is T { - return value !== null && value !== undefined; -} - export function getDateKey(date: Date): number { return normalizeDay(date).getTime(); } diff --git a/src/lib/server/daily-fruit.ts b/src/lib/server/daily-fruit.ts new file mode 100644 index 0000000..bda237c --- /dev/null +++ b/src/lib/server/daily-fruit.ts @@ -0,0 +1,138 @@ +import { db } from '$lib/server/db'; +import { character, devilFruit, devilFruitHistory, type DevilFruit, type Character } from '$lib/server/db/schema'; +import { desc, eq, and } from 'drizzle-orm'; + +// Generate or get random seed for daily fruit selection +const RANDOM_SEED = Math.random(); + +const characterWithFruitSelect = { + id: character.id, + name: character.name, + devilFruitId: character.devilFruitId, + devilFruitName: devilFruit.name, + devilFruitType: devilFruit.type, + // include url if available from devilFruit join + url: devilFruit.url +}; + +export type CharacterWithFruit = Character & { + devilFruitName: string | null; + devilFruitType: string | null; +}; + +export function getDateKey(date: Date): number { + return normalizeDay(date).getTime(); +} + +export function normalizeDay(date: Date = new Date()): Date { + const normalized = new Date(date); + normalized.setHours(1, 0, 0, 0); + return normalized; +} + +function pickDailyFruit(fruits: DevilFruit[], date: Date): DevilFruit { + const timestamp = getDateKey(date); + const daysSinceEpoch = Math.floor(timestamp / 1000 / 60 / 60 / 24); + const combinedSeed = (daysSinceEpoch + Math.floor(RANDOM_SEED * 1000000)) % fruits.length; + return fruits[combinedSeed]; +} + +export async function getCharactersWithDevilFruit(): Promise { + const rows = (await db + .select(characterWithFruitSelect) + .from(character) + .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) + .all()) as any[]; + + return rows.filter((r) => r.devilFruitId) as CharacterWithFruit[]; +} + +export async function getAllDevilFruitsFromCharacters(): Promise { + const characters = (await db + .select(characterWithFruitSelect) + .from(character) + .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) + .all()) as any[]; + + const map = new Map(); + for (const row of characters) { + if (row.devilFruitId && row.devilFruitName) { + if (!map.has(row.devilFruitId)) { + map.set(row.devilFruitId, { + id: row.devilFruitId, + name: row.devilFruitName, + type: row.devilFruitType ?? 'Unknown', + url: row.url ?? null + } as DevilFruit); + } + } + } + + return Array.from(map.values()); +} + +export async function getFruitById(fruitId: string): Promise { + const [found] = await db.select().from(devilFruit).where(eq(devilFruit.id, fruitId)).limit(1); + return (found as DevilFruit) ?? null; +} + +export async function getOrCreateTodayFruit(date: Date = new Date()): Promise { + const today = normalizeDay(date); + const todayDate = getDateKey(today); + + const [existingEntry] = await db + .select() + .from(devilFruitHistory) + .where(eq(devilFruitHistory.date, todayDate)) + .limit(1); + + if (existingEntry?.devilFruitId) { + return getFruitById(existingEntry.devilFruitId); + } + + const allFruits = await getAllDevilFruitsFromCharacters(); + if (allFruits.length === 0) return null; + + const chosen = pickDailyFruit(allFruits, today); + + try { + await db.insert(devilFruitHistory).values({ + devilFruitId: chosen.id, + date: todayDate, + createdAt: Date.now(), + updatedAt: Date.now() + }); + } catch (error) { + console.error('Failed to record daily fruit:', error); + } + + return chosen; +} + +export async function getYesterdayFruit(date: Date = new Date()): Promise { + const yesterday = new Date(date); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayDate = getDateKey(yesterday); + + const [yesterdayEntry] = await db + .select() + .from(devilFruitHistory) + .where(eq(devilFruitHistory.date, yesterdayDate)) + .limit(1); + + if (!yesterdayEntry?.devilFruitId) return null; + + return getFruitById(yesterdayEntry.devilFruitId); +} + +export async function getTodayFruitWinsCount(fruitId: string, date: Date = new Date()): Promise { + const today = normalizeDay(date); + const todayDate = getDateKey(today); + + const [result] = await db + .select({ won: devilFruitHistory.won }) + .from(devilFruitHistory) + .where(and(eq(devilFruitHistory.devilFruitId, fruitId), eq(devilFruitHistory.date, todayDate))); + + return result?.won ?? 0; +} \ No newline at end of file diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 79957d2..c4f12ae 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -126,6 +126,20 @@ export const userCharacterHistory = sqliteTable('user_character_history', { export type UserCharacterHistory = InferSelectModel; +// Define the devil fruit history table schema +export const devilFruitHistory = sqliteTable('devil_fruit_history', { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'cascade' }), + date: integer('date').notNull().unique(), + won: integer('won').notNull().default(0), + createdAt: integer('created_at').notNull().$default(() => Date.now()), + updatedAt: integer('updated_at').notNull().$default(() => Date.now()), +}); + +export type DevilFruitHistory = InferSelectModel; + // Define the friendship table schema (friend requests + accepted friends) export const friendship = sqliteTable('friendship', { id: text('id') diff --git a/src/routes/(game)/+page.svelte b/src/routes/(game)/+page.svelte index bfad3c7..4d464cf 100644 --- a/src/routes/(game)/+page.svelte +++ b/src/routes/(game)/+page.svelte @@ -82,7 +82,7 @@

{$t.game.home.dailySubtitle}

{$t.game.home.dailyDescription}

{$t.game.home.dailyCta} diff --git a/src/routes/(game)/daily/+page.server.ts b/src/routes/(game)/daily/character/+page.server.ts similarity index 100% rename from src/routes/(game)/daily/+page.server.ts rename to src/routes/(game)/daily/character/+page.server.ts diff --git a/src/routes/(game)/daily/+page.svelte b/src/routes/(game)/daily/character/+page.svelte similarity index 99% rename from src/routes/(game)/daily/+page.svelte rename to src/routes/(game)/daily/character/+page.svelte index 9bc10c2..b393b27 100644 --- a/src/routes/(game)/daily/+page.svelte +++ b/src/routes/(game)/daily/character/+page.svelte @@ -269,7 +269,7 @@

- {$t.game.daily.title} + {$t.game.daily.characterTitle}

{data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular} {data.winCount > 1 ? $t.game.daily.winsVerbPlural : $t.game.daily.winsVerbSingular} {$t.game.daily.winsSuffix} diff --git a/src/routes/(game)/daily/+server.ts b/src/routes/(game)/daily/character/+server.ts similarity index 100% rename from src/routes/(game)/daily/+server.ts rename to src/routes/(game)/daily/character/+server.ts diff --git a/src/routes/(game)/daily/fruit/+page.server.ts b/src/routes/(game)/daily/fruit/+page.server.ts new file mode 100644 index 0000000..a1090e5 --- /dev/null +++ b/src/routes/(game)/daily/fruit/+page.server.ts @@ -0,0 +1,32 @@ +import { error } from '@sveltejs/kit'; +import { getAllDevilFruitsFromCharacters, getOrCreateTodayFruit, getYesterdayFruit, getTodayFruitWinsCount } from '$lib/server/daily-fruit'; +import { getAllCharacters } from '$lib/server/daily-character'; + +export async function load(event) { + const fruits = await getAllDevilFruitsFromCharacters(); + const dailyFruit = await getOrCreateTodayFruit(); + + if (!dailyFruit) { + throw error(404, 'No daily fruit available. Please check if devil fruits are configured from characters.'); + } + + const yesterdayFruit = await getYesterdayFruit(new Date()); + + // Load all characters for searching and filter only those with a devil fruit + const allCharacters = await getAllCharacters(); + const characters = allCharacters.filter((c) => c.devilFruitId); + + // Find the character tied to the chosen fruit among characters that have a devil fruit + const dailyCharacter = characters.find((c) => c.devilFruitId === dailyFruit.id) ?? null; + + const winCount = await getTodayFruitWinsCount(dailyFruit.id); + + return { + fruits, + dailyFruit, + dailyCharacter, + yesterdayFruit, + winCount, + characters + }; +} diff --git a/src/routes/(game)/daily/fruit/+page.svelte b/src/routes/(game)/daily/fruit/+page.svelte new file mode 100644 index 0000000..b398f31 --- /dev/null +++ b/src/routes/(game)/daily/fruit/+page.svelte @@ -0,0 +1,143 @@ + + + + {$t.game.daily.metaTitle} + + +

+
+
+
+
+

+ {$t.game.daily.fruitTitle} +

+

+ {data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular} +

+
+ {#if hasWon} + + {/if} +
+

+ Trouvez la personne liée au fruit du jour. +

+
+ +
+

{dailyFruit?.name}

+
+ +
+ {#if hasWon} + + {:else} + + {/if} +
+ + + + +
+
diff --git a/src/routes/(game)/daily/fruit/+server.ts b/src/routes/(game)/daily/fruit/+server.ts new file mode 100644 index 0000000..90e2f98 --- /dev/null +++ b/src/routes/(game)/daily/fruit/+server.ts @@ -0,0 +1,32 @@ +import { json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { devilFruitHistory } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { sql } from 'drizzle-orm'; +import { getDateKey } from '$lib/server/daily-fruit'; + +export async function POST({ request, locals }) { + try { + const { characterId, tryCount, triedCharacterIds } = await request.json(); + + if (!characterId) { + return json({ error: 'Missing characterId' }, { status: 400 }); + } + + const todayDate = getDateKey(new Date()); + + // Increment the won counter for today's devil fruit entry + await db + .update(devilFruitHistory) + .set({ + won: sql`${devilFruitHistory.won} + 1`, + updatedAt: Date.now() + }) + .where(eq(devilFruitHistory.date, todayDate)); + + return json({ success: true }); + } catch (error) { + console.error('Error recording fruit win:', error); + return json({ error: 'Failed to record win' }, { status: 500 }); + } +}