Files
ttrpg_manager/src/lib/assistant/prompts.ts
T
NilsBriggen 3f524ce82c Fixes batch 2: player quests, AI parse, advisor, iPad share
- Player view: a Quest log now syncs to players (snapshot += quests; broadcaster
  passes campaign quests; PlayerBoards renders titles + objective checkboxes).
- AI (DeepSeek etc.): robust JSON extraction — strip code fences anywhere + a
  brace-balanced scanner that ignores prose/braces in strings. Fixes "AI unavailable
  (parse)" with OpenAI-compatible providers. +4 tests.
- Encounter advisor: now bidirectional — for an over-tuned (too-hard) fight it
  suggests REMOVING/swapping monsters instead of only ever adding. +3 tests.
- Character share: works on iPad — native share sheet → clipboard → a copyable
  link modal fallback (was a silently-failing clipboard write).

230 unit + 34 e2e + 2 realtime green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:56:10 +02:00

93 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from 'zod';
import type { Character } from '@/lib/schemas';
import type { CampaignContext, CreatureCandidate } from './context';
export const balanceSuggestionSchema = z.object({
reasoning: z.string(),
add: z.array(z.object({
name: z.string(),
count: z.number().int().min(1).max(8),
ref: z.string().optional(),
})),
/** Monsters to take out when the fight is over-tuned (already at/above target). */
remove: z.array(z.object({
name: z.string(),
count: z.number().int().min(1).max(8),
})).optional(),
targetDifficulty: z.string(),
});
export type BalanceSuggestion = z.infer<typeof balanceSuggestionSchema>;
function partyLine(ctx: CampaignContext): string {
return ctx.party.map((p) => `${p.name} (level ${p.level} ${p.className})`).join(', ') || 'no PCs recorded';
}
/**
* Prompt for rebalancing the current encounter. The system message leads with the
* grounding constraint and restricts choices to the provided candidate list.
*/
export function buildBalancePrompt(
ctx: CampaignContext,
current: { difficulty: string; targetDifficulty: string; candidates: CreatureCandidate[] },
): { system: string; user: string } {
const system =
`${ctx.systemConstraint}\n\n` +
`You are an encounter-balancing assistant for a ${ctx.systemLabel} game master. ` +
`Recommend which creatures to ADD to the current encounter to bring it to roughly "${current.targetDifficulty}" difficulty. ` +
`You MUST choose only from the provided candidate creatures (by exact name). Do not invent creatures or use any from another system. ` +
`Prefer 13 distinct creatures with sensible counts. Keep the reasoning to one or two sentences.`;
const candidateList = current.candidates
.map((c) => `- ${c.name} (rating ${c.rating}${c.hp ? `, hp ${c.hp}` : ''}${c.ac ? `, ac ${c.ac}` : ''})`)
.join('\n');
const user =
`Party: ${partyLine(ctx)}.\n` +
`Current encounter difficulty: ${current.difficulty}.\n` +
`Target difficulty: ${current.targetDifficulty}.\n\n` +
`Candidate creatures (choose only from these):\n${candidateList}\n\n` +
`Respond with the creatures to add (name + count), a short reasoning, and the targetDifficulty.`;
return { system, user };
}
// ---------------- Level-up advisor ----------------
export const buildRoutesSchema = z.object({
routes: z.array(z.object({
title: z.string(),
summary: z.string(),
playstyle: z.string(),
})).min(3).max(4),
});
export type BuildRoute = z.infer<typeof buildRoutesSchema>['routes'][number];
export const buildStepsSchema = z.object({
steps: z.array(z.object({ label: z.string(), detail: z.string() })).min(1),
});
export type BuildStep = z.infer<typeof buildStepsSchema>['steps'][number];
function charLine(c: Character): string {
const abilities = Object.entries(c.abilities).map(([k, v]) => `${k.toUpperCase()} ${v}`).join(', ');
return `${c.name}, a level ${c.level} ${c.className || 'adventurer'} (${abilities})`;
}
export function buildLevelUpRoutesPrompt(ctx: CampaignContext, c: Character, nextLevel: number): { system: string; user: string } {
const system =
`${ctx.systemConstraint}\n\n` +
`You are a character-building advisor for ${ctx.systemLabel}. Propose 4 distinct build routes for the next level. ` +
`Each route needs a short title, a one-sentence summary, and a one-phrase playstyle. ` +
`Use only ${ctx.systemLabel} mechanics, feats, and class options — never another system's.`;
const user = `Character: ${charLine(c)}. Leveling up to ${nextLevel}. Suggest 4 build routes.`;
return { system, user };
}
export function buildLevelUpStepsPrompt(ctx: CampaignContext, c: Character, nextLevel: number, chosenRoute: string): { system: string; user: string } {
const system =
`${ctx.systemConstraint}\n\n` +
`You are a character-building advisor for ${ctx.systemLabel}. Give concrete, ordered steps to pursue the chosen build route at this level. ` +
`Each step has a short label and a one-sentence detail. Reference only ${ctx.systemLabel} options (feats, ability boosts, spells, skill increases as appropriate). Keep to 35 steps.`;
const user = `Character: ${charLine(c)}. Leveling up to ${nextLevel}. Chosen route: "${chosenRoute}". How should they achieve it?`;
return { system, user };
}