Compare commits
9
Commits
33d8d17d33
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63951df5a7 | ||
|
|
dac084a75e | ||
|
|
b0c071e3dd | ||
|
|
d3fafd75c6 | ||
|
|
efb0b3d330 | ||
|
|
2e6d90ce8c | ||
|
|
262f619013 | ||
|
|
42c4c7e20c | ||
|
|
2d9cd43def |
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `devil_fruit` ADD `translated_name` text;--> statement-breakpoint
|
||||
ALTER TABLE `devil_fruit` ADD `fr_translated_name` text;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
||||
"when": 1782152097346,
|
||||
"tag": "0004_magenta_screwball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1783545248600,
|
||||
"tag": "0005_uneven_talkback",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,6 +18,8 @@ type ArcRecord = {
|
||||
type DevilFruitRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
translatedName?: string | null;
|
||||
frTranslatedName?: string | null;
|
||||
type?: DevilFruitType | string | null;
|
||||
url?: string | null;
|
||||
};
|
||||
@@ -94,7 +96,7 @@ function toJsonArray(value: string[] | string | null | undefined): string[] | nu
|
||||
|
||||
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
|
||||
if (!value) return null;
|
||||
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Smile' || value === 'Unknown') {
|
||||
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
|
||||
return value;
|
||||
}
|
||||
return 'Unknown';
|
||||
@@ -229,6 +231,8 @@ async function importFromJson(): Promise<void> {
|
||||
.values({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
translatedName: toNullable(item.translatedName),
|
||||
frTranslatedName: toNullable(item.frTranslatedName),
|
||||
type: toDevilFruitType(item.type),
|
||||
url: toNullable(item.url)
|
||||
})
|
||||
@@ -236,6 +240,8 @@ async function importFromJson(): Promise<void> {
|
||||
target: devilFruit.id,
|
||||
set: {
|
||||
name: item.name,
|
||||
translatedName: toNullable(item.translatedName),
|
||||
frTranslatedName: toNullable(item.frTranslatedName),
|
||||
type: toDevilFruitType(item.type),
|
||||
url: toNullable(item.url)
|
||||
}
|
||||
|
||||
+66
-22
@@ -23,6 +23,8 @@ interface Character {
|
||||
frOrigin: string | null;
|
||||
devilFruitId: string | null;
|
||||
devilFruitName: string | null;
|
||||
devilFruitTranslatedName: string | null;
|
||||
frDevilFruitTranslatedName: string | null;
|
||||
devilFruitUrl: string | null;
|
||||
affiliation: string | null;
|
||||
frAffiliation: string | null;
|
||||
@@ -49,12 +51,16 @@ interface CharacterListItem {
|
||||
interface DevilFruitData {
|
||||
devilFruitId: string;
|
||||
devilFruitTitle: string;
|
||||
devilFruitTranslatedName?: string | null;
|
||||
frDevilFruitTranslatedName?: string | null;
|
||||
devilFruitUrl: string;
|
||||
}
|
||||
|
||||
interface DevilFruit {
|
||||
id: string;
|
||||
name: string;
|
||||
translatedName?: string | null;
|
||||
frTranslatedName?: string | null;
|
||||
type: string | null;
|
||||
url: string;
|
||||
}
|
||||
@@ -173,17 +179,6 @@ function removeParentheticalNotes(text: string): string {
|
||||
return text.replace(/\([^)]*\)/g, '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the French link from the API response links array
|
||||
*/
|
||||
|
||||
function getFrLink(links: { lang: string; ['*']: string; url: string }[]): { url: string } | null {
|
||||
// Get french url by getting parse.langlinks where lang is "fr" and extract the name from there
|
||||
const frLink = links.find(
|
||||
(link: { lang: string; ['*']: string; url: string }) => link.lang === 'fr'
|
||||
);
|
||||
return frLink ? { url: frLink['url'] } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize string by decoding URI components, punctuation, and replacing spaces with underscores
|
||||
@@ -485,9 +480,10 @@ async function fetchCharacter(
|
||||
const epithets = extractEpithets($);
|
||||
|
||||
// Extract devil fruit
|
||||
const devilFruitData = await extractDevilFruit($);
|
||||
const devilFruitData = await extractDevilFruit($, 'en');
|
||||
const devilFruitId = devilFruitData?.devilFruitId || null;
|
||||
const devilFruitName = devilFruitData?.devilFruitTitle || null;
|
||||
const devilFruitTranslatedName = devilFruitData?.devilFruitTranslatedName || null;
|
||||
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
|
||||
|
||||
// Extract haki from JSON categories
|
||||
@@ -546,6 +542,8 @@ async function fetchCharacter(
|
||||
|
||||
const frOrigin = frPage ? extractOrigin(frPage) : null;
|
||||
|
||||
const frDevilFruitTranslatedName = frPage ? (await extractDevilFruit(frPage, 'fr'))?.devilFruitTranslatedName || null : null;
|
||||
|
||||
if (name !== jsonData.parse?.title) {
|
||||
frName = name;
|
||||
}
|
||||
@@ -563,6 +561,8 @@ async function fetchCharacter(
|
||||
frOrigin,
|
||||
devilFruitId,
|
||||
devilFruitName,
|
||||
devilFruitTranslatedName,
|
||||
frDevilFruitTranslatedName,
|
||||
devilFruitUrl,
|
||||
affiliation,
|
||||
frAffiliation,
|
||||
@@ -703,17 +703,50 @@ function extractEpithets($: cheerio.CheerioAPI): string[] {
|
||||
* Extract devil fruit from infobox
|
||||
* Returns both normalized ID and URL
|
||||
*/
|
||||
async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData | null> {
|
||||
// dfname or dfename are used as data-source for devil fruit in the infobox, we check both to be safe
|
||||
const fruit = $('[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value').first();
|
||||
const link = fruit.find('a').first();
|
||||
if (link.length === 0) return null;
|
||||
async function extractDevilFruit($: cheerio.CheerioAPI, lang: 'en' | 'fr'): Promise<DevilFruitData | null> {
|
||||
// dfname, dfename, or dfnomf may be used depending on the wiki language and page format.
|
||||
const fruit = $(
|
||||
lang === 'fr'
|
||||
? '[data-source="dfnomf"] .pi-data-value, [data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value'
|
||||
: '[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value, [data-source="dfnomf"] .pi-data-value'
|
||||
).first();
|
||||
if (fruit.length === 0) return null;
|
||||
|
||||
let fruitTitle = fruit.text().trim().toLowerCase().includes('smile') ? fruit.text().trim() : link.text().trim();
|
||||
const fruitClone = fruit.clone();
|
||||
fruitClone.find('sup, s, del, strike').remove();
|
||||
const fruitText = fruitClone.text().replace(/\s+/g, ' ').trim();
|
||||
const link = fruit.find('a').first();
|
||||
|
||||
let fruitTitle = link.length > 0 ? link.text().trim() : fruitText;
|
||||
if (!fruitTitle) return null;
|
||||
|
||||
let translatedName = null;
|
||||
|
||||
if (lang === 'en') {
|
||||
const translatedNameBox = $('[data-source="dfename"] .pi-data-value').first();
|
||||
if (translatedNameBox.length > 0) {
|
||||
const translatedClone = translatedNameBox.clone();
|
||||
translatedClone.find('sup, s, del, strike').remove();
|
||||
translatedName = translatedClone.text().replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
} else if (lang === 'fr') {
|
||||
const translatedNameBox = $('[data-source="dfnomf"] .pi-data-value').first();
|
||||
if (translatedNameBox.length > 0) {
|
||||
const translatedClone = translatedNameBox.clone();
|
||||
translatedClone.find('sup, s, del, strike').remove();
|
||||
translatedName = translatedClone.text().replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
const href = link.attr('href');
|
||||
if (!href || !href.startsWith('/wiki/')) return null;
|
||||
if (!href || !href.startsWith('/wiki/')) {
|
||||
return {
|
||||
devilFruitId: normalizeId(fruitTitle),
|
||||
devilFruitTitle: fruitTitle,
|
||||
devilFruitTranslatedName: translatedName || null,
|
||||
devilFruitUrl: ''
|
||||
};
|
||||
}
|
||||
|
||||
const cleanUrl = href.replace('/wiki/', '');
|
||||
|
||||
@@ -727,6 +760,7 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
|
||||
return {
|
||||
devilFruitId: normalizeId(fruitTitle),
|
||||
devilFruitTitle: fruitTitle,
|
||||
devilFruitTranslatedName: translatedName || null,
|
||||
devilFruitUrl: cleanUrl
|
||||
};
|
||||
}
|
||||
@@ -918,6 +952,8 @@ async function saveToCSV(characters: Character[]): Promise<void> {
|
||||
async function fetchDevilFruit(
|
||||
devilFruitUrl: string,
|
||||
devilFruitTitle: string,
|
||||
devilFruitTranslatedName: string | null,
|
||||
frdevilFruitTranslatedName: string | null,
|
||||
devilFruitId: string
|
||||
): Promise<DevilFruit | null> {
|
||||
try {
|
||||
@@ -956,6 +992,8 @@ async function fetchDevilFruit(
|
||||
return {
|
||||
id: devilFruitId,
|
||||
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
|
||||
translatedName: devilFruitTranslatedName || null,
|
||||
frTranslatedName: frdevilFruitTranslatedName || null,
|
||||
type,
|
||||
url: devilFruitUrl
|
||||
};
|
||||
@@ -984,6 +1022,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
||||
header: [
|
||||
{ id: 'id', title: 'ID' },
|
||||
{ id: 'name', title: 'Name' },
|
||||
{ id: 'translatedName', title: 'Translated Name' },
|
||||
{ id: 'frTranslatedName', title: 'Translated Name (FR)' },
|
||||
{ id: 'type', title: 'Type' },
|
||||
{ id: 'url', title: 'URL' }
|
||||
]
|
||||
@@ -994,6 +1034,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
||||
.map((df) => ({
|
||||
id: df.id || '',
|
||||
name: df.name || '',
|
||||
translatedName: df.translatedName || '',
|
||||
frTranslatedName: df.frTranslatedName || '',
|
||||
type: df.type || '',
|
||||
url: df.url || ''
|
||||
}));
|
||||
@@ -1056,7 +1098,9 @@ async function main(): Promise<void> {
|
||||
const name = c.devilFruitName!;
|
||||
const url = c.devilFruitUrl!;
|
||||
const id = normalizeId(name);
|
||||
return [`${normalizeId(url)}::${id}`, { url, name, id }] as const;
|
||||
const translatedName = c.devilFruitTranslatedName || null;
|
||||
const frTranslatedName = c.frDevilFruitTranslatedName || null;
|
||||
return [`${normalizeId(url)}::${id}`, { url, name, id, translatedName, frTranslatedName }] as const;
|
||||
})
|
||||
)
|
||||
.values()
|
||||
@@ -1064,7 +1108,7 @@ async function main(): Promise<void> {
|
||||
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
|
||||
|
||||
// Step 3: Scraping Devil Fruits
|
||||
console.log('=== Step 2: Scraping Devil Fruits ===\n');
|
||||
console.log('=== Step 3: Scraping Devil Fruits ===\n');
|
||||
|
||||
if (devilFruitEntries.length === 0) {
|
||||
console.warn('No devil fruits found from characters, skipping...\n');
|
||||
@@ -1075,7 +1119,7 @@ async function main(): Promise<void> {
|
||||
const batch = devilFruitEntries.slice(i, i + FETCH_CONCURRENCY);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (entry) => {
|
||||
const data = await fetchDevilFruit(entry.url, entry.name, entry.id);
|
||||
const data = await fetchDevilFruit(entry.url, entry.name, entry.translatedName, entry.frTranslatedName, entry.id);
|
||||
return { entry, data };
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||
import type { DevilFruit } from '$lib/server/db/schema';
|
||||
import { language, t } from '$lib/i18n';
|
||||
|
||||
interface $$Props {
|
||||
dailyFruit: DevilFruit | null;
|
||||
selectedCharacters?: CharacterWithRelations[];
|
||||
}
|
||||
|
||||
export let dailyFruit: $$Props['dailyFruit'];
|
||||
export let selectedCharacters: NonNullable<$$Props['selectedCharacters']> = [];
|
||||
|
||||
let showFruitTypeHint = false;
|
||||
let showFruitTranslationHint = false;
|
||||
|
||||
$: isFruitTypeAvailable = selectedCharacters.length >= 3;
|
||||
$: isFruitTranslationAvailable = selectedCharacters.length >= 6;
|
||||
$: guessesBeforeUnlock = Math.max(0, 3 - selectedCharacters.length);
|
||||
$: translationGuessesBeforeUnlock = Math.max(0, 6 - selectedCharacters.length);
|
||||
$: isFrench = $language === 'fr';
|
||||
$: if (!isFruitTypeAvailable) {
|
||||
showFruitTypeHint = false;
|
||||
}
|
||||
$: if (!isFruitTranslationAvailable) {
|
||||
showFruitTranslationHint = false;
|
||||
}
|
||||
|
||||
function getFruitTranslation(fruit: DevilFruit | null): string | null {
|
||||
if (!fruit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isFrench) {
|
||||
return fruit.frTranslatedName ?? fruit.translatedName ?? null;
|
||||
}
|
||||
|
||||
return fruit.translatedName ?? fruit.frTranslatedName ?? null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
@keyframes hint-unlock {
|
||||
0% {
|
||||
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.hint-unlocking {
|
||||
animation: hint-unlock 0.6s ease-out;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full flex-col items-center justify-center rounded-2xl border border-white/10 px-3 py-4 transition-colors hover:bg-slate-900/80 {isFruitTypeAvailable ? 'bg-slate-950/60' : 'cursor-not-allowed bg-slate-950/30 opacity-50 hover:bg-slate-950/30'}"
|
||||
disabled={!isFruitTypeAvailable}
|
||||
onclick={() => (showFruitTypeHint = !showFruitTypeHint)}
|
||||
>
|
||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.fruitKind}</p>
|
||||
{#if showFruitTypeHint}
|
||||
<p class="mt-2 text-xs font-semibold text-white">{dailyFruit?.type || $t.game.components.hints.unknown}</p>
|
||||
{:else if guessesBeforeUnlock > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{guessesBeforeUnlock} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full flex-col items-center justify-center rounded-2xl border border-white/10 px-3 py-4 transition-colors hover:bg-slate-900/80 {isFruitTranslationAvailable ? 'bg-slate-950/60' : 'cursor-not-allowed bg-slate-950/30 opacity-50 hover:bg-slate-950/30'}"
|
||||
disabled={!isFruitTranslationAvailable}
|
||||
onclick={() => (showFruitTranslationHint = !showFruitTranslationHint)}
|
||||
>
|
||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.fruitTranslation}</p>
|
||||
{#if showFruitTranslationHint}
|
||||
<p class="mt-2 text-xs font-semibold text-white">{getFruitTranslation(dailyFruit) || $t.game.components.hints.unknown}</p>
|
||||
{:else if translationGuessesBeforeUnlock > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{translationGuessesBeforeUnlock} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,9 +13,9 @@
|
||||
let showHintAffiliation = false;
|
||||
|
||||
// Hint availability - indices are available after a certain number of guesses
|
||||
$: isOriginAvailable = selectedCharacters.length >= 5;
|
||||
$: isFruitAvailable = selectedCharacters.length >= 10;
|
||||
$: isAffiliationAvailable = selectedCharacters.length >= 15;
|
||||
$: isOriginAvailable = selectedCharacters.length >= 3;
|
||||
$: isFruitAvailable = selectedCharacters.length >= 6;
|
||||
$: isAffiliationAvailable = selectedCharacters.length >= 9;
|
||||
$: isFrench = $language === 'fr';
|
||||
|
||||
function getDisplayOrigin(character: CharacterWithRelations): string | null {
|
||||
@@ -57,8 +57,8 @@
|
||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.origin}</p>
|
||||
{#if showHintOrigin}
|
||||
<p class="mt-2 text-xs text-white font-semibold">{getDisplayOrigin(dailyCharacter) || $t.game.components.hints.unknown}</p>
|
||||
{:else if Math.max(0, 5 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else if Math.max(0, 3 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 3 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||
{/if}
|
||||
@@ -72,8 +72,8 @@
|
||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.devilFruit}</p>
|
||||
{#if showHintFruit}
|
||||
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || $t.game.components.hints.none}</p>
|
||||
{:else if Math.max(0, 10 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else if Math.max(0, 6 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 6 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||
{/if}
|
||||
@@ -87,8 +87,8 @@
|
||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
|
||||
{#if showHintAffiliation}
|
||||
<p class="mt-2 text-xs text-white font-semibold">{isFrench && dailyCharacter.frAffiliation ? dailyCharacter.frAffiliation : dailyCharacter.affiliation || $t.game.components.hints.unknown}</p>
|
||||
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else if Math.max(0, 9 - selectedCharacters.length) > 0}
|
||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 9 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||
{/if}
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"hints": {
|
||||
"origin": "Origin",
|
||||
"devilFruit": "Devil fruit",
|
||||
"fruitKind": "Fruit type",
|
||||
"fruitTranslation": "Fruit translation",
|
||||
"affiliation": "Affiliation",
|
||||
"unknown": "Unknown",
|
||||
"none": "None",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"hints": {
|
||||
"origin": "Origine",
|
||||
"devilFruit": "Fruit du demon",
|
||||
"fruitKind": "Type du fruit",
|
||||
"fruitTranslation": "Traduction du fruit",
|
||||
"affiliation": "Affiliation",
|
||||
"unknown": "Inconnue",
|
||||
"none": "Aucun",
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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();
|
||||
import { character, devilFruit, devilFruitHistory, type DevilFruit, type Character, type DevilFruitType } from '$lib/server/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
const characterWithFruitSelect = {
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
frName: character.frName,
|
||||
devilFruitId: character.devilFruitId,
|
||||
devilFruitName: devilFruit.name,
|
||||
devilFruitType: devilFruit.type,
|
||||
@@ -15,9 +13,11 @@ const characterWithFruitSelect = {
|
||||
url: devilFruit.url
|
||||
};
|
||||
|
||||
export type CharacterWithFruit = Character & {
|
||||
export type CharacterWithFruit = Pick<Character, 'id' | 'name' | 'devilFruitId'> & {
|
||||
devilFruitName: string | null;
|
||||
devilFruitType: string | null;
|
||||
devilFruitType: DevilFruitType | null;
|
||||
frName: string | null;
|
||||
url: string | null;
|
||||
};
|
||||
|
||||
export function getDateKey(date: Date): number {
|
||||
@@ -30,29 +30,29 @@ export function normalizeDay(date: Date = new Date()): Date {
|
||||
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];
|
||||
function pickDailyFruit(fruits: DevilFruit[]): DevilFruit {
|
||||
const randomIndex = Math.floor(Math.random() * fruits.length);
|
||||
return fruits[randomIndex];
|
||||
}
|
||||
|
||||
export async function getCharactersWithDevilFruit(): Promise<CharacterWithFruit[]> {
|
||||
const rows = (await db
|
||||
const rows = await db
|
||||
.select(characterWithFruitSelect)
|
||||
.from(character)
|
||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||
.all()) as any[];
|
||||
.where(eq(character.isInDailyMode, true))
|
||||
.all();
|
||||
|
||||
return rows.filter((r) => r.devilFruitId) as CharacterWithFruit[];
|
||||
return rows.filter((r): r is CharacterWithFruit => Boolean(r.devilFruitId));
|
||||
}
|
||||
|
||||
export async function getAllDevilFruitsFromCharacters(): Promise<DevilFruit[]> {
|
||||
const characters = (await db
|
||||
const characters = await db
|
||||
.select(characterWithFruitSelect)
|
||||
.from(character)
|
||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||
.all()) as any[];
|
||||
.where(eq(character.isInDailyMode, true))
|
||||
.all();
|
||||
|
||||
const map = new Map<string, DevilFruit>();
|
||||
for (const row of characters) {
|
||||
@@ -93,7 +93,7 @@ export async function getOrCreateTodayFruit(date: Date = new Date()): Promise<De
|
||||
const allFruits = await getAllDevilFruitsFromCharacters();
|
||||
if (allFruits.length === 0) return null;
|
||||
|
||||
const chosen = pickDailyFruit(allFruits, today);
|
||||
const chosen = pickDailyFruit(allFruits);
|
||||
|
||||
try {
|
||||
await db.insert(devilFruitHistory).values({
|
||||
|
||||
@@ -30,6 +30,8 @@ export type Arc = InferSelectModel<typeof arc>;
|
||||
export const devilFruit = sqliteTable('devil_fruit', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull().unique(),
|
||||
translatedName: text('translated_name'),
|
||||
frTranslatedName: text('fr_translated_name'),
|
||||
type: text('type').$type<DevilFruitType>(),
|
||||
url: text('url')
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.infiniteSubtitle}</p>
|
||||
<p class="mt-2 text-sm text-slate-200">{$t.game.home.infiniteDescription}</p>
|
||||
<a
|
||||
href={resolve("/infinite")}
|
||||
href={resolve("/infinite/character")}
|
||||
class="mt-5 inline-flex w-full items-center justify-center rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
|
||||
>
|
||||
{$t.game.home.infiniteCta}
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
||||
|
||||
// Send request to record win in database
|
||||
fetch('/daily', {
|
||||
fetch('/daily/character', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getAllDevilFruitsFromCharacters, getOrCreateTodayFruit, getYesterdayFruit, getTodayFruitWinsCount } from '$lib/server/daily-fruit';
|
||||
import { getAllCharacters } from '$lib/server/daily-character';
|
||||
import { getDailyModeCharacters } from '$lib/server/daily-character';
|
||||
|
||||
export async function load(event) {
|
||||
const fruits = await getAllDevilFruitsFromCharacters();
|
||||
@@ -12,8 +12,8 @@ export async function load(event) {
|
||||
|
||||
const yesterdayFruit = await getYesterdayFruit(new Date());
|
||||
|
||||
// Load all characters for searching and filter only those with a devil fruit
|
||||
const allCharacters = await getAllCharacters();
|
||||
// Load daily-mode characters for searching and filter only those with a devil fruit
|
||||
const allCharacters = await getDailyModeCharacters();
|
||||
const characters = allCharacters.filter((c) => c.devilFruitId);
|
||||
|
||||
// Find the character tied to the chosen fruit among characters that have a devil fruit
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
|
||||
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||
import DailyFruitHint from '$lib/components/DailyFruitHint.svelte';
|
||||
import SimpleGuessHistory from '$lib/components/SimpleGuessHistory.svelte';
|
||||
import WinPanel from '$lib/components/WinPanel.svelte';
|
||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||
@@ -43,8 +44,6 @@
|
||||
isLoaded = true;
|
||||
});
|
||||
|
||||
onDestroy(() => {});
|
||||
|
||||
$: if (isLoaded && selectedCharacters) {
|
||||
const ids = selectedCharacters.map(char => char.id);
|
||||
localStorage.setItem('dailyFruitHistory', JSON.stringify(ids));
|
||||
@@ -121,6 +120,13 @@
|
||||
<p class="mt-2 text-center text-xl font-bold text-amber-50 sm:text-2xl">{dailyFruit?.name}</p>
|
||||
</div>
|
||||
|
||||
<section class="mt-6">
|
||||
<DailyFruitHint
|
||||
dailyFruit={dailyFruit}
|
||||
{selectedCharacters}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 grid gap-6">
|
||||
{#if hasWon}
|
||||
<WinPanel
|
||||
|
||||
Reference in New Issue
Block a user