160 lines
6.2 KiB
PL/PgSQL
160 lines
6.2 KiB
PL/PgSQL
-- Make a newly launched campaign playable immediately: the owner receives a
|
|
-- human-controlled starter, and only actionable members hold the round open.
|
|
|
|
create or replace function public.stage_two_create_campaign(
|
|
p_world_id uuid,
|
|
p_owner_id uuid,
|
|
p_title text
|
|
) returns uuid language plpgsql security definer set search_path = '' as $$
|
|
declare
|
|
v_world public.worlds%rowtype;
|
|
v_campaign_id uuid;
|
|
v_owner_name text;
|
|
begin
|
|
if p_title is null or char_length(btrim(p_title)) not between 3 and 100 then
|
|
raise exception 'campaign title must be between 3 and 100 characters';
|
|
end if;
|
|
select * into v_world from public.worlds
|
|
where id = p_world_id and owner_id = p_owner_id and status = 'confirmed' for share;
|
|
if not found then raise exception 'confirmed world not found'; end if;
|
|
|
|
select nullif(btrim(display_name), '') into v_owner_name
|
|
from public.profiles where id = p_owner_id;
|
|
v_owner_name := left(coalesce(v_owner_name, 'Wayfinder'), 80);
|
|
|
|
insert into public.campaigns(world_id, owner_id, title, current_scene, next_prompt, status)
|
|
values (p_world_id, p_owner_id, btrim(p_title), coalesce(v_world.opening_scene, v_world.premise), 'What do you do?', 'active')
|
|
returning id into v_campaign_id;
|
|
insert into public.campaign_members(campaign_id, user_id, role)
|
|
values (v_campaign_id, p_owner_id, 'owner');
|
|
|
|
insert into public.characters(
|
|
campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
|
|
defense, proficiency, inventory, statuses, persona
|
|
) values (
|
|
v_campaign_id,
|
|
p_owner_id,
|
|
v_owner_name,
|
|
left('An adaptable protagonist ready to confront the opening mystery of ' || v_world.title || '.', 600),
|
|
'human'::public.character_controller,
|
|
'{"str":10,"dex":12,"con":12,"int":11,"wis":13,"cha":10}'::jsonb,
|
|
12,
|
|
12,
|
|
12,
|
|
2,
|
|
'["Field kit","Personal keepsake"]'::jsonb,
|
|
'[]'::jsonb,
|
|
jsonb_build_object(
|
|
'voice', 'Defined by the player.',
|
|
'motivation', 'Discover what the opening scene is hiding.',
|
|
'flaw', 'Still learning what this world demands.',
|
|
'bond', 'Protect the party through the first danger.'
|
|
)
|
|
);
|
|
|
|
insert into public.rounds(campaign_id, number) values (v_campaign_id, 1);
|
|
return v_campaign_id;
|
|
end;
|
|
$$;
|
|
|
|
-- Members who have not created a character cannot submit an intent and must not
|
|
-- hold the round open. Only active members with a human-controlled character
|
|
-- participate in the readiness barrier.
|
|
create or replace function public.stage_two_submit_intent(
|
|
p_round_id uuid,
|
|
p_user_id uuid,
|
|
p_character_id uuid,
|
|
p_action text,
|
|
p_ready boolean default false
|
|
) returns jsonb language plpgsql security definer set search_path = '' as $$
|
|
declare
|
|
v_round public.rounds%rowtype;
|
|
v_member public.campaign_members%rowtype;
|
|
v_intent_id uuid;
|
|
v_job_id uuid;
|
|
begin
|
|
if p_action is null or char_length(btrim(p_action)) not between 1 and 2000 then
|
|
raise exception 'action must be between 1 and 2000 characters';
|
|
end if;
|
|
select * into v_round from public.rounds where id = p_round_id for update;
|
|
if not found or v_round.status <> 'open' then raise exception 'round is not open'; end if;
|
|
select * into v_member from public.campaign_members
|
|
where campaign_id = v_round.campaign_id and user_id = p_user_id and active;
|
|
if not found then raise exception 'active campaign membership not found'; end if;
|
|
if not exists (
|
|
select 1 from public.characters
|
|
where id = p_character_id and campaign_id = v_round.campaign_id
|
|
and user_id = p_user_id and controller = 'human'
|
|
) then raise exception 'controlled character not found'; end if;
|
|
|
|
insert into public.player_intents(round_id, member_id, character_id, action, ready)
|
|
values (p_round_id, v_member.id, p_character_id, btrim(p_action), coalesce(p_ready, false))
|
|
on conflict (round_id, member_id) do update
|
|
set character_id = excluded.character_id, action = excluded.action,
|
|
ready = excluded.ready, updated_at = now()
|
|
returning id into v_intent_id;
|
|
|
|
if coalesce(p_ready, false) and not exists (
|
|
select 1
|
|
from public.campaign_members member
|
|
join public.characters character
|
|
on character.campaign_id = member.campaign_id
|
|
and character.user_id = member.user_id
|
|
and character.controller = 'human'::public.character_controller
|
|
where member.campaign_id = v_round.campaign_id
|
|
and member.active
|
|
and not exists (
|
|
select 1 from public.player_intents intent
|
|
where intent.round_id = p_round_id and intent.member_id = member.id and intent.ready
|
|
)
|
|
) then
|
|
v_job_id := public.enqueue_round_resolution(p_round_id, null);
|
|
end if;
|
|
return jsonb_build_object('intentId', v_intent_id, 'jobId', v_job_id);
|
|
end;
|
|
$$;
|
|
|
|
-- Repair campaigns created before starter parties were automatic.
|
|
insert into public.characters(
|
|
campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
|
|
defense, proficiency, inventory, statuses, persona
|
|
)
|
|
select
|
|
campaign.id,
|
|
campaign.owner_id,
|
|
left(coalesce(nullif(btrim(profile.display_name), ''), 'Wayfinder'), 80),
|
|
left('An adaptable protagonist ready to confront the opening mystery of ' || world.title || '.', 600),
|
|
'human'::public.character_controller,
|
|
'{"str":10,"dex":12,"con":12,"int":11,"wis":13,"cha":10}'::jsonb,
|
|
12,
|
|
12,
|
|
12,
|
|
2,
|
|
'["Field kit","Personal keepsake"]'::jsonb,
|
|
'[]'::jsonb,
|
|
jsonb_build_object(
|
|
'voice', 'Defined by the player.',
|
|
'motivation', 'Discover what the opening scene is hiding.',
|
|
'flaw', 'Still learning what this world demands.',
|
|
'bond', 'Protect the party through the first danger.'
|
|
)
|
|
from public.campaigns campaign
|
|
join public.worlds world on world.id = campaign.world_id
|
|
left join public.profiles profile on profile.id = campaign.owner_id
|
|
where campaign.status = 'active'
|
|
and not exists (
|
|
select 1 from public.characters character
|
|
where character.campaign_id = campaign.id
|
|
and character.user_id = campaign.owner_id
|
|
);
|
|
|
|
create or replace function public.dng_schema_version()
|
|
returns integer language sql stable security definer set search_path = '' as $$
|
|
select 10;
|
|
$$;
|
|
|
|
revoke all on function public.dng_schema_version() from public, anon, authenticated;
|
|
grant execute on function public.dng_schema_version() to service_role;
|
|
|
|
notify pgrst, 'reload schema';
|