303 lines
8.5 KiB
TypeScript
303 lines
8.5 KiB
TypeScript
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<string, string | null>;
|
|
devilFruitById: Map<string, { name: string | null; type: string | null }>;
|
|
};
|
|
|
|
function isNotNullish<T>(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<string, unknown>)[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<CharacterWithRelations[]> {
|
|
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<string, CharacterOverrideRow>(
|
|
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<CharacterWithRelations[]> {
|
|
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<CharacterWithRelations[]> {
|
|
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<CharacterWithRelations | null> {
|
|
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<CharacterWithRelations | null> {
|
|
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<CharacterWithRelations | null> {
|
|
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<number> {
|
|
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;
|
|
} |