Overhaul bottom bar: batch add, custom fields, stat block viewer
Unify the action bar into a single search input with inline bestiary dropdown. Clicking a dropdown entry queues it with +/- count controls and a confirm button; Enter or confirm adds N copies to combat. When no bestiary match exists, optional Init/AC/MaxHP fields appear for custom creatures. The eye icon opens a separate search dropdown to preview stat blocks without leaving the add flow. Fix batch-add bug where only the last creature got a creatureId by using store.save() instead of setEncounter() in addFromBestiary. Prevent dropdown buttons from stealing input focus so Enter confirms the queued batch. Remove the now-redundant BestiarySearch component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,33 @@ function deriveNextId(encounter: Encounter): number {
|
||||
return max;
|
||||
}
|
||||
|
||||
interface CombatantOpts {
|
||||
initiative?: number;
|
||||
ac?: number;
|
||||
maxHp?: number;
|
||||
}
|
||||
|
||||
function applyCombatantOpts(
|
||||
makeStore: () => EncounterStore,
|
||||
id: ReturnType<typeof combatantId>,
|
||||
opts: CombatantOpts,
|
||||
): DomainEvent[] {
|
||||
const events: DomainEvent[] = [];
|
||||
if (opts.maxHp !== undefined) {
|
||||
const r = setHpUseCase(makeStore(), id, opts.maxHp);
|
||||
if (!isDomainError(r)) events.push(...r);
|
||||
}
|
||||
if (opts.ac !== undefined) {
|
||||
const r = setAcUseCase(makeStore(), id, opts.ac);
|
||||
if (!isDomainError(r)) events.push(...r);
|
||||
}
|
||||
if (opts.initiative !== undefined) {
|
||||
const r = setInitiativeUseCase(makeStore(), id, opts.initiative);
|
||||
if (!isDomainError(r)) events.push(...r);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
export function useEncounter() {
|
||||
const [encounter, setEncounter] = useState<Encounter>(initializeEncounter);
|
||||
const [events, setEvents] = useState<DomainEvent[]>([]);
|
||||
@@ -108,7 +135,7 @@ export function useEncounter() {
|
||||
const nextId = useRef(deriveNextId(encounter));
|
||||
|
||||
const addCombatant = useCallback(
|
||||
(name: string) => {
|
||||
(name: string, opts?: CombatantOpts) => {
|
||||
const id = combatantId(`c-${++nextId.current}`);
|
||||
const result = addCombatantUseCase(makeStore(), id, name);
|
||||
|
||||
@@ -116,6 +143,13 @@ export function useEncounter() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts) {
|
||||
const optEvents = applyCombatantOpts(makeStore, id, opts);
|
||||
if (optEvents.length > 0) {
|
||||
setEvents((prev) => [...prev, ...optEvents]);
|
||||
}
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore],
|
||||
@@ -279,15 +313,14 @@ export function useEncounter() {
|
||||
.replace(/(^-|-$)/g, "");
|
||||
const cId = makeCreatureId(`${entry.source.toLowerCase()}:${slug}`);
|
||||
|
||||
// Set creatureId on the combatant
|
||||
// Set creatureId on the combatant (use store.save to keep ref in sync for batch calls)
|
||||
const currentEncounter = store.get();
|
||||
const updated = {
|
||||
store.save({
|
||||
...currentEncounter,
|
||||
combatants: currentEncounter.combatants.map((c) =>
|
||||
c.id === id ? { ...c, creatureId: cId } : c,
|
||||
),
|
||||
};
|
||||
setEncounter(updated);
|
||||
});
|
||||
|
||||
setEvents((prev) => [...prev, ...addResult]);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user