Ajoute La Corne d'Abondance, jeu de fusion façon Watermelon Game
Build and deploy / deploy (push) Successful in 35s
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:
@@ -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 $$;
|
||||
|
||||
Reference in New Issue
Block a user