3f524ce82c
- 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>
93 lines
4.2 KiB
TypeScript
93 lines
4.2 KiB
TypeScript
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 1–3 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 3–5 steps.`;
|
||
const user = `Character: ${charLine(c)}. Leveling up to ${nextLevel}. Chosen route: "${chosenRoute}". How should they achieve it?`;
|
||
return { system, user };
|
||
}
|