Affiche la deuxième pièce à venir dans La Corne d'Abondance
Build and deploy / deploy (push) Successful in 36s
Build and deploy / deploy (push) Successful in 36s
Petit badge "Puis" à côté du score, taille d'affichage fixe (juste informatif, pas de position à viser) — la file glisse d'un cran à chaque lâcher : ce qui était annoncé en second devient la pièce à viser. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ V1 à V11 sont livrées :
|
|||||||
- **V8** : les Citoyens peuvent désormais proposer une question pour Le Char — une par personne (upsert), modifiable autant de fois que voulu depuis une nouvelle section sur `/profile`, jusqu'à ce que la date du Tribunal (`settings.tribunal_date`) soit atteinte (même mécanisme de gel que les scores d'Icare, no-op silencieux plutôt qu'une erreur). Les Archontes modèrent ces suggestions depuis une nouvelle section « Suggestions des Citoyens » sur `/char/questions` : chaque proposition affiche l'auteur, avec un bouton « Ajouter à la banque » (la copie dans `chariot_questions`, à la fin de l'ordre existant) et un bouton de rejet (suppression, confirmation à deux temps comme le reste de la page). Par ailleurs, `/admin` affiche maintenant l'adresse email de chaque membre (RPC `admin_list_members()`, seul moyen d'exposer `auth.users.email` aux juges sans passer par la `service_role key` côté client).
|
- **V8** : les Citoyens peuvent désormais proposer une question pour Le Char — une par personne (upsert), modifiable autant de fois que voulu depuis une nouvelle section sur `/profile`, jusqu'à ce que la date du Tribunal (`settings.tribunal_date`) soit atteinte (même mécanisme de gel que les scores d'Icare, no-op silencieux plutôt qu'une erreur). Les Archontes modèrent ces suggestions depuis une nouvelle section « Suggestions des Citoyens » sur `/char/questions` : chaque proposition affiche l'auteur, avec un bouton « Ajouter à la banque » (la copie dans `chariot_questions`, à la fin de l'ordre existant) et un bouton de rejet (suppression, confirmation à deux temps comme le reste de la page). Par ailleurs, `/admin` affiche maintenant l'adresse email de chaque membre (RPC `admin_list_members()`, seul moyen d'exposer `auth.users.email` aux juges sans passer par la `service_role key` côté client).
|
||||||
- **V9** : **Le Mur de la Honte** (`/mur`) — chaque Citoyen peut publier une note anonyme par jour (dix places maximum sur le mur, remis à zéro chaque jour — pas de suppression, juste un filtrage par date). L'anonymat n'est qu'à moitié réel : les Archontes voient une section « Historique complet » (filtrable par jour) avec l'auteur de chaque note jamais publiée, cohérent avec le thème du site (les Archontes ont un ascendant sur les Citoyens). Pas de gel lié à la date du Tribunal : contrairement au Char, c'est une mécanique de toute la semaine, pas de la seule soirée de clôture.
|
- **V9** : **Le Mur de la Honte** (`/mur`) — chaque Citoyen peut publier une note anonyme par jour (dix places maximum sur le mur, remis à zéro chaque jour — pas de suppression, juste un filtrage par date). L'anonymat n'est qu'à moitié réel : les Archontes voient une section « Historique complet » (filtrable par jour) avec l'auteur de chaque note jamais publiée, cohérent avec le thème du site (les Archontes ont un ascendant sur les Citoyens). Pas de gel lié à la date du Tribunal : contrairement au Char, c'est une mécanique de toute la semaine, pas de la seule soirée de clôture.
|
||||||
- **V10** : **L'Urne de l'Agora** (`/urne`) — vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque Citoyen dépose un jeton par jour sur une personne de son choix (jamais lui-même), confirmation à deux temps avant l'envoi (vote définitif pour la journée, aucune RPC de modification). Contrairement au Mur de la Honte, les jetons s'accumulent sur toute la semaine dans un classement public et cumulatif — **identique pour tout le monde, Archontes compris** : contraste volontaire avec le Mur, ici les Archontes n'ont aucune vision privilégiée sur qui a voté pour qui, seul le total par personne est public. Les Archontes ne votent pas et ne peuvent pas recevoir de jetons.
|
- **V10** : **L'Urne de l'Agora** (`/urne`) — vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque Citoyen dépose un jeton par jour sur une personne de son choix (jamais lui-même), confirmation à deux temps avant l'envoi (vote définitif pour la journée, aucune RPC de modification). Contrairement au Mur de la Honte, les jetons s'accumulent sur toute la semaine dans un classement public et cumulatif — **identique pour tout le monde, Archontes compris** : contraste volontaire avec le Mur, ici les Archontes n'ont aucune vision privilégiée sur qui a voté pour qui, seul le total par personne est public. Les Archontes ne votent pas et ne peuvent pas recevoir de jetons.
|
||||||
- **V11** : deuxième mini-jeu compétitif, **La Corne d'Abondance** (`/corne`) — jeu de fusion façon « Watermelon Game » : des avatars de Citoyens tombent dans un bac (physique gérée par `matter-js`, seule dépendance de ce type dans le projet — l'empilement stable de cercles est notoirement difficile à obtenir sans bugs de superposition/tremblement en physique artisanale), deux avatars de même taille qui se touchent fusionnent en un plus gros, jusqu'au débordement soutenu (5 secondes de délai de grâce, pour ne pas sanctionner un simple rebond) qui met fin à la partie — les pièces qui dépassent la ligne de danger clignotent (`.animate-danger-blink`) pour signaler que le compte à rebours est en cours. **Un palier de taille par Citoyen** (pas les Archontes) plutôt qu'un nombre de paliers arbitraire (`buildTierRadii`, `src/lib/melon/constants.ts`) : `citizens[tier]` (indexation directe, sans tirage) donne l'association palier → Citoyen, **fixe pour tout le monde** (pas juste pour une partie) puisque `corne/page.tsx` trie déjà les Citoyens par pseudo, un ordre déterministe. `TIER_OVERRIDES` (`src/lib/melon/constants.ts`, id de profil → index de palier) permet de fixer certains Citoyens à une taille précise à la main ; `applyTierOverrides()` les place d'abord, puis remplit le reste dans l'ordre alphabétique. Même mécanique de gloires que Le Vol d'Icare : record personnel all-time, scores figés à la date du Tribunal, gloires du top 3 attribuées automatiquement (`pg_cron`, sans intervention d'un Archonte) — n'importe qui (Archontes compris) peut jouer, seul le pool d'avatars affichés est limité aux Citoyens. Rendu en DOM (pas canvas) pour réutiliser tel quel le composant `Avatar` existant (`src/components/avatar.tsx`, étendu pour accepter une taille en pixels arbitraire en plus des tailles nommées) — la position **et la rotation** de chaque pièce sont appliquées en impératif (`style.transform`, `translate(...) rotate(body.angle)`) à 60 fps depuis la boucle physique matter-js, jamais via `setState`. Le halo de l'animation d'apparition doit porter `rounded-full` lui-même, sinon son ombre portée dessine un carré visible autour du rond ; l'apparition d'une pièce utilise un simple fondu (`.animate-fade-in`) plutôt que l'animation avec mise à l'échelle utilisée ailleurs dans l'app (`.animate-reveal`), pour que sa taille exacte soit lisible dès la première frame. Trait de visée vertical + preview flottante au-dessus du bac pour montrer où la prochaine pièce va tomber.
|
- **V11** : deuxième mini-jeu compétitif, **La Corne d'Abondance** (`/corne`) — jeu de fusion façon « Watermelon Game » : des avatars de Citoyens tombent dans un bac (physique gérée par `matter-js`, seule dépendance de ce type dans le projet — l'empilement stable de cercles est notoirement difficile à obtenir sans bugs de superposition/tremblement en physique artisanale), deux avatars de même taille qui se touchent fusionnent en un plus gros, jusqu'au débordement soutenu (5 secondes de délai de grâce, pour ne pas sanctionner un simple rebond) qui met fin à la partie — les pièces qui dépassent la ligne de danger clignotent (`.animate-danger-blink`) pour signaler que le compte à rebours est en cours. **Un palier de taille par Citoyen** (pas les Archontes) plutôt qu'un nombre de paliers arbitraire (`buildTierRadii`, `src/lib/melon/constants.ts`) : `citizens[tier]` (indexation directe, sans tirage) donne l'association palier → Citoyen, **fixe pour tout le monde** (pas juste pour une partie) puisque `corne/page.tsx` trie déjà les Citoyens par pseudo, un ordre déterministe. `TIER_OVERRIDES` (`src/lib/melon/constants.ts`, id de profil → index de palier) permet de fixer certains Citoyens à une taille précise à la main ; `applyTierOverrides()` les place d'abord, puis remplit le reste dans l'ordre alphabétique. Même mécanique de gloires que Le Vol d'Icare : record personnel all-time, scores figés à la date du Tribunal, gloires du top 3 attribuées automatiquement (`pg_cron`, sans intervention d'un Archonte) — n'importe qui (Archontes compris) peut jouer, seul le pool d'avatars affichés est limité aux Citoyens. Rendu en DOM (pas canvas) pour réutiliser tel quel le composant `Avatar` existant (`src/components/avatar.tsx`, étendu pour accepter une taille en pixels arbitraire en plus des tailles nommées) — la position **et la rotation** de chaque pièce sont appliquées en impératif (`style.transform`, `translate(...) rotate(body.angle)`) à 60 fps depuis la boucle physique matter-js, jamais via `setState`. Le halo de l'animation d'apparition doit porter `rounded-full` lui-même, sinon son ombre portée dessine un carré visible autour du rond ; l'apparition d'une pièce utilise un simple fondu (`.animate-fade-in`) plutôt que l'animation avec mise à l'échelle utilisée ailleurs dans l'app (`.animate-reveal`), pour que sa taille exacte soit lisible dès la première frame. Trait de visée vertical + preview flottante au-dessus du bac pour montrer où la prochaine pièce va tomber, plus un second aperçu, plus petit et fixe (pas de position à viser), pour voir venir deux coups à l'avance.
|
||||||
|
|
||||||
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, mode grand écran.
|
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, mode grand écran.
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export function MelonGame({ citizens, onFinish }: { citizens: MemberRow[]; onFin
|
|||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [pieces, setPieces] = useState<Piece[]>([]);
|
const [pieces, setPieces] = useState<Piece[]>([]);
|
||||||
const [nextTier, setNextTier] = useState(() => randomTier(DROP_MIN_TIER, dropMaxTier));
|
const [nextTier, setNextTier] = useState(() => randomTier(DROP_MIN_TIER, dropMaxTier));
|
||||||
|
// Palier annoncé après nextTier — juste informatif (pas de position à
|
||||||
|
// viser), pour voir venir deux coups à l'avance au lieu d'un seul.
|
||||||
|
const [secondTier, setSecondTier] = useState(() => randomTier(DROP_MIN_TIER, dropMaxTier));
|
||||||
|
|
||||||
const onFinishRef = useRef(onFinish);
|
const onFinishRef = useRef(onFinish);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,6 +62,7 @@ export function MelonGame({ citizens, onFinish }: { citizens: MemberRow[]; onFin
|
|||||||
const statusRef = useRef<Status>("playing");
|
const statusRef = useRef<Status>("playing");
|
||||||
const scoreRef = useRef(0);
|
const scoreRef = useRef(0);
|
||||||
const nextTierRef = useRef(nextTier);
|
const nextTierRef = useRef(nextTier);
|
||||||
|
const secondTierRef = useRef(secondTier);
|
||||||
const dropXRef = useRef(CONTAINER_WIDTH / 2);
|
const dropXRef = useRef(CONTAINER_WIDTH / 2);
|
||||||
const engineRef = useRef<Matter.Engine | null>(null);
|
const engineRef = useRef<Matter.Engine | null>(null);
|
||||||
const bodiesRef = useRef(new Map<number, Matter.Body>());
|
const bodiesRef = useRef(new Map<number, Matter.Body>());
|
||||||
@@ -121,8 +125,11 @@ export function MelonGame({ citizens, onFinish }: { citizens: MemberRow[]; onFin
|
|||||||
scoreRef.current = 0;
|
scoreRef.current = 0;
|
||||||
statusRef.current = "playing";
|
statusRef.current = "playing";
|
||||||
const firstNextTier = randomTier(DROP_MIN_TIER, dropMaxTier);
|
const firstNextTier = randomTier(DROP_MIN_TIER, dropMaxTier);
|
||||||
|
const firstSecondTier = randomTier(DROP_MIN_TIER, dropMaxTier);
|
||||||
nextTierRef.current = firstNextTier;
|
nextTierRef.current = firstNextTier;
|
||||||
|
secondTierRef.current = firstSecondTier;
|
||||||
setNextTier(firstNextTier);
|
setNextTier(firstNextTier);
|
||||||
|
setSecondTier(firstSecondTier);
|
||||||
setPieces([]);
|
setPieces([]);
|
||||||
setScore(0);
|
setScore(0);
|
||||||
setStatus("playing");
|
setStatus("playing");
|
||||||
@@ -146,9 +153,14 @@ export function MelonGame({ citizens, onFinish }: { citizens: MemberRow[]; onFin
|
|||||||
const dropped = spawnPiece(tier, x, radius + 6);
|
const dropped = spawnPiece(tier, x, radius + 6);
|
||||||
setPieces((current) => [...current, dropped]);
|
setPieces((current) => [...current, dropped]);
|
||||||
|
|
||||||
const upcoming = randomTier(DROP_MIN_TIER, dropMaxTier);
|
// La file glisse d'un cran : ce qui était annoncé en second devient la
|
||||||
|
// pièce à viser, et un nouveau palier est tiré pour la deuxième place.
|
||||||
|
const upcoming = secondTierRef.current;
|
||||||
|
const newSecond = randomTier(DROP_MIN_TIER, dropMaxTier);
|
||||||
nextTierRef.current = upcoming;
|
nextTierRef.current = upcoming;
|
||||||
|
secondTierRef.current = newSecond;
|
||||||
setNextTier(upcoming);
|
setNextTier(upcoming);
|
||||||
|
setSecondTier(newSecond);
|
||||||
setDropX(dropXRef.current);
|
setDropX(dropXRef.current);
|
||||||
}, [spawnPiece, setDropX, tierRadii, dropMaxTier]);
|
}, [spawnPiece, setDropX, tierRadii, dropMaxTier]);
|
||||||
|
|
||||||
@@ -343,7 +355,21 @@ export function MelonGame({ citizens, onFinish }: { citizens: MemberRow[]; onFin
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<p className="font-heading text-2xl text-gold-bright">{score}</p>
|
<div className="flex items-center justify-between" style={{ width: CONTAINER_WIDTH }}>
|
||||||
|
<p className="font-heading text-2xl text-gold-bright">{score}</p>
|
||||||
|
|
||||||
|
{/* Deuxième pièce annoncée (après celle à viser) : juste
|
||||||
|
informatif, taille d'affichage fixe quel que soit le vrai
|
||||||
|
rayon du palier — pas de position à viser pour celle-ci. */}
|
||||||
|
<div className="flex items-center gap-1.5 opacity-70">
|
||||||
|
<span className="font-heading text-[0.6rem] tracking-wide text-marble/50 uppercase">Puis</span>
|
||||||
|
<Avatar
|
||||||
|
pseudo={citizens[secondTier]?.pseudo ?? "?"}
|
||||||
|
avatarUrl={citizens[secondTier]?.avatar_url ?? null}
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user