Files
ki-fwi/src/routes/[[locale=locale]]/je/rekonet/+page.svelte
T

277 lines
7.1 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { page } from '$app/state';
import Seo from '$lib/components/Seo.svelte';
import { chemin, t } from '$lib/i18n';
import { FRUITS, entreesJouables, photosValidees } from '$lib/data';
import { melanger, propositions } from '$lib/jeu/tirage';
import { ordreRevision, repondre } from '$lib/jeu/progression.svelte';
import { compte } from '$lib/motion/countup';
import type { Entree, Media } from '$lib/data/types';
const { data } = $props();
const locale = $derived(data.locale);
const QUESTIONS = 8;
const POINTS = 25;
interface Question {
cible: Entree;
photo: Media;
choix: Entree[];
}
/**
* Le bassin ne contient que les espèces disposant d'une photographie de filière A,
* validée par un humain nommé et portant l'usage « épreuve ». C'est la même règle
* que `tools/data/validate.mjs`, qui fait échouer le build si elle est violée.
*
* Aujourd'hui ce bassin est vide, et c'est le comportement correct : personne n'a
* encore validé de photo. Le mode ne triche pas avec une image générée.
*/
const jouables = entreesJouables();
function fabriquer(): Question[] {
return ordreRevision(jouables.map((e) => e.id))
.slice(0, QUESTIONS)
.map((id) => {
const cible = jouables.find((e) => e.id === id) as Entree;
const photos = photosValidees(cible).filter((m) => m.usages.includes('epreuve'));
return {
cible,
photo: melanger(photos)[0],
choix: propositions(cible, jouables)
};
});
}
let questions = $state<Question[]>(jouables.length >= 4 ? fabriquer() : []);
let index = $state(0);
let reponse = $state<Entree | null>(null);
let score = $state(0);
let bonnes = $state(0);
const q = $derived(questions[index]);
const fini = $derived(questions.length > 0 && index >= questions.length);
const correct = $derived(reponse?.id === q?.cible.id);
function choisir(e: Entree) {
if (reponse || !q) return;
reponse = e;
const juste = e.id === q.cible.id;
if (juste) {
bonnes += 1;
score += POINTS;
}
repondre(q.cible.id, juste, juste ? POINTS : 0);
}
function suivant() {
reponse = null;
index += 1;
}
function rejouer() {
questions = fabriquer();
index = 0;
reponse = null;
score = 0;
bonnes = 0;
}
</script>
<Seo {locale} pathname={page.url.pathname} titre={t(locale, 'jeu.rekonet')} />
<div class="container section jeu">
<p class="fil"><a href={chemin(locale, 'je')}>{t(locale, 'jeu.quitter')}</a></p>
{#if questions.length === 0}
<!-- État designé, pas page d'erreur : le manque est une information (ADR 0006). -->
<section class="bloque" aria-labelledby="bloque-titre">
<span class="flag-chip" aria-hidden="true"></span>
<h1 id="bloque-titre">{t(locale, 'jeu.indisponible.titre')}</h1>
<p>{t(locale, 'jeu.indisponible.texte')}</p>
<p class="chiffre">
<strong>{jouables.length}</strong> espèce{jouables.length > 1 ? 's' : ''} sur {FRUITS.length}
dispose dune photographie validée. Il en faut au moins quatre pour poser une question
honnête.
</p>
<p class="muted">
Ce mode fonctionne uniquement sur des photographies réelles, validées par un humain. Il ne
se rabattra jamais sur une image générée : ce serait apprendre à reconnaître ce quun
modèle invente.
</p>
<p class="actions">
<a class="btn" href={chemin(locale, 'kontribiye')}>{t(locale, 'manques.aider')}</a>
<a class="btn btn--sobre" href={chemin(locale, 'je', 'kache')}>{t(locale, 'jeu.kache')}</a>
</p>
</section>
{:else if fini}
<section class="bilan" aria-labelledby="bilan-titre">
<h1 id="bilan-titre">{t(locale, 'jeu.termine')}</h1>
<p class="gros">
<span use:compte={score} aria-hidden="true">0</span>
<span class="sr-only">{score}</span>
</p>
<p>{t(locale, 'jeu.resultat', { bons: bonnes, total: questions.length })}</p>
<p class="actions">
<button class="btn" onclick={rejouer}>{t(locale, 'jeu.rejouer')}</button>
<a class="btn" href={chemin(locale, 'je', 'lakou')}>{t(locale, 'jeu.album')}</a>
</p>
</section>
{:else if q}
<header class="entete">
<h1 lang="gcf">{t(locale, 'jeu.rekonet')}</h1>
<p class="muted">{index + 1} / {questions.length}{t(locale, 'jeu.score')} : {score}</p>
</header>
<figure class="photo">
<img src={q.photo.fichier_local ?? ''} alt={t(locale, 'jeu.question')} />
<figcaption class="muted petit">
{q.photo.auteur}{q.photo.licence}
</figcaption>
</figure>
<section aria-labelledby="question-titre">
<h2 id="question-titre">{t(locale, 'jeu.question')}</h2>
<ul class="choix">
{#each q.choix as choix (choix.id)}
<li>
<button
class="btn reponse"
class:juste={reponse && choix.id === q.cible.id}
class:faux={reponse === choix && choix.id !== q.cible.id}
disabled={!!reponse}
onclick={() => choisir(choix)}
>
<span lang="gcf">{choix.noms.gcf}</span>
</button>
</li>
{/each}
</ul>
</section>
<div class="revelation" aria-live="polite">
{#if reponse}
<p class="verdict" class:ok={correct}>
{correct ? t(locale, 'jeu.bonne-reponse') : t(locale, 'jeu.mauvaise-reponse')}
</p>
<p>{t(locale, 'jeu.reponse-etait', { nom: q.cible.noms.gcf })}</p>
<p class="actions">
<button class="btn" onclick={suivant}>{t(locale, 'jeu.suivant')}</button>
<a href={chemin(locale, 'zerbaj', q.cible.id)}>{t(locale, 'zerbaj.voir')}</a>
</p>
{/if}
</div>
{/if}
</div>
<style>
.jeu {
max-width: 44rem;
}
.fil {
font-size: 0.85rem;
}
.bloque {
padding: var(--space-3);
background: var(--card-bg);
border: var(--border-card);
border-left: 4px solid var(--or-oki);
border-radius: var(--radius-sm);
}
.chiffre strong {
font-family: var(--font-display);
font-size: 1.3rem;
color: var(--or-oki);
}
.photo {
margin: 0 0 var(--space-3);
}
.photo img {
width: 100%;
border-radius: var(--radius-sm);
border: var(--border-card);
}
.choix {
display: grid;
gap: var(--space-1);
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
list-style: none;
margin: 0;
padding: 0;
}
.reponse {
width: 100%;
font-size: 1.05rem;
}
.reponse.juste {
border-color: var(--vert-oki);
color: var(--vert-oki);
}
.reponse.juste::after {
background: var(--vert-oki);
}
.reponse.faux {
border-color: var(--rouge-oki);
color: var(--rouge-oki);
}
.reponse.faux::after {
background: var(--rouge-oki);
}
.reponse[disabled] {
opacity: 1;
}
.revelation {
margin-top: var(--space-3);
min-height: 6rem;
}
.verdict {
font-family: var(--font-display);
font-size: 1.2rem;
text-transform: uppercase;
color: var(--rouge-oki);
margin-bottom: 0.25rem;
}
.verdict.ok {
color: var(--vert-oki);
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
}
.btn--sobre {
color: var(--blanc-creme);
border-color: var(--line);
}
.bilan {
text-align: center;
}
.gros {
font-family: var(--font-display);
font-size: clamp(3rem, 12vw, 6rem);
color: var(--or-oki);
margin: var(--space-2) 0;
line-height: 1;
}
.petit {
font-size: 0.85rem;
}
</style>