import { db } from '$lib/server/db'; import { arc, character, characterHistory, devilFruit, type Character } from '$lib/server/db/schema'; import { desc, eq, and } from 'drizzle-orm'; // Generate or get random seed for daily character selection const RANDOM_SEED = Math.random(); const characterWithRelationsSelect = { id: character.id, name: character.name, frName: character.frName, gender: character.gender, age: character.age, affiliation: character.affiliation, frAffiliation: character.frAffiliation, devilFruitId: character.devilFruitId, devilFruitName: devilFruit.name, devilFruitType: devilFruit.type, hakiObservation: character.hakiObservation, hakiArmament: character.hakiArmament, hakiConqueror: character.hakiConqueror, bounty: character.bounty, height: character.height, origin: character.origin, frOrigin: character.frOrigin, firstAppearance: character.firstAppearance, pictureUrl: character.pictureUrl, epithets: character.epithets, frEpithets: character.frEpithets, status: character.status, url: character.url, frUrl: character.frUrl, arcId: character.arcId, arcName: arc.name, frArcName: arc.frName, }; export type CharacterWithRelations = Character & { devilFruitName: string | null; devilFruitType: string | null; arcName: string | null; 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(); } export function normalizeDay(date: Date = new Date()): Date { const normalized = new Date(date); normalized.setHours(1, 0, 0, 0); return normalized; } function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): CharacterWithRelations { const timestamp = getDateKey(date); const daysSinceEpoch = Math.floor(timestamp / 1000 / 60 / 60 / 24); // Combine timestamp with random seed to avoid predictable results const combinedSeed = (daysSinceEpoch + Math.floor(RANDOM_SEED * 1000000)) % characters.length; return characters[combinedSeed]; } export async function getDailyModeCharacters(): Promise { return (await db .select(characterWithRelationsSelect) .from(character) .leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .where(eq(character.isInDailyMode, true)) .all()) as CharacterWithRelations[]; } export async function getAllCharacters(): Promise { return (await db .select(characterWithRelationsSelect) .from(character) .leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .all()) as CharacterWithRelations[]; } export async function getCharacterById(characterId: string): Promise { const [found] = await db .select(characterWithRelationsSelect) .from(character) .leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .where(eq(character.id, characterId)) .limit(1); if (!found) { return null; } return found as CharacterWithRelations } export async function getOrCreateTodayCharacter( characters?: CharacterWithRelations[], date: Date = new Date() ): Promise { const dailyCharacters = characters ?? (await getDailyModeCharacters()); if (dailyCharacters.length === 0) { return null; } const today = normalizeDay(date); const todayDate = getDateKey(today); const [existingEntry] = await db .select() .from(characterHistory) .where(eq(characterHistory.date, todayDate)) .limit(1); if (existingEntry?.characterId) { return ( dailyCharacters.find((currentCharacter) => currentCharacter.id === existingEntry.characterId) ?? (await getCharacterById(existingEntry.characterId)) ); } const recentHistory = await db .select({ characterId: characterHistory.characterId }) .from(characterHistory) .orderBy(desc(characterHistory.date)) .limit(100); const excludedIds = new Set(recentHistory.map((entry) => entry.characterId)); const availableCharacters = dailyCharacters.filter((currentCharacter) => !excludedIds.has(currentCharacter.id)); const dailyCharacter = pickDailyCharacter( availableCharacters.length > 0 ? availableCharacters : dailyCharacters, today ); try { await db.insert(characterHistory).values({ characterId: dailyCharacter.id, date: todayDate, createdAt: Date.now(), updatedAt: Date.now() }); } catch (error) { console.error('Failed to record daily character:', error); } return dailyCharacter; } export async function getYesterdayCharacter( date: Date = new Date(), characters?: CharacterWithRelations[] ): Promise { const yesterday = new Date(date); yesterday.setDate(yesterday.getDate() - 1); const yesterdayDate = getDateKey(yesterday); const [yesterdayEntry] = await db .select() .from(characterHistory) .where(eq(characterHistory.date, yesterdayDate)) .limit(1); if (!yesterdayEntry?.characterId) { return null; } if (characters) { return ( characters.find((currentCharacter) => currentCharacter.id === yesterdayEntry.characterId) ?? (await getCharacterById(yesterdayEntry.characterId)) ); } return getCharacterById(yesterdayEntry.characterId); } export async function getTodayCharacterWinsCount( characterId: string, date: Date = new Date() ): Promise { const today = normalizeDay(date); const todayDate = getDateKey(today); const [result] = await db .select({ won: characterHistory.won }) .from(characterHistory) .where( and( eq(characterHistory.characterId, characterId), eq(characterHistory.date, todayDate) ) ); return result?.won ?? 0; }