Fixes batch 3: autosave, campaign-agnostic PCs, live push, builder

- Autosave: while signed in, a debounced full backup pushes whenever durable data
  changes (useCloudAutosave watches a fingerprint of the main tables; high-churn
  tables excluded). No-op signed out.
- Campaign-agnostic characters: PCs are now global — the Characters roster shows
  every player character regardless of active campaign, and any PC can be added to
  a fight. NPCs stay per-campaign. (charactersRepo.listAllPcs / useAllPcs; difficulty
  fallback stays campaign-scoped.)
- Live "bring your own character": a joining player can push one of their own local
  characters; the GM's grant inserts it into the campaign and seats them.
- Character builder: a system picker (5e / PF2e, defaults to the campaign) so creation
  no longer assumes 5e; PF2e skill wording ("Trained" vs "proficient"); spell step now
  explains how many to pick + that they're known spells prepared later.

230 unit + 34 e2e + 2 realtime green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 22:12:18 +02:00
parent 3f524ce82c
commit 235cdecb53
10 changed files with 184 additions and 49 deletions
+5 -2
View File
@@ -22,7 +22,7 @@ import { createRng } from '@/lib/rng';
import { rollDice } from '@/lib/dice/notation';
import { getSystem, getConditions, type SystemId } from '@/lib/rules';
import { computeBudget, DIFFICULTY_COLOR } from '@/lib/combat/budget';
import { useCharacters } from '@/features/characters/hooks';
import { useCharacters, useAllPcs } from '@/features/characters/hooks';
import { useConditionGlossary } from './useConditionGlossary';
import {
addCombatant,
@@ -48,6 +48,9 @@ import { EncounterTipCard } from '@/features/assistant/EncounterTipCard';
export function EncounterTracker({ encounter, campaign }: { encounter: Encounter; campaign: Campaign }) {
const characters = useCharacters(campaign.id);
// PCs are campaign-agnostic, so any PC can join a fight; NPCs stay campaign-scoped.
const allPcs = useAllPcs();
const addableChars = [...allPcs, ...characters.filter((c) => c.kind === 'npc')];
const glossary = useConditionGlossary(campaign.system);
const undoStack = useRef<Encounter[]>([]);
@@ -162,7 +165,7 @@ export function EncounterTracker({ encounter, campaign }: { encounter: Encounter
<EncounterTipCard campaign={campaign} encounter={encounter} />
<AddCombatantBar encounter={encounter} characters={characters} onAdd={(c) => mutate((e) => addCombatant(e, c))} />
<AddCombatantBar encounter={encounter} characters={addableChars} onAdd={(c) => mutate((e) => addCombatant(e, c))} />
{/* Combatant list */}
{encounter.combatants.length === 0 ? (