Add profile photo uploads and custom difficulty support
This commit is contained in:
361
supabase/migrations/20261013095201_custom_difficulty_presets.sql
Normal file
361
supabase/migrations/20261013095201_custom_difficulty_presets.sql
Normal file
@@ -0,0 +1,361 @@
|
||||
-- Store shared custom difficulty presets and snapshot their rules in each game.
|
||||
-- Writes stay behind the MineGeo server API; the exposed public schema grants
|
||||
-- no table access to anon or authenticated clients.
|
||||
|
||||
create table public.difficulty_presets (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null check (
|
||||
name = btrim(name)
|
||||
and length(name) between 2 and 40
|
||||
and name !~ '[[:cntrl:]]'
|
||||
and name !~ U&'[\\202A-\\202E\\2066-\\2069]'
|
||||
),
|
||||
seconds integer not null check (
|
||||
seconds between 15 and 600 and seconds % 15 = 0
|
||||
),
|
||||
zoom boolean not null,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create unique index difficulty_presets_name
|
||||
on public.difficulty_presets (lower(name));
|
||||
alter table public.difficulty_presets enable row level security;
|
||||
revoke all on public.difficulty_presets from public, anon, authenticated;
|
||||
grant select, insert on public.difficulty_presets to service_role;
|
||||
|
||||
alter table public.game_sessions
|
||||
drop constraint if exists game_sessions_difficulty_check;
|
||||
alter table public.game_sessions
|
||||
add column difficulty_settings jsonb;
|
||||
update public.game_sessions
|
||||
set difficulty_settings = jsonb_build_object(
|
||||
'id', difficulty,
|
||||
'name', initcap(difficulty),
|
||||
'seconds', case difficulty when 'easy' then 180 when 'hard' then 30 else 90 end,
|
||||
'zoom', difficulty <> 'hard'
|
||||
);
|
||||
alter table public.game_sessions
|
||||
alter column difficulty_settings set not null;
|
||||
alter table public.game_sessions
|
||||
add constraint game_sessions_difficulty_check check (
|
||||
difficulty in ('easy', 'normal', 'hard')
|
||||
or difficulty ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
);
|
||||
|
||||
alter table public.multiplayer_rooms
|
||||
drop constraint if exists multiplayer_rooms_difficulty_check;
|
||||
alter table public.multiplayer_rooms
|
||||
add column difficulty_settings jsonb;
|
||||
update public.multiplayer_rooms
|
||||
set difficulty_settings = jsonb_build_object(
|
||||
'id', difficulty,
|
||||
'name', initcap(difficulty),
|
||||
'seconds', case difficulty when 'easy' then 180 when 'hard' then 30 else 90 end,
|
||||
'zoom', difficulty <> 'hard'
|
||||
);
|
||||
alter table public.multiplayer_rooms
|
||||
alter column difficulty_settings set not null;
|
||||
alter table public.multiplayer_rooms
|
||||
add constraint multiplayer_rooms_difficulty_check check (
|
||||
difficulty in ('easy', 'normal', 'hard')
|
||||
or difficulty ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
);
|
||||
|
||||
create or replace function public.resolve_difficulty_preset(p_id text)
|
||||
returns jsonb language plpgsql stable security invoker set search_path = '' as $$
|
||||
declare preset jsonb;
|
||||
begin
|
||||
case p_id
|
||||
when 'easy' then
|
||||
return jsonb_build_object('id', 'easy', 'name', 'Easy', 'seconds', 180, 'zoom', true);
|
||||
when 'normal' then
|
||||
return jsonb_build_object('id', 'normal', 'name', 'Normal', 'seconds', 90, 'zoom', true);
|
||||
when 'hard' then
|
||||
return jsonb_build_object('id', 'hard', 'name', 'Hard', 'seconds', 30, 'zoom', false);
|
||||
end case;
|
||||
|
||||
select jsonb_build_object(
|
||||
'id', p.id::text,
|
||||
'name', p.name,
|
||||
'seconds', p.seconds,
|
||||
'zoom', p.zoom
|
||||
)
|
||||
into preset
|
||||
from public.difficulty_presets p
|
||||
where p.id::text = lower(p_id);
|
||||
if not found then raise exception 'Choose a saved difficulty'; end if;
|
||||
return preset;
|
||||
end;
|
||||
$$;
|
||||
revoke all on function public.resolve_difficulty_preset(text)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.resolve_difficulty_preset(text) to service_role;
|
||||
|
||||
create or replace function public.create_game(
|
||||
p_dataset uuid,
|
||||
p_difficulty text,
|
||||
p_owner uuid,
|
||||
p_access_hash text
|
||||
) returns uuid language plpgsql security invoker set search_path = '' as $$
|
||||
declare
|
||||
sid uuid;
|
||||
revision uuid;
|
||||
rev public.revisions;
|
||||
vp jsonb;
|
||||
n integer := 0;
|
||||
settings jsonb;
|
||||
begin
|
||||
settings := public.resolve_difficulty_preset(p_difficulty);
|
||||
revision := public.playable_map_revision(p_dataset, p_owner);
|
||||
select * into rev from public.revisions where id = revision;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if jsonb_array_length(rev.manifest->'viewpoints') < 5 then
|
||||
raise exception 'Map has fewer than five viewpoints';
|
||||
end if;
|
||||
insert into public.game_sessions(
|
||||
revision_id, owner_id, access_hash, difficulty, difficulty_settings
|
||||
) values (rev.id, p_owner, p_access_hash, p_difficulty, settings)
|
||||
returning id into sid;
|
||||
for vp in
|
||||
select value from jsonb_array_elements(rev.manifest->'viewpoints')
|
||||
order by random() limit 5
|
||||
loop
|
||||
insert into public.game_rounds(session_id, round_number, viewpoint)
|
||||
values (sid, n, vp);
|
||||
n := n + 1;
|
||||
end loop;
|
||||
return sid;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.activate_round(
|
||||
p_session uuid,
|
||||
p_round integer,
|
||||
p_access_hash text
|
||||
) returns timestamptz language plpgsql security invoker set search_path = '' as $$
|
||||
declare
|
||||
s public.game_sessions;
|
||||
r public.game_rounds;
|
||||
seconds integer;
|
||||
begin
|
||||
select * into s from public.game_sessions
|
||||
where id = p_session and access_hash = p_access_hash for update;
|
||||
if not found then raise exception 'Session not found'; end if;
|
||||
if s.current_round <> p_round then raise exception 'Round is not current'; end if;
|
||||
select * into r from public.game_rounds
|
||||
where session_id = p_session and round_number = p_round for update;
|
||||
if r.result is not null then raise exception 'Round already finished'; end if;
|
||||
if r.deadline is not null then return r.deadline; end if;
|
||||
seconds := (s.difficulty_settings->>'seconds')::integer;
|
||||
update public.game_rounds
|
||||
set deadline = clock_timestamp() + make_interval(secs => seconds)
|
||||
where session_id = p_session and round_number = p_round
|
||||
returning deadline into r.deadline;
|
||||
return r.deadline;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.create_map_room(
|
||||
p_dataset uuid,
|
||||
p_difficulty text,
|
||||
p_name text,
|
||||
p_limit integer,
|
||||
p_access_hash text,
|
||||
p_owner uuid
|
||||
) returns uuid language plpgsql security invoker set search_path = '' as $$
|
||||
declare
|
||||
rid uuid;
|
||||
revision uuid;
|
||||
pid uuid := gen_random_uuid();
|
||||
rev public.revisions;
|
||||
vp jsonb;
|
||||
n integer := 0;
|
||||
settings jsonb;
|
||||
begin
|
||||
if p_limit is null or p_limit not between 2 and 16 then
|
||||
raise exception 'Choose between 2 and 16 players';
|
||||
end if;
|
||||
settings := public.resolve_difficulty_preset(p_difficulty);
|
||||
revision := public.playable_map_revision(p_dataset, p_owner);
|
||||
select * into rev from public.revisions where id = revision;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if coalesce(jsonb_array_length(rev.manifest->'viewpoints'), 0) < 5 then
|
||||
raise exception 'Map has fewer than five viewpoints';
|
||||
end if;
|
||||
insert into public.multiplayer_rooms(
|
||||
revision_id, host_player_id, difficulty, difficulty_settings, player_limit
|
||||
) values (rev.id, pid, p_difficulty, settings, p_limit)
|
||||
returning id into rid;
|
||||
insert into public.multiplayer_players(
|
||||
id, room_id, access_hash, display_name
|
||||
) values (pid, rid, p_access_hash, btrim(p_name));
|
||||
for vp in
|
||||
select value from jsonb_array_elements(rev.manifest->'viewpoints')
|
||||
order by random() limit 5
|
||||
loop
|
||||
insert into public.multiplayer_rounds(room_id, round_number, viewpoint)
|
||||
values (rid, n, vp);
|
||||
n := n + 1;
|
||||
end loop;
|
||||
return rid;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.create_multiplayer_room(
|
||||
p_dataset uuid,
|
||||
p_difficulty text,
|
||||
p_name text,
|
||||
p_limit integer,
|
||||
p_access_hash text
|
||||
) returns uuid language sql security invoker set search_path = '' as $$
|
||||
select public.create_map_room(
|
||||
p_dataset, p_difficulty, p_name, p_limit, p_access_hash, null
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function public.multiplayer_tick(p_room uuid)
|
||||
returns void language plpgsql security invoker set search_path = '' as $$
|
||||
declare
|
||||
room public.multiplayer_rooms;
|
||||
turn public.multiplayer_rounds;
|
||||
seconds integer;
|
||||
begin
|
||||
select * into room from public.multiplayer_rooms where id = p_room for update;
|
||||
if room.status not in ('finished', 'closed')
|
||||
and room.created_at < clock_timestamp() - interval '24 hours' then
|
||||
update public.multiplayer_rooms
|
||||
set status = 'closed', transition_at = null where id = p_room;
|
||||
return;
|
||||
end if;
|
||||
if room.status = 'results' and room.transition_at <= clock_timestamp() then
|
||||
update public.multiplayer_rooms
|
||||
set current_round = current_round + 1,
|
||||
status = 'loading',
|
||||
transition_at = clock_timestamp() + interval '30 seconds'
|
||||
where id = p_room returning * into room;
|
||||
end if;
|
||||
if room.status = 'loading' and (
|
||||
room.transition_at <= clock_timestamp()
|
||||
or not exists (
|
||||
select 1 from public.multiplayer_players
|
||||
where room_id = p_room and not has_left
|
||||
and ready_round <> room.current_round
|
||||
)
|
||||
) then
|
||||
seconds := (room.difficulty_settings->>'seconds')::integer;
|
||||
update public.multiplayer_rounds
|
||||
set deadline = clock_timestamp() + make_interval(secs => seconds)
|
||||
where room_id = p_room and round_number = room.current_round;
|
||||
update public.multiplayer_rooms
|
||||
set status = 'playing', transition_at = null
|
||||
where id = p_room returning * into room;
|
||||
end if;
|
||||
if room.status <> 'playing' then return; end if;
|
||||
select * into turn from public.multiplayer_rounds
|
||||
where room_id = p_room and round_number = room.current_round;
|
||||
insert into public.multiplayer_guesses(room_id, player_id, round_number, result)
|
||||
select p_room, p.id, room.current_round,
|
||||
jsonb_build_object(
|
||||
'score', 0, 'distance', null, 'timedOut', true, 'guess', null,
|
||||
'actual', jsonb_build_object(
|
||||
'x', turn.viewpoint->'x', 'z', turn.viewpoint->'z'
|
||||
)
|
||||
)
|
||||
from public.multiplayer_players p
|
||||
where p.room_id = p_room
|
||||
and (p.has_left or turn.deadline <= clock_timestamp())
|
||||
on conflict (room_id, player_id, round_number) do nothing;
|
||||
if not exists (
|
||||
select 1 from public.multiplayer_players p
|
||||
where p.room_id = p_room and not exists (
|
||||
select 1 from public.multiplayer_guesses g
|
||||
where g.room_id = p_room and g.player_id = p.id
|
||||
and g.round_number = room.current_round
|
||||
)
|
||||
) then
|
||||
update public.multiplayer_rounds set closed_at = clock_timestamp()
|
||||
where room_id = p_room and round_number = room.current_round;
|
||||
update public.multiplayer_rooms
|
||||
set status = case when current_round = 4 then 'finished' else 'results' end,
|
||||
transition_at = case when current_round = 4 then null
|
||||
else clock_timestamp() + interval '20 seconds' end
|
||||
where id = p_room;
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.multiplayer_snapshot(
|
||||
p_room uuid,
|
||||
p_access_hash text
|
||||
) returns jsonb language sql security invoker set search_path = '' as $$
|
||||
with room as (select * from public.multiplayer_rooms where id = p_room),
|
||||
viewer as (
|
||||
select p.id from public.multiplayer_players p
|
||||
where p.room_id = p_room and p.access_hash = p_access_hash and not p.has_left
|
||||
),
|
||||
totals as (
|
||||
select p.*,
|
||||
coalesce((
|
||||
select sum((g.result->>'score')::integer)
|
||||
from public.multiplayer_guesses g
|
||||
join public.multiplayer_rounds r
|
||||
on r.room_id = g.room_id and r.round_number = g.round_number
|
||||
where g.room_id = p_room and g.player_id = p.id and r.closed_at is not null
|
||||
), 0) as total
|
||||
from public.multiplayer_players p where p.room_id = p_room
|
||||
), ranked as (
|
||||
select *, dense_rank() over (order by total desc) as rank from totals
|
||||
)
|
||||
select jsonb_build_object(
|
||||
'id', room.id,
|
||||
'datasetId', rev.dataset_id,
|
||||
'name', rev.manifest->>'name',
|
||||
'difficulty', room.difficulty,
|
||||
'difficultySettings', room.difficulty_settings,
|
||||
'playerLimit', room.player_limit,
|
||||
'playerCount', (select count(*) from totals where not has_left),
|
||||
'status', room.status,
|
||||
'round', room.current_round,
|
||||
'hostId', room.host_player_id,
|
||||
'you', (select id from viewer),
|
||||
'serverNow', floor(extract(epoch from clock_timestamp()) * 1000),
|
||||
'transitionAt', room.transition_at,
|
||||
'players', case when exists(select 1 from viewer) then coalesce((
|
||||
select jsonb_agg(jsonb_build_object(
|
||||
'id', p.id,
|
||||
'name', p.display_name,
|
||||
'left', p.has_left,
|
||||
'ready', p.ready_round = room.current_round,
|
||||
'submitted', exists(
|
||||
select 1 from public.multiplayer_guesses g
|
||||
where g.room_id = p_room and g.player_id = p.id
|
||||
and g.round_number = room.current_round
|
||||
),
|
||||
'total', p.total,
|
||||
'rank', p.rank
|
||||
) order by p.rank, p.joined_at, p.id)
|
||||
from ranked p
|
||||
), '[]'::jsonb) else '[]'::jsonb end,
|
||||
'rounds', case when exists(select 1 from viewer) then coalesce((
|
||||
select jsonb_agg(jsonb_build_object(
|
||||
'number', r.round_number,
|
||||
'deadline', r.deadline,
|
||||
'closedAt', r.closed_at,
|
||||
'results', case when r.closed_at is not null then coalesce((
|
||||
select jsonb_agg(
|
||||
g.result || jsonb_build_object('playerId', g.player_id)
|
||||
order by g.player_id
|
||||
)
|
||||
from public.multiplayer_guesses g
|
||||
where g.room_id = p_room and g.round_number = r.round_number
|
||||
), '[]'::jsonb) else '[]'::jsonb end
|
||||
) order by r.round_number)
|
||||
from public.multiplayer_rounds r where r.room_id = p_room
|
||||
), '[]'::jsonb) else '[]'::jsonb end,
|
||||
'myGuess', case when exists(select 1 from viewer) then (
|
||||
select g.result->'guess' from public.multiplayer_guesses g
|
||||
where g.room_id = p_room and g.player_id = (select id from viewer)
|
||||
and g.round_number = room.current_round
|
||||
) else null end
|
||||
)
|
||||
from room join public.revisions rev on rev.id = room.revision_id;
|
||||
$$;
|
||||
Reference in New Issue
Block a user