fix(gameplay): start campaigns in playable state
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -2324,6 +2324,226 @@ grant execute on function public.dng_schema_version() to service_role;
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
|
||||
-- ============================================================================
|
||||
-- 0010_playable_starter_parties.sql
|
||||
-- ============================================================================
|
||||
|
||||
-- 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';
|
||||
|
||||
|
||||
do $$
|
||||
begin
|
||||
@@ -2348,7 +2568,7 @@ begin
|
||||
if to_regprocedure('public.stage_four_create_character(uuid,uuid,text,text,text,jsonb,integer,integer,integer,integer,jsonb,jsonb,jsonb)') is null then
|
||||
raise exception 'D&G bootstrap verification failed: character creation RPC is missing';
|
||||
end if;
|
||||
if public.dng_schema_version() <> 9 then
|
||||
if public.dng_schema_version() <> 10 then
|
||||
raise exception 'D&G bootstrap verification failed: unexpected schema version';
|
||||
end if;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user