Ajoute La Corne d'Abondance, jeu de fusion façon Watermelon Game
Build and deploy / deploy (push) Successful in 35s

Nouveau mini-jeu à /corne : des avatars de Citoyens tombent dans un bac
(physique matter-js), deux avatars de même taille qui se touchent
fusionnent en un plus gros, jusqu'au débordement soutenu (5s de grâce,
pièces clignotantes pour prévenir) qui met fin à la partie. Même
mécanique de gloires que Le Vol d'Icare (record personnel, classement,
gel + attribution automatique au Tribunal via pg_cron).

Un palier de taille par Citoyen (pas les Archontes) plutôt qu'un nombre
arbitraire : citizens[tier] donne l'association palier → Citoyen, fixe
pour tout le monde (les Citoyens sont triés par pseudo côté serveur, un
ordre déterministe). Rendu en DOM pour réutiliser le composant Avatar
existant (étendu pour accepter une taille en pixels arbitraire) plutôt
que de redessiner des images sur canvas — position et rotation de chaque
pièce appliquées en impératif depuis la boucle physique, jamais via
setState.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-24 00:52:07 +02:00
parent c3a3577769
commit 3b64e1cdb5
13 changed files with 955 additions and 4 deletions
+146
View File
@@ -1419,3 +1419,149 @@ end;
$$;
grant execute on function public.cast_urn_vote(uuid, text) to authenticated;
-- 16. La Corne d'Abondance -------------------------------------------------------
-- Mini-jeu de fusion façon "Watermelon Game" : des cercles tombent dans un
-- bac, deux cercles de même palier qui se touchent fusionnent en un cercle
-- plus gros, jusqu'à débordement. Au lieu de fruits, chaque cercle affiche
-- l'avatar d'un membre tiré au hasard (décoratif, indépendant du palier).
-- 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).
create table if not exists public.melon_scores (
user_id uuid primary key references public.profiles (id) on delete cascade,
best_score integer not null check (best_score between 0 and 1000000),
updated_at timestamptz not null default now()
);
alter table public.melon_scores enable row level security;
-- Même modèle que icarus_scores : verrouillée en écriture, seule la RPC
-- submit_melon_score() (SECURITY DEFINER) peut écrire.
revoke insert, update, delete on public.melon_scores from authenticated, anon;
grant select on public.melon_scores to authenticated;
drop policy if exists "melon_scores readable by authenticated" on public.melon_scores;
create policy "melon_scores readable by authenticated"
on public.melon_scores for select
to authenticated
using (true);
-- Marqueur d'idempotence pour l'attribution automatique des gloires (un seul
-- événement, comme icarus_points_awarded).
alter table public.settings add column if not exists melon_points_awarded boolean not null default false;
-- Seul point d'entrée pour soumettre un score. Une fois la date du Tribunal
-- atteinte, les scores sont figés : la RPC ne fait plus rien (retourne le
-- record existant sans le modifier) plutôt que d'échouer bruyamment.
create or replace function public.submit_melon_score(p_score integer)
returns public.melon_scores
language plpgsql
security definer
set search_path = public
as $$
declare
v_tribunal_date timestamptz;
v_row public.melon_scores;
begin
if auth.uid() is null then
raise exception 'authentication required';
end if;
if p_score is null or p_score < 0 or p_score > 1000000 then
raise exception 'invalid score';
end if;
select tribunal_date into v_tribunal_date from public.settings where id = true;
if v_tribunal_date is not null and now() >= v_tribunal_date then
select * into v_row from public.melon_scores where user_id = auth.uid();
return v_row;
end if;
insert into public.melon_scores (user_id, best_score, updated_at)
values (auth.uid(), p_score, now())
on conflict (user_id) do update
set best_score = excluded.best_score,
updated_at = now()
where excluded.best_score > public.melon_scores.best_score;
select * into v_row from public.melon_scores where user_id = auth.uid();
return v_row;
end;
$$;
grant execute on function public.submit_melon_score(integer) to authenticated;
do $$
begin
alter publication supabase_realtime add table public.melon_scores;
exception
when duplicate_object then null;
end $$;
-- Attribue les gloires du top 3 (ex-aequo inclus au même rang) dès que la
-- date du Tribunal est atteinte ; no-op tant qu'elle n'est pas encore
-- passée, et no-op définitif une fois déjà fait (melon_points_awarded).
-- Volontairement pas de grant execute à authenticated : uniquement appelée
-- par pg_cron ou depuis le SQL Editor.
create or replace function public.award_melon_points_if_due()
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
v_tribunal_date timestamptz;
v_already_awarded boolean;
r record;
v_points int;
begin
select tribunal_date, melon_points_awarded into v_tribunal_date, v_already_awarded
from public.settings where id = true;
if v_tribunal_date is null or now() < v_tribunal_date or v_already_awarded then
return;
end if;
for r in
with ranked as (
select user_id, best_score,
rank() over (order by best_score desc) as rnk
from public.melon_scores
)
select * from ranked where rnk <= 3
loop
v_points := case r.rnk when 1 then 3 when 2 then 2 when 3 then 1 else 0 end;
update public.profiles set points = points + v_points where id = r.user_id;
insert into public.points_log (target_id, judge_id, delta, reason)
values (r.user_id, null, v_points, 'La Corne d''Abondance — rang ' || r.rnk || ' au Tribunal');
end loop;
update public.settings set melon_points_awarded = true where id = true;
end;
$$;
-- ⚠️ pg_cron doit être activé une fois pour toutes via le Dashboard Supabase
-- (Database → Extensions → "pg_cron" → Enable) — voir la même remarque à la
-- section Icare. Les deux blocs ci-dessous n'échouent jamais bruyamment si
-- l'extension n'est pas encore activée.
do $$
begin
perform cron.unschedule('award-melon-points');
exception
when others then null; -- la tâche n'existe pas encore, ou pg_cron pas activé
end $$;
do $$
begin
perform cron.schedule(
'award-melon-points',
'*/15 * * * *',
$cron$select public.award_melon_points_if_due();$cron$
);
exception
when others then
raise notice 'pg_cron indisponible : active l''extension via le Dashboard Supabase (Database → Extensions → pg_cron), puis ré-exécute ce script pour planifier l''attribution automatique des gloires de La Corne d''Abondance.';
end $$;