-- Make a newly launched campaign playable immediately: the owner receives a -- human-controlled starter and the party receives one persistent AI companion. 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.characters( campaign_id, user_id, name, concept, controller, abilities, hp, max_hp, defense, proficiency, inventory, statuses, persona ) values ( v_campaign_id, null, 'Echo', left('A persistent AI companion shaped by the ' || v_world.genre || ' world of ' || v_world.title || ', ready to support the player without taking over their choices.', 600), 'ai'::public.character_controller, '{"str":9,"dex":13,"con":11,"int":13,"wis":12,"cha":10}'::jsonb, 11, 11, 13, 2, '["Survey kit","Emergency supplies"]'::jsonb, '[]'::jsonb, jsonb_build_object( 'voice', 'Observant, concise, and quietly curious.', 'motivation', 'Help the party understand this unfamiliar world.', 'flaw', 'Sometimes values patterns more than instinct.', 'bond', 'Stays beside the player when the path turns dangerous.' ) ); 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 ); insert into public.characters( campaign_id, user_id, name, concept, controller, abilities, hp, max_hp, defense, proficiency, inventory, statuses, persona ) select campaign.id, null, 'Echo', left('A persistent AI companion shaped by the ' || world.genre || ' world of ' || world.title || ', ready to support the player without taking over their choices.', 600), 'ai'::public.character_controller, '{"str":9,"dex":13,"con":11,"int":13,"wis":12,"cha":10}'::jsonb, 11, 11, 13, 2, '["Survey kit","Emergency supplies"]'::jsonb, '[]'::jsonb, jsonb_build_object( 'voice', 'Observant, concise, and quietly curious.', 'motivation', 'Help the party understand this unfamiliar world.', 'flaw', 'Sometimes values patterns more than instinct.', 'bond', 'Stays beside the player when the path turns dangerous.' ) from public.campaigns campaign join public.worlds world on world.id = campaign.world_id where campaign.status = 'active' and not exists ( select 1 from public.characters character where character.campaign_id = campaign.id and character.controller = 'ai'::public.character_controller ); 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';