"""Shared context blocks for RPG narrator / plot LLM calls.""" from services.rpg_plot import count_active_quests def format_narrator_context( arc: dict | None, quests: list | None, status_quo: str = "", ) -> str: parts: list[str] = [] arc = arc or {} beats = arc.get("beats") or [] if not isinstance(beats, list): beats = [] parts.append(f"Plot phase: {arc.get('phase', 'opening')}. Scripted beats left: {len(beats)}.") if not beats: parts.append( "IMPORTANT: Scripted beats are EXHAUSTED (quests may already be done). " "The story must CONTINUE — do not stall. " "Always return 2-4 meaningful choices for the player's next actions. " "You may add quest_updates with status 'active' for NEW optional threads. " "Do NOT re-activate quests the player already completed unless they explicitly revisit that thread." ) elif count_active_quests(quests) == 0: pending = [ (b.get("title") or b.get("id") or "beat") for b in beats[:3] if isinstance(b, dict) ] parts.append( "IMPORTANT: No active quests but scripted beats remain — arc was likely desynced. " "The engine will inject the next beat; prefer choices that fit pending beats: " + ", ".join(pending) + ". Do NOT treat the arc as finished." ) hint = (arc.get("next_beat_hint") or "").strip() if hint: parts.append(f"Arc hint: {hint}") if quests: parts.append("Quest log:") for q in quests: parts.append(f" [{q.get('status', 'active')}] {q.get('title', '')}") else: parts.append("Quest log: (empty)") sq = (status_quo or "").strip() if sq: parts.append(f"Status quo: {sq[:400]}") return "\n".join(parts)