feat: implement daily character guessing game with local storage and hint system

- Added character selection and history management using local storage.
- Implemented hint system that unlocks based on the number of guesses.
- Enhanced UI with animations for hint unlocks and special win conditions.
- Created a server endpoint to record wins in the database.
This commit is contained in:
2026-03-01 03:59:16 +01:00
parent 6f7bae2307
commit b8b3f8bddc
23 changed files with 2988 additions and 620 deletions

View File

@@ -0,0 +1,145 @@
[
"aladdin_aladdin",
"alvida_alvida",
"aramaki_aramaki",
"arlong_arlong",
"ashura_doji_ashura_doji",
"baby_5_baby_5",
"baggy_baggy",
"bartholomew_kuma_bartholomew_kuma",
"bartolomeo_bartolomeo",
"basil_hawkins_basil_hawkins",
"batman_batman",
"bellamy_bellamy",
"belo_betty_belo_betty",
"ben_beckman_ben_beckman",
"bentham_bentham",
"bepo_bepo",
"black_maria_black_maria",
"boa_hancock_boa_hancock",
"boa_marigold_boa_marigold",
"boa_sandersonia_boa_sandersonia",
"borsalino_borsalino",
"brogy_brogy",
"brook_brook",
"camie_camie",
"capone_bege_capone_bege",
"caribou_caribou",
"carrot_carrot",
"catarina_devon_catarina_devon",
"cavendish_cavendish",
"cesar_clown_cesar_clown",
"chinjao_chinjao",
"coby_coby",
"corazon_corazon",
"crocodile_crocodile",
"crocus_crocus",
"curly_dadan_curly_dadan",
"dalton_dalton",
"daz_bones_daz_bones",
"denjiro_denjiro",
"diamante_diamante",
"doc_q_doc_q",
"don_quichotte_doflamingo_don_quichotte_doflamingo",
"don_quichotte_rossinante_don_quichotte_rossinante",
"dorry_dorry",
"dracule_mihawk_dracule_mihawk",
"duval_duval",
"edward_newgate_edward_newgate",
"edward_weevil_edward_weevil",
"emporio_ivankov_emporio_ivankov",
"enel_enel",
"eustass_kid_eustass_kid",
"fisher_tiger_fisher_tiger",
"foxy_foxy",
"franky_franky",
"fujitora_fujitora",
"gan_forr_gan_forr",
"gecko_moria_gecko_moria",
"gin_gin",
"gol_d_roger_gol_d_roger",
"haguar_d_sauro_haguar_d_sauro",
"hajrudin_hajrudin",
"hannyabal_hannyabal",
"hatchan_hatchan",
"hina_hina",
"hody_jones_hody_jones",
"hyogoro_hyogoro",
"iceburg_iceburg",
"imu_imu",
"inazuma_inazuma",
"inuarashi_inuarashi",
"issho_issho",
"izo_izo",
"jabra_jabra",
"jack_jack",
"jesus_burgess_jesus_burgess",
"jewelry_bonney_jewelry_bonney",
"jinbei_jinbei",
"joy_boy_joy_boy",
"kaidou_kaidou",
"kaku_kaku",
"kalgara_kalgara",
"kalifa_kalifa",
"karasu_karasu",
"karoo_karoo",
"kawamatsu_kawamatsu",
"kaya_kaya",
"killer_killer",
"kinemon_kinemon",
"koala_koala",
"koby_koby",
"kong_kong",
"kozuki_hiyori_kozuki_hiyori",
"kozuki_momonosuke_kozuki_momonosuke",
"kozuki_oden_kozuki_oden",
"krieg_krieg",
"kureha_kureha",
"kuro_kuro",
"kurozumi_orochi_kurozumi_orochi",
"kuzan_kuzan",
"kyros_kyros",
"laboon_laboon",
"laffitte_laffitte",
"lao_g_lao_g",
"leo_leo",
"lindbergh_lindbergh",
"loki_loki",
"lucky_roux_lucky_roux",
"magellan_magellan",
"makino_makino",
"marco_marco",
"marshall_d_teach_marshall_d_teach",
"monkey_d_dragon_monkey_d_dragon",
"monkey_d_garp_monkey_d_garp",
"monkey_d_luffy_monkey_d_luffy",
"montblanc_norland_montblanc_norland",
"morgans_morgans",
"morley_morley",
"mr_3_mr_3",
"nami_nami",
"nefertari_cobra_nefertari_cobra",
"nefertari_vivi_nefertari_vivi",
"nekomamushi_nekomamushi",
"neptune_neptune",
"nico_robin_nico_robin",
"oars_oars",
"orlumbus_orlumbus",
"otohime_otohime",
"page_one_page_one",
"pandaman_pandaman",
"pekoms_pekoms",
"pell_pell",
"perona_perona",
"pica_pica",
"portgas_d_ace_portgas_d_ace",
"queen_queen",
"raizo_raizo",
"rebecca_rebecca",
"rob_lucci_rob_lucci",
"rocks_d_xebec_rocks_d_xebec",
"roronoa_zoro_roronoa_zoro",
"sabo_sabo",
"vegapunk_vegapunk",
"yamato_yamato"
]

406
scripts/import-json.ts Normal file
View File

@@ -0,0 +1,406 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq } from 'drizzle-orm';
import fs from 'fs';
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
type ArcRecord = {
id: string;
name: string;
startChapter: number;
endChapter?: number | null;
url?: string | null;
};
type DevilFruitRecord = {
id: string;
name: string;
type?: DevilFruitType | string | null;
url?: string | null;
};
type CharacterRecord = {
id: string;
name: string;
gender?: string | null;
age?: number | null;
affiliations?: string[] | string | null;
devilFruitId?: string | null;
hakiObservation?: boolean;
hakiArmament?: boolean;
hakiConqueror?: boolean;
bounty?: number | null;
height?: number | null;
origin?: string | null;
firstAppearance?: number;
pictureUrl?: string | null;
epithets?: string[] | string | null;
status?: string | null;
arcId?: string | null;
url?: string | null;
};
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
const client = createClient({ url: DATABASE_URL });
const db = drizzle(client);
function readJsonFile<T>(path: string): T[] | null {
if (!fs.existsSync(path)) {
return null;
}
const content = fs.readFileSync(path, 'utf-8');
return JSON.parse(content) as T[];
}
function toNullable<T>(value: T | undefined | null | ''): T | null {
return value === undefined || value === null || value === '' ? null : value;
}
function toJsonArray(value: string[] | string | null | undefined): string[] | null {
if (Array.isArray(value)) {
return value.length > 0 ? value : null;
}
if (typeof value === 'string' && value.trim() !== '') {
if (value.trim().startsWith('[')) {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [value];
} catch {
return [value];
}
}
const splitValues = value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
return splitValues.length > 0 ? splitValues : null;
}
return null;
}
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
if (!value) return null;
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
return value;
}
return 'Unknown';
}
function toNumber(value: string | number | null | undefined): number | null {
if (value === null || value === undefined || value === '') return null;
const num = typeof value === 'string' ? parseFloat(value) : value;
return isNaN(num) ? null : num;
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function logSqlOnError(statement: { sql: string; params: unknown[] } | null): void {
if (!statement) return;
console.error(` SQL: ${statement.sql}`);
console.error(` Params: ${JSON.stringify(statement.params)}`);
}
function transformCharacterData(item: CharacterRecord) {
return {
id: item.id,
name: item.name,
gender: toNullable(item.gender),
age: toNullable(item.age),
affiliations: toJsonArray(item.affiliations),
devilFruitId: toNullable(item.devilFruitId),
hakiObservation: !!item.hakiObservation,
hakiArmament: !!item.hakiArmament,
hakiConqueror: !!item.hakiConqueror,
bounty: item.bounty ?? 0,
height: toNumber(item.height as any),
origin: toNullable(item.origin),
firstAppearance: item.firstAppearance ?? 0,
pictureUrl: toNullable(item.pictureUrl),
epithets: toJsonArray(item.epithets),
status: toNullable(item.status),
arcId: toNullable(item.arcId),
url: toNullable(item.url)
};
}
function hasChanged(jsonData: any, dbData: any): boolean {
if (!dbData) return true;
// Print any differences for debugging
for (const key in jsonData) {
const jsonValue = jsonData[key];
const dbValue = dbData[key];
const jsonString = typeof jsonValue === 'object' ? JSON.stringify(jsonValue) : String(jsonValue);
const dbString = typeof dbValue === 'object' ? JSON.stringify(dbValue) : String(dbValue);
if (jsonString !== dbString) {
console.log(`\nField "${key}" changed for character ID ${jsonData.id}:`);
console.log(` JSON: ${jsonString}`);
console.log(` DB: ${dbString}`);
} }
// Compare each field
return (
jsonData.name != dbData.name ||
jsonData.gender != dbData.gender ||
jsonData.age != dbData.age ||
JSON.stringify(jsonData.affiliations) != JSON.stringify(dbData.affiliations) ||
jsonData.devilFruitId != dbData.devilFruitId ||
jsonData.hakiObservation != dbData.hakiObservation ||
jsonData.hakiArmament != dbData.hakiArmament ||
jsonData.hakiConqueror != dbData.hakiConqueror ||
jsonData.bounty != dbData.bounty ||
jsonData.height != dbData.height ||
jsonData.origin != dbData.origin ||
jsonData.firstAppearance != dbData.firstAppearance ||
jsonData.pictureUrl != dbData.pictureUrl ||
JSON.stringify(jsonData.epithets) != JSON.stringify(dbData.epithets) ||
jsonData.status != dbData.status ||
jsonData.arcId != dbData.arcId ||
jsonData.url != dbData.url
);
}
async function isCharacterTableEmpty(): Promise<boolean> {
const result = await db.select({ count: sql<number>`COUNT(*)` }).from(character);
return result[0]?.count === 0;
}
async function importFromJson(): Promise<void> {
let totalSuccess = 0;
let totalErrors = 0;
try {
const arcs = readJsonFile<ArcRecord>('./scraped-data/arcs.json');
if (arcs) {
console.log('\n=== Importing Arcs ===\n');
console.log(`Found ${arcs.length} arcs\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < arcs.length; i++) {
const item = arcs[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const query = db
.insert(arc)
.values({
id: item.id,
name: item.name,
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
})
.onConflictDoUpdate({
target: arc.id,
set: {
name: item.name,
startChapter: item.startChapter,
endChapter: toNullable(item.endChapter),
url: toNullable(item.url)
}
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${arcs.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at arc ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
console.log(`\n\n✓ Arcs imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No arcs.json found, skipping...\n');
}
const fruits = readJsonFile<DevilFruitRecord>('./scraped-data/devil-fruits.json');
if (fruits) {
console.log('\n=== Importing Devil Fruits ===\n');
console.log(`Found ${fruits.length} devil fruits\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < fruits.length; i++) {
const item = fruits[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const query = db
.insert(devilFruit)
.values({
id: item.id,
name: item.name,
type: toDevilFruitType(item.type),
url: toNullable(item.url)
})
.onConflictDoUpdate({
target: devilFruit.id,
set: {
name: item.name,
type: toDevilFruitType(item.type),
url: toNullable(item.url)
}
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${fruits.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at devil fruit ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
console.log(`\n\n✓ Devil Fruits imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No devil-fruits.json found, skipping...\n');
}
const characters = readJsonFile<CharacterRecord>('./scraped-data/characters.json');
if (characters) {
console.log('\n=== Importing Characters ===\n');
console.log(`Found ${characters.length} characters\n`);
const isEmpty = await isCharacterTableEmpty();
let successCount = 0;
let errorCount = 0;
if (isEmpty) {
// Populate empty character table
console.log('Characters table is empty, populating...\n');
for (let i = 0; i < characters.length; i++) {
const item = characters[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const data = transformCharacterData(item);
const query = db
.insert(character)
.values(data)
.onConflictDoUpdate({
target: character.id,
set: data
});
lastSql = query.toSQL();
await query;
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${characters.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at character ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
} else {
// Check for changes and update scrapeValidation table
console.log('Characters table not empty, checking for changes...\n');
for (let i = 0; i < characters.length; i++) {
const item = characters[i];
let lastSql: { sql: string; params: unknown[] } | null = null;
try {
const selectQuery = db
.select()
.from(character)
.where(eq(character.id, item.id));
lastSql = selectQuery.toSQL();
const [dbCharacter] = await selectQuery;
const jsonData = transformCharacterData(item);
const changed = hasChanged(jsonData, dbCharacter);
if (changed) {
// Update scrapeValidation table with changes
const upsertQuery = db
.insert(characterScrapeValidation)
.values(jsonData)
.onConflictDoUpdate({
target: characterScrapeValidation.id,
set: jsonData
});
lastSql = upsertQuery.toSQL();
await upsertQuery;
} else {
// No changes, delete from scrapeValidation if it exists
const deleteQuery = db
.delete(characterScrapeValidation)
.where(eq(characterScrapeValidation.id, item.id));
lastSql = deleteQuery.toSQL();
await deleteQuery;
}
successCount++;
process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error at character ${i + 1}:`);
console.error(` ID: ${item.id ?? 'N/A'}`);
console.error(` Message: ${getErrorMessage(error)}`);
logSqlOnError(lastSql);
}
}
}
console.log(`\n\n✓ Characters imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No characters.json found, skipping...\n');
}
console.log(`\n=== Total Import Summary ===`);
console.log(` Total Success: ${totalSuccess}`);
console.log(` Total Errors: ${totalErrors}\n`);
} catch (error) {
console.error('✗ Import failed:', getErrorMessage(error));
process.exit(1);
} finally {
client.close();
}
}
importFromJson().catch((error) => {
console.error(getErrorMessage(error));
process.exit(1);
});

View File

@@ -1,104 +0,0 @@
import { createClient } from '@libsql/client';
import fs from 'fs';
// Load environment variables
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
const client = createClient({
url: DATABASE_URL
});
async function importSQL() {
try {
let totalSuccess = 0;
let totalErrors = 0;
// Step 1: Import Devil Fruits
if (fs.existsSync('./scraped-data/devil-fruits.sql')) {
console.log('\n=== Importing Devil Fruits ===\n');
const devilFruitsSql = fs.readFileSync('./scraped-data/devil-fruits.sql', 'utf-8');
const dfStatements = devilFruitsSql.split(';\n\n').filter(s => s.trim());
console.log(`Found ${dfStatements.length} devil fruit statements\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < dfStatements.length; i++) {
const statement = dfStatements[i];
if (statement.trim()) {
try {
await client.execute(statement.trim() + ';');
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${dfStatements.length}`);
} catch (error) {
errorCount++;
const valuesMatch = statement.match(/VALUES\s*\(([^)]+)\)/);
const values = valuesMatch ? valuesMatch[1] : 'N/A';
console.error(`\n✗ Error at statement ${i + 1}:`);
console.error(` Values: ${values}`);
console.error(` Message: ${error.message}`);
}
}
}
console.log(`\n\n✓ Devil Fruits imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No devil-fruits.sql found, skipping...\n');
}
// Step 2: Import Characters
if (fs.existsSync('./scraped-data/characters.sql')) {
console.log('\n=== Importing Characters ===\n');
const charactersSql = fs.readFileSync('./scraped-data/characters.sql', 'utf-8');
const charStatements = charactersSql.split(';\n\n').filter(s => s.trim());
console.log(`Found ${charStatements.length} character statements\n`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < charStatements.length; i++) {
const statement = charStatements[i];
if (statement.trim()) {
try {
await client.execute(statement.trim() + ';');
successCount++;
process.stdout.write(`\rExecuted: ${successCount}/${charStatements.length}`);
} catch (error) {
errorCount++;
const valuesMatch = statement.match(/VALUES\s*\(([^)]+)\)/);
const values = valuesMatch ? valuesMatch[1] : 'N/A';
console.error(`\n✗ Error at statement ${i + 1}:`);
console.error(` Values: ${values}`);
console.error(` Message: ${error.message}`);
}
}
}
console.log(`\n\n✓ Characters imported!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
totalSuccess += successCount;
totalErrors += errorCount;
} else {
console.log('\n⚠ No characters.sql found, skipping...\n');
}
console.log(`\n=== Total Import Summary ===`);
console.log(` Total Success: ${totalSuccess}`);
console.log(` Total Errors: ${totalErrors}\n`);
} catch (error) {
console.error('✗ Import failed:', error.message);
process.exit(1);
}
}
importSQL().catch(console.error);

View File

@@ -0,0 +1,64 @@
/**
* Initialize config table with column visibility settings for characterHistory table
*/
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import { config } from '../src/lib/server/db/schema';
import { like } from 'drizzle-orm';
// Load environment variables
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
const client = createClient({ url: DATABASE_URL });
const db = drizzle(client);
// Define the columns in characterHistory table
const columns = [
'gender',
'affiliations',
'haki',
'bounty',
'height',
'origin',
'devilFruitType',
'arc',
'status'
] as const;
async function initColumnConfig(): Promise<void> {
try {
console.log('Initializing column visibility config...\n');
for (const column of columns) {
const key = `characterHistory.column.${column}.visible`;
const value = 'true';
await db.insert(config)
.values({ key, value })
.onConflictDoNothing();
console.log(`✓ Added config key: ${key} = ${value}`);
}
console.log('\n✓ Successfully initialized column visibility config');
// Display all config entries
const allConfig = await db.select()
.from(config)
.where(like(config.key, 'characterHistory.column.%.visible'));
console.log('\nCurrent column visibility config:');
allConfig.forEach(row => {
console.log(` ${row.key}: ${row.value}`);
});
} catch (error) {
console.error('Error initializing config:', error);
process.exit(1);
} finally {
client.close();
}
}
initColumnConfig();

File diff suppressed because it is too large Load Diff

84
scripts/set-daily-mode.ts Normal file
View File

@@ -0,0 +1,84 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import { eq } from 'drizzle-orm';
import fs from 'fs';
import { character } from '../src/lib/server/db/schema';
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
const client = createClient({ url: DATABASE_URL });
const db = drizzle(client);
function readJsonFile(path: string): string[] | null {
if (!fs.existsSync(path)) {
return null;
}
const content = fs.readFileSync(path, 'utf-8');
return JSON.parse(content) as string[];
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function setDailyCharacters(): Promise<void> {
try {
const dailyCharacterIds = readJsonFile('./scripts/daily-characters.json');
if (!dailyCharacterIds || dailyCharacterIds.length === 0) {
console.error('❌ No daily characters found in daily-characters.json');
process.exit(1);
}
console.log(`\n=== Setting Daily Mode Characters ===\n`);
console.log(`Found ${dailyCharacterIds.length} characters to set as daily\n`);
// Step 1: Disable isInDailyMode for all characters
console.log('Step 1: Disabling isInDailyMode for all characters...');
await db.update(character).set({ isInDailyMode: false });
console.log('✓ All characters disabled from daily mode\n');
// Step 2: Enable isInDailyMode for characters in the list
console.log('Step 2: Enabling isInDailyMode for daily characters...\n');
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < dailyCharacterIds.length; i++) {
const charId = dailyCharacterIds[i];
try {
const result = await db
.update(character)
.set({ isInDailyMode: true })
.where(eq(character.id, charId));
successCount++;
process.stdout.write(`\rUpdated: ${successCount}/${dailyCharacterIds.length}`);
} catch (error) {
errorCount++;
console.error(`\n✗ Error updating character ${i + 1}:`);
console.error(` ID: ${charId}`);
console.error(` Message: ${getErrorMessage(error)}`);
}
}
console.log(`\n\n✓ Daily characters updated!`);
console.log(` Success: ${successCount}`);
console.log(` Errors: ${errorCount}`);
if (errorCount === 0) {
console.log(`\n✅ Successfully set ${successCount} characters as daily mode characters\n`);
}
} catch (error) {
console.error('✗ Operation failed:', getErrorMessage(error));
process.exit(1);
} finally {
client.close();
}
}
setDailyCharacters().catch((error) => {
console.error(getErrorMessage(error));
process.exit(1);
});