import { db } from '$lib/server/db'; import { arc, character, characterHistory, characterOverride, devilFruit } from '$lib/server/db/schema'; import { desc, eq, inArray, 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, gender: character.gender, age: character.age, affiliations: character.affiliations, 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, firstAppearance: character.firstAppearance, pictureUrl: character.pictureUrl, epithets: character.epithets, status: character.status, url: character.url, arcId: character.arcId, arcName: arc.name }; export type CharacterWithRelations = typeof character.$inferSelect & { devilFruitName: string | null; devilFruitType: string | null; arcName: string | null; }; type CharacterOverrideRow = typeof characterOverride.$inferSelect; type RelationMaps = { arcNameById: Map; devilFruitById: Map; }; function isNotNullish(value: T | null | undefined): value is T { return value !== null && value !== undefined; } function mergeCharacterWithOverride( baseCharacter: CharacterWithRelations, overrideRow?: CharacterOverrideRow, relationMaps?: RelationMaps ): CharacterWithRelations { if (!overrideRow) { return baseCharacter; } const mergedCharacter = { ...baseCharacter } as CharacterWithRelations; for (const [key, value] of Object.entries(overrideRow)) { if (key === 'characterId' || key === 'notes') { continue; } if (isNotNullish(value)) { (mergedCharacter as Record)[key] = value; } } if (relationMaps) { if (mergedCharacter.arcId) { mergedCharacter.arcName = relationMaps.arcNameById.get(mergedCharacter.arcId) ?? null; } else { mergedCharacter.arcName = null; } if (mergedCharacter.devilFruitId) { const devilFruitData = relationMaps.devilFruitById.get(mergedCharacter.devilFruitId); mergedCharacter.devilFruitName = devilFruitData?.name ?? null; mergedCharacter.devilFruitType = devilFruitData?.type ?? null; } else { mergedCharacter.devilFruitName = null; mergedCharacter.devilFruitType = null; } } return mergedCharacter; } async function applyCharacterOverrides( characters: CharacterWithRelations[] ): Promise { if (characters.length === 0) { return characters; } const characterIds = characters.map((currentCharacter) => currentCharacter.id); const overrideRows = await db .select() .from(characterOverride) .where(inArray(characterOverride.characterId, characterIds)); if (overrideRows.length === 0) { return characters; } const overrideByCharacterId = new Map( overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow]) ); const shouldRefreshRelations = overrideRows.some( (overrideRow) => isNotNullish(overrideRow.arcId) || isNotNullish(overrideRow.devilFruitId) ); let relationMaps: RelationMaps | undefined; if (shouldRefreshRelations) { const [allArcs, allDevilFruits] = await Promise.all([ db.select({ id: arc.id, name: arc.name }).from(arc), db .select({ id: devilFruit.id, name: devilFruit.name, type: devilFruit.type }) .from(devilFruit) ]); relationMaps = { arcNameById: new Map(allArcs.map((currentArc) => [currentArc.id, currentArc.name])), devilFruitById: new Map( allDevilFruits.map((currentDevilFruit) => [ currentDevilFruit.id, { name: currentDevilFruit.name, type: currentDevilFruit.type } ]) ) }; } return characters.map((currentCharacter) => mergeCharacterWithOverride( currentCharacter, overrideByCharacterId.get(currentCharacter.id), relationMaps ) ); } 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 { const characters = (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[]; return applyCharacterOverrides(characters); } export async function getAllCharacters(): Promise { const characters = (await db .select(characterWithRelationsSelect) .from(character) .leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .all()) as CharacterWithRelations[]; return applyCharacterOverrides(characters); } 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; } const [overriddenCharacter] = await applyCharacterOverrides([found as CharacterWithRelations]); return overriddenCharacter ?? null; } 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; }