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,
|
"when": 1782152097346,
|
||||||
"tag": "0004_magenta_screwball",
|
"tag": "0004_magenta_screwball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1783545248600,
|
||||||
|
"tag": "0005_uneven_talkback",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ type ArcRecord = {
|
|||||||
type DevilFruitRecord = {
|
type DevilFruitRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
translatedName?: string | null;
|
||||||
|
frTranslatedName?: string | null;
|
||||||
type?: DevilFruitType | string | null;
|
type?: DevilFruitType | string | null;
|
||||||
url?: 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 {
|
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
|
||||||
if (!value) return 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 value;
|
||||||
}
|
}
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
@@ -229,6 +231,8 @@ async function importFromJson(): Promise<void> {
|
|||||||
.values({
|
.values({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
translatedName: toNullable(item.translatedName),
|
||||||
|
frTranslatedName: toNullable(item.frTranslatedName),
|
||||||
type: toDevilFruitType(item.type),
|
type: toDevilFruitType(item.type),
|
||||||
url: toNullable(item.url)
|
url: toNullable(item.url)
|
||||||
})
|
})
|
||||||
@@ -236,6 +240,8 @@ async function importFromJson(): Promise<void> {
|
|||||||
target: devilFruit.id,
|
target: devilFruit.id,
|
||||||
set: {
|
set: {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
translatedName: toNullable(item.translatedName),
|
||||||
|
frTranslatedName: toNullable(item.frTranslatedName),
|
||||||
type: toDevilFruitType(item.type),
|
type: toDevilFruitType(item.type),
|
||||||
url: toNullable(item.url)
|
url: toNullable(item.url)
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-22
@@ -23,6 +23,8 @@ interface Character {
|
|||||||
frOrigin: string | null;
|
frOrigin: string | null;
|
||||||
devilFruitId: string | null;
|
devilFruitId: string | null;
|
||||||
devilFruitName: string | null;
|
devilFruitName: string | null;
|
||||||
|
devilFruitTranslatedName: string | null;
|
||||||
|
frDevilFruitTranslatedName: string | null;
|
||||||
devilFruitUrl: string | null;
|
devilFruitUrl: string | null;
|
||||||
affiliation: string | null;
|
affiliation: string | null;
|
||||||
frAffiliation: string | null;
|
frAffiliation: string | null;
|
||||||
@@ -49,12 +51,16 @@ interface CharacterListItem {
|
|||||||
interface DevilFruitData {
|
interface DevilFruitData {
|
||||||
devilFruitId: string;
|
devilFruitId: string;
|
||||||
devilFruitTitle: string;
|
devilFruitTitle: string;
|
||||||
|
devilFruitTranslatedName?: string | null;
|
||||||
|
frDevilFruitTranslatedName?: string | null;
|
||||||
devilFruitUrl: string;
|
devilFruitUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DevilFruit {
|
interface DevilFruit {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
translatedName?: string | null;
|
||||||
|
frTranslatedName?: string | null;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
@@ -173,17 +179,6 @@ function removeParentheticalNotes(text: string): string {
|
|||||||
return text.replace(/\([^)]*\)/g, '').trim();
|
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
|
* Normalize string by decoding URI components, punctuation, and replacing spaces with underscores
|
||||||
@@ -485,9 +480,10 @@ async function fetchCharacter(
|
|||||||
const epithets = extractEpithets($);
|
const epithets = extractEpithets($);
|
||||||
|
|
||||||
// Extract devil fruit
|
// Extract devil fruit
|
||||||
const devilFruitData = await extractDevilFruit($);
|
const devilFruitData = await extractDevilFruit($, 'en');
|
||||||
const devilFruitId = devilFruitData?.devilFruitId || null;
|
const devilFruitId = devilFruitData?.devilFruitId || null;
|
||||||
const devilFruitName = devilFruitData?.devilFruitTitle || null;
|
const devilFruitName = devilFruitData?.devilFruitTitle || null;
|
||||||
|
const devilFruitTranslatedName = devilFruitData?.devilFruitTranslatedName || null;
|
||||||
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
|
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
|
||||||
|
|
||||||
// Extract haki from JSON categories
|
// Extract haki from JSON categories
|
||||||
@@ -546,6 +542,8 @@ async function fetchCharacter(
|
|||||||
|
|
||||||
const frOrigin = frPage ? extractOrigin(frPage) : null;
|
const frOrigin = frPage ? extractOrigin(frPage) : null;
|
||||||
|
|
||||||
|
const frDevilFruitTranslatedName = frPage ? (await extractDevilFruit(frPage, 'fr'))?.devilFruitTranslatedName || null : null;
|
||||||
|
|
||||||
if (name !== jsonData.parse?.title) {
|
if (name !== jsonData.parse?.title) {
|
||||||
frName = name;
|
frName = name;
|
||||||
}
|
}
|
||||||
@@ -563,6 +561,8 @@ async function fetchCharacter(
|
|||||||
frOrigin,
|
frOrigin,
|
||||||
devilFruitId,
|
devilFruitId,
|
||||||
devilFruitName,
|
devilFruitName,
|
||||||
|
devilFruitTranslatedName,
|
||||||
|
frDevilFruitTranslatedName,
|
||||||
devilFruitUrl,
|
devilFruitUrl,
|
||||||
affiliation,
|
affiliation,
|
||||||
frAffiliation,
|
frAffiliation,
|
||||||
@@ -703,17 +703,50 @@ function extractEpithets($: cheerio.CheerioAPI): string[] {
|
|||||||
* Extract devil fruit from infobox
|
* Extract devil fruit from infobox
|
||||||
* Returns both normalized ID and URL
|
* Returns both normalized ID and URL
|
||||||
*/
|
*/
|
||||||
async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData | null> {
|
async function extractDevilFruit($: cheerio.CheerioAPI, lang: 'en' | 'fr'): Promise<DevilFruitData | null> {
|
||||||
// dfname or dfename are used as data-source for devil fruit in the infobox, we check both to be safe
|
// dfname, dfename, or dfnomf may be used depending on the wiki language and page format.
|
||||||
const fruit = $('[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value').first();
|
const fruit = $(
|
||||||
const link = fruit.find('a').first();
|
lang === 'fr'
|
||||||
if (link.length === 0) return null;
|
? '[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;
|
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');
|
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/', '');
|
const cleanUrl = href.replace('/wiki/', '');
|
||||||
|
|
||||||
@@ -727,6 +760,7 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
|
|||||||
return {
|
return {
|
||||||
devilFruitId: normalizeId(fruitTitle),
|
devilFruitId: normalizeId(fruitTitle),
|
||||||
devilFruitTitle: fruitTitle,
|
devilFruitTitle: fruitTitle,
|
||||||
|
devilFruitTranslatedName: translatedName || null,
|
||||||
devilFruitUrl: cleanUrl
|
devilFruitUrl: cleanUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -918,6 +952,8 @@ async function saveToCSV(characters: Character[]): Promise<void> {
|
|||||||
async function fetchDevilFruit(
|
async function fetchDevilFruit(
|
||||||
devilFruitUrl: string,
|
devilFruitUrl: string,
|
||||||
devilFruitTitle: string,
|
devilFruitTitle: string,
|
||||||
|
devilFruitTranslatedName: string | null,
|
||||||
|
frdevilFruitTranslatedName: string | null,
|
||||||
devilFruitId: string
|
devilFruitId: string
|
||||||
): Promise<DevilFruit | null> {
|
): Promise<DevilFruit | null> {
|
||||||
try {
|
try {
|
||||||
@@ -956,6 +992,8 @@ async function fetchDevilFruit(
|
|||||||
return {
|
return {
|
||||||
id: devilFruitId,
|
id: devilFruitId,
|
||||||
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
|
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
|
||||||
|
translatedName: devilFruitTranslatedName || null,
|
||||||
|
frTranslatedName: frdevilFruitTranslatedName || null,
|
||||||
type,
|
type,
|
||||||
url: devilFruitUrl
|
url: devilFruitUrl
|
||||||
};
|
};
|
||||||
@@ -984,6 +1022,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
|||||||
header: [
|
header: [
|
||||||
{ id: 'id', title: 'ID' },
|
{ id: 'id', title: 'ID' },
|
||||||
{ id: 'name', title: 'Name' },
|
{ id: 'name', title: 'Name' },
|
||||||
|
{ id: 'translatedName', title: 'Translated Name' },
|
||||||
|
{ id: 'frTranslatedName', title: 'Translated Name (FR)' },
|
||||||
{ id: 'type', title: 'Type' },
|
{ id: 'type', title: 'Type' },
|
||||||
{ id: 'url', title: 'URL' }
|
{ id: 'url', title: 'URL' }
|
||||||
]
|
]
|
||||||
@@ -994,6 +1034,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
|||||||
.map((df) => ({
|
.map((df) => ({
|
||||||
id: df.id || '',
|
id: df.id || '',
|
||||||
name: df.name || '',
|
name: df.name || '',
|
||||||
|
translatedName: df.translatedName || '',
|
||||||
|
frTranslatedName: df.frTranslatedName || '',
|
||||||
type: df.type || '',
|
type: df.type || '',
|
||||||
url: df.url || ''
|
url: df.url || ''
|
||||||
}));
|
}));
|
||||||
@@ -1056,7 +1098,9 @@ async function main(): Promise<void> {
|
|||||||
const name = c.devilFruitName!;
|
const name = c.devilFruitName!;
|
||||||
const url = c.devilFruitUrl!;
|
const url = c.devilFruitUrl!;
|
||||||
const id = normalizeId(name);
|
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()
|
.values()
|
||||||
@@ -1064,7 +1108,7 @@ async function main(): Promise<void> {
|
|||||||
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
|
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
|
||||||
|
|
||||||
// Step 3: Scraping Devil Fruits
|
// 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) {
|
if (devilFruitEntries.length === 0) {
|
||||||
console.warn('No devil fruits found from characters, skipping...\n');
|
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 batch = devilFruitEntries.slice(i, i + FETCH_CONCURRENCY);
|
||||||
const batchResults = await Promise.all(
|
const batchResults = await Promise.all(
|
||||||
batch.map(async (entry) => {
|
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 };
|
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;
|
let showHintAffiliation = false;
|
||||||
|
|
||||||
// Hint availability - indices are available after a certain number of guesses
|
// Hint availability - indices are available after a certain number of guesses
|
||||||
$: isOriginAvailable = selectedCharacters.length >= 5;
|
$: isOriginAvailable = selectedCharacters.length >= 3;
|
||||||
$: isFruitAvailable = selectedCharacters.length >= 10;
|
$: isFruitAvailable = selectedCharacters.length >= 6;
|
||||||
$: isAffiliationAvailable = selectedCharacters.length >= 15;
|
$: isAffiliationAvailable = selectedCharacters.length >= 9;
|
||||||
$: isFrench = $language === 'fr';
|
$: isFrench = $language === 'fr';
|
||||||
|
|
||||||
function getDisplayOrigin(character: CharacterWithRelations): string | null {
|
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>
|
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.origin}</p>
|
||||||
{#if showHintOrigin}
|
{#if showHintOrigin}
|
||||||
<p class="mt-2 text-xs text-white font-semibold">{getDisplayOrigin(dailyCharacter) || $t.game.components.hints.unknown}</p>
|
<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}
|
{:else if Math.max(0, 3 - selectedCharacters.length) > 0}
|
||||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 3 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.devilFruit}</p>
|
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.devilFruit}</p>
|
||||||
{#if showHintFruit}
|
{#if showHintFruit}
|
||||||
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || $t.game.components.hints.none}</p>
|
<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}
|
{:else if Math.max(0, 6 - selectedCharacters.length) > 0}
|
||||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 6 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -87,8 +87,8 @@
|
|||||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
|
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
|
||||||
{#if showHintAffiliation}
|
{#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>
|
<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}
|
{:else if Math.max(0, 9 - selectedCharacters.length) > 0}
|
||||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 9 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
<p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -160,6 +160,8 @@
|
|||||||
"hints": {
|
"hints": {
|
||||||
"origin": "Origin",
|
"origin": "Origin",
|
||||||
"devilFruit": "Devil fruit",
|
"devilFruit": "Devil fruit",
|
||||||
|
"fruitKind": "Fruit type",
|
||||||
|
"fruitTranslation": "Fruit translation",
|
||||||
"affiliation": "Affiliation",
|
"affiliation": "Affiliation",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
|||||||
@@ -160,6 +160,8 @@
|
|||||||
"hints": {
|
"hints": {
|
||||||
"origin": "Origine",
|
"origin": "Origine",
|
||||||
"devilFruit": "Fruit du demon",
|
"devilFruit": "Fruit du demon",
|
||||||
|
"fruitKind": "Type du fruit",
|
||||||
|
"fruitTranslation": "Traduction du fruit",
|
||||||
"affiliation": "Affiliation",
|
"affiliation": "Affiliation",
|
||||||
"unknown": "Inconnue",
|
"unknown": "Inconnue",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { character, devilFruit, devilFruitHistory, type DevilFruit, type Character } from '$lib/server/db/schema';
|
import { character, devilFruit, devilFruitHistory, type DevilFruit, type Character, type DevilFruitType } from '$lib/server/db/schema';
|
||||||
import { desc, eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
// Generate or get random seed for daily fruit selection
|
|
||||||
const RANDOM_SEED = Math.random();
|
|
||||||
|
|
||||||
const characterWithFruitSelect = {
|
const characterWithFruitSelect = {
|
||||||
id: character.id,
|
id: character.id,
|
||||||
name: character.name,
|
name: character.name,
|
||||||
|
frName: character.frName,
|
||||||
devilFruitId: character.devilFruitId,
|
devilFruitId: character.devilFruitId,
|
||||||
devilFruitName: devilFruit.name,
|
devilFruitName: devilFruit.name,
|
||||||
devilFruitType: devilFruit.type,
|
devilFruitType: devilFruit.type,
|
||||||
@@ -15,9 +13,11 @@ const characterWithFruitSelect = {
|
|||||||
url: devilFruit.url
|
url: devilFruit.url
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CharacterWithFruit = Character & {
|
export type CharacterWithFruit = Pick<Character, 'id' | 'name' | 'devilFruitId'> & {
|
||||||
devilFruitName: string | null;
|
devilFruitName: string | null;
|
||||||
devilFruitType: string | null;
|
devilFruitType: DevilFruitType | null;
|
||||||
|
frName: string | null;
|
||||||
|
url: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDateKey(date: Date): number {
|
export function getDateKey(date: Date): number {
|
||||||
@@ -30,29 +30,29 @@ export function normalizeDay(date: Date = new Date()): Date {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickDailyFruit(fruits: DevilFruit[], date: Date): DevilFruit {
|
function pickDailyFruit(fruits: DevilFruit[]): DevilFruit {
|
||||||
const timestamp = getDateKey(date);
|
const randomIndex = Math.floor(Math.random() * fruits.length);
|
||||||
const daysSinceEpoch = Math.floor(timestamp / 1000 / 60 / 60 / 24);
|
return fruits[randomIndex];
|
||||||
const combinedSeed = (daysSinceEpoch + Math.floor(RANDOM_SEED * 1000000)) % fruits.length;
|
|
||||||
return fruits[combinedSeed];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCharactersWithDevilFruit(): Promise<CharacterWithFruit[]> {
|
export async function getCharactersWithDevilFruit(): Promise<CharacterWithFruit[]> {
|
||||||
const rows = (await db
|
const rows = await db
|
||||||
.select(characterWithFruitSelect)
|
.select(characterWithFruitSelect)
|
||||||
.from(character)
|
.from(character)
|
||||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
.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[]> {
|
export async function getAllDevilFruitsFromCharacters(): Promise<DevilFruit[]> {
|
||||||
const characters = (await db
|
const characters = await db
|
||||||
.select(characterWithFruitSelect)
|
.select(characterWithFruitSelect)
|
||||||
.from(character)
|
.from(character)
|
||||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
.all()) as any[];
|
.where(eq(character.isInDailyMode, true))
|
||||||
|
.all();
|
||||||
|
|
||||||
const map = new Map<string, DevilFruit>();
|
const map = new Map<string, DevilFruit>();
|
||||||
for (const row of characters) {
|
for (const row of characters) {
|
||||||
@@ -93,7 +93,7 @@ export async function getOrCreateTodayFruit(date: Date = new Date()): Promise<De
|
|||||||
const allFruits = await getAllDevilFruitsFromCharacters();
|
const allFruits = await getAllDevilFruitsFromCharacters();
|
||||||
if (allFruits.length === 0) return null;
|
if (allFruits.length === 0) return null;
|
||||||
|
|
||||||
const chosen = pickDailyFruit(allFruits, today);
|
const chosen = pickDailyFruit(allFruits);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.insert(devilFruitHistory).values({
|
await db.insert(devilFruitHistory).values({
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export type Arc = InferSelectModel<typeof arc>;
|
|||||||
export const devilFruit = sqliteTable('devil_fruit', {
|
export const devilFruit = sqliteTable('devil_fruit', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
name: text('name').notNull().unique(),
|
name: text('name').notNull().unique(),
|
||||||
|
translatedName: text('translated_name'),
|
||||||
|
frTranslatedName: text('fr_translated_name'),
|
||||||
type: text('type').$type<DevilFruitType>(),
|
type: text('type').$type<DevilFruitType>(),
|
||||||
url: text('url')
|
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-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>
|
<p class="mt-2 text-sm text-slate-200">{$t.game.home.infiniteDescription}</p>
|
||||||
<a
|
<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"
|
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}
|
{$t.game.home.infiniteCta}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
||||||
|
|
||||||
// Send request to record win in database
|
// Send request to record win in database
|
||||||
fetch('/daily', {
|
fetch('/daily/character', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { getAllDevilFruitsFromCharacters, getOrCreateTodayFruit, getYesterdayFruit, getTodayFruitWinsCount } from '$lib/server/daily-fruit';
|
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) {
|
export async function load(event) {
|
||||||
const fruits = await getAllDevilFruitsFromCharacters();
|
const fruits = await getAllDevilFruitsFromCharacters();
|
||||||
@@ -12,8 +12,8 @@ export async function load(event) {
|
|||||||
|
|
||||||
const yesterdayFruit = await getYesterdayFruit(new Date());
|
const yesterdayFruit = await getYesterdayFruit(new Date());
|
||||||
|
|
||||||
// Load all characters for searching and filter only those with a devil fruit
|
// Load daily-mode characters for searching and filter only those with a devil fruit
|
||||||
const allCharacters = await getAllCharacters();
|
const allCharacters = await getDailyModeCharacters();
|
||||||
const characters = allCharacters.filter((c) => c.devilFruitId);
|
const characters = allCharacters.filter((c) => c.devilFruitId);
|
||||||
|
|
||||||
// Find the character tied to the chosen fruit among characters that have a devil fruit
|
// Find the character tied to the chosen fruit among characters that have a devil fruit
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
|
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
|
||||||
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||||
|
import DailyFruitHint from '$lib/components/DailyFruitHint.svelte';
|
||||||
import SimpleGuessHistory from '$lib/components/SimpleGuessHistory.svelte';
|
import SimpleGuessHistory from '$lib/components/SimpleGuessHistory.svelte';
|
||||||
import WinPanel from '$lib/components/WinPanel.svelte';
|
import WinPanel from '$lib/components/WinPanel.svelte';
|
||||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||||
@@ -43,8 +44,6 @@
|
|||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {});
|
|
||||||
|
|
||||||
$: if (isLoaded && selectedCharacters) {
|
$: if (isLoaded && selectedCharacters) {
|
||||||
const ids = selectedCharacters.map(char => char.id);
|
const ids = selectedCharacters.map(char => char.id);
|
||||||
localStorage.setItem('dailyFruitHistory', JSON.stringify(ids));
|
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>
|
<p class="mt-2 text-center text-xl font-bold text-amber-50 sm:text-2xl">{dailyFruit?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="mt-6">
|
||||||
|
<DailyFruitHint
|
||||||
|
dailyFruit={dailyFruit}
|
||||||
|
{selectedCharacters}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-6 grid gap-6">
|
<section class="mt-6 grid gap-6">
|
||||||
{#if hasWon}
|
{#if hasWon}
|
||||||
<WinPanel
|
<WinPanel
|
||||||
|
|||||||
Reference in New Issue
Block a user