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>
This commit is contained in:
@@ -53,11 +53,20 @@ export function EncounterTipCard({ campaign, encounter }: { campaign: Campaign;
|
||||
<p className="mb-2 text-sm text-ink">{suggestion.reasoning}</p>
|
||||
<ul className="mb-2 text-sm text-ink">
|
||||
{suggestion.add.map((a) => (
|
||||
<li key={a.name}>• {a.count}× {a.name}</li>
|
||||
<li key={`add-${a.name}`}>• Add {a.count}× {a.name}</li>
|
||||
))}
|
||||
{(suggestion.remove ?? []).map((r) => (
|
||||
<li key={`remove-${r.name}`}>• Remove {r.count}× {r.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="primary" onClick={apply}>Add to encounter</Button>
|
||||
{(suggestion.add.length > 0 || (suggestion.remove?.length ?? 0) > 0) && (
|
||||
<Button size="sm" variant="primary" onClick={apply}>
|
||||
{suggestion.add.length === 0 && (suggestion.remove?.length ?? 0) > 0
|
||||
? 'Apply to encounter'
|
||||
: 'Add to encounter'}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="ghost" onClick={run}>Regenerate</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { encountersRepo } from '@/lib/db/repositories';
|
||||
import { newId } from '@/lib/ids';
|
||||
import { createRng } from '@/lib/rng';
|
||||
import { rollDice } from '@/lib/dice/notation';
|
||||
import { addCombatant } from '@/lib/combat/engine';
|
||||
import { addCombatant, removeCombatant } from '@/lib/combat/engine';
|
||||
import { computeBudget } from '@/lib/combat/budget';
|
||||
import { loadMonsters, loadPf2e } from '@/lib/compendium';
|
||||
import { complete } from '@/lib/llm/client';
|
||||
@@ -162,6 +162,16 @@ export function useEncounterAdvisor(campaign: Campaign, encounter: Encounter) {
|
||||
if (!suggestion) return;
|
||||
await encountersRepo.mutate(encounter.id, (e) => {
|
||||
let next = e;
|
||||
// Over-tuned fights: take out the suggested monsters (newest copies first).
|
||||
for (const r of suggestion.remove ?? []) {
|
||||
for (let i = 0; i < r.count; i++) {
|
||||
const victim = [...next.combatants].reverse().find(
|
||||
(c) => c.kind === 'monster' && baseName(c.name) === r.name,
|
||||
);
|
||||
if (!victim) break;
|
||||
next = removeCombatant(next, victim.id);
|
||||
}
|
||||
}
|
||||
for (const a of suggestion.add) {
|
||||
// Prefer cloning a creature already in the fight (handles "add another goblin"
|
||||
// and works even for custom/homebrew combatants); otherwise pull from the pool.
|
||||
|
||||
Reference in New Issue
Block a user