diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f08446e..c94768d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,7 +2,12 @@ import { rollAllInitiativeUseCase, rollInitiativeUseCase, } from "@initiative/application"; -import type { CombatantId, Creature, CreatureId } from "@initiative/domain"; +import { + type CombatantId, + type Creature, + type CreatureId, + isDomainError, +} from "@initiative/domain"; import { useCallback, useEffect, @@ -109,6 +114,8 @@ export function App() { const bulkImport = useBulkImport(); + const [rollSkippedCount, setRollSkippedCount] = useState(0); + const [selectedCreatureId, setSelectedCreatureId] = useState(null); const [bulkImportMode, setBulkImportMode] = useState(false); @@ -158,7 +165,10 @@ export function App() { ); const handleRollAllInitiative = useCallback(() => { - rollAllInitiativeUseCase(makeStore(), rollDice, getCreature); + const result = rollAllInitiativeUseCase(makeStore(), rollDice, getCreature); + if (!isDomainError(result) && result.skippedNoSource > 0) { + setRollSkippedCount(result.skippedNoSource); + } }, [makeStore, getCreature]); const handleViewStatBlock = useCallback((result: SearchResult) => { @@ -252,7 +262,10 @@ export function App() { }, [encounter.activeIndex, encounter.combatants, isLoaded]); const isEmpty = encounter.combatants.length === 0; - const showRollAllInitiative = encounter.combatants.some( + const hasCreatureCombatants = encounter.combatants.some( + (c) => c.creatureId != null, + ); + const canRollAllInitiative = encounter.combatants.some( (c) => c.creatureId != null && c.initiative == null, ); @@ -293,7 +306,8 @@ export function App() { onAddFromPlayerCharacter={addFromPlayerCharacter} onManagePlayers={() => setManagementOpen(true)} onRollAllInitiative={handleRollAllInitiative} - showRollAllInitiative={showRollAllInitiative} + showRollAllInitiative={hasCreatureCombatants} + rollAllInitiativeDisabled={!canRollAllInitiative} onOpenSourceManager={handleOpenSourceManager} autoFocus /> @@ -349,7 +363,8 @@ export function App() { onAddFromPlayerCharacter={addFromPlayerCharacter} onManagePlayers={() => setManagementOpen(true)} onRollAllInitiative={handleRollAllInitiative} - showRollAllInitiative={showRollAllInitiative} + showRollAllInitiative={hasCreatureCombatants} + rollAllInitiativeDisabled={!canRollAllInitiative} onOpenSourceManager={handleOpenSourceManager} /> @@ -427,6 +442,14 @@ export function App() { /> )} + {rollSkippedCount > 0 && ( + setRollSkippedCount(0)} + autoDismissMs={4000} + /> + )} + { diff --git a/apps/web/src/components/action-bar.tsx b/apps/web/src/components/action-bar.tsx index c5773a9..41c9416 100644 --- a/apps/web/src/components/action-bar.tsx +++ b/apps/web/src/components/action-bar.tsx @@ -40,6 +40,7 @@ interface ActionBarProps { onManagePlayers?: () => void; onRollAllInitiative?: () => void; showRollAllInitiative?: boolean; + rollAllInitiativeDisabled?: boolean; onOpenSourceManager?: () => void; autoFocus?: boolean; } @@ -262,6 +263,7 @@ export function ActionBar({ onManagePlayers, onRollAllInitiative, showRollAllInitiative, + rollAllInitiativeDisabled, onOpenSourceManager, autoFocus, }: ActionBarProps) { @@ -575,6 +577,7 @@ export function ActionBar({ variant="ghost" className="text-muted-foreground hover:text-hover-action" onClick={onRollAllInitiative} + disabled={rollAllInitiativeDisabled} title="Roll all initiative" aria-label="Roll all initiative" > diff --git a/apps/web/src/components/toast.tsx b/apps/web/src/components/toast.tsx index dd248fd..39b4985 100644 --- a/apps/web/src/components/toast.tsx +++ b/apps/web/src/components/toast.tsx @@ -23,7 +23,7 @@ export function Toast({ }, [autoDismissMs, onDismiss]); return createPortal( -
+
{message} {progress !== undefined && ( diff --git a/packages/application/src/index.ts b/packages/application/src/index.ts index 895dfef..77ae634 100644 --- a/packages/application/src/index.ts +++ b/packages/application/src/index.ts @@ -13,7 +13,10 @@ export type { } from "./ports.js"; export { removeCombatantUseCase } from "./remove-combatant-use-case.js"; export { retreatTurnUseCase } from "./retreat-turn-use-case.js"; -export { rollAllInitiativeUseCase } from "./roll-all-initiative-use-case.js"; +export { + type RollAllResult, + rollAllInitiativeUseCase, +} from "./roll-all-initiative-use-case.js"; export { rollInitiativeUseCase } from "./roll-initiative-use-case.js"; export { setAcUseCase } from "./set-ac-use-case.js"; export { setHpUseCase } from "./set-hp-use-case.js"; diff --git a/packages/application/src/roll-all-initiative-use-case.ts b/packages/application/src/roll-all-initiative-use-case.ts index a0c9e3a..b3fd646 100644 --- a/packages/application/src/roll-all-initiative-use-case.ts +++ b/packages/application/src/roll-all-initiative-use-case.ts @@ -10,20 +10,29 @@ import { } from "@initiative/domain"; import type { EncounterStore } from "./ports.js"; +export interface RollAllResult { + events: DomainEvent[]; + skippedNoSource: number; +} + export function rollAllInitiativeUseCase( store: EncounterStore, rollDice: () => number, getCreature: (id: CreatureId) => Creature | undefined, -): DomainEvent[] | DomainError { +): RollAllResult | DomainError { let encounter = store.get(); const allEvents: DomainEvent[] = []; + let skippedNoSource = 0; for (const combatant of encounter.combatants) { if (!combatant.creatureId) continue; if (combatant.initiative !== undefined) continue; const creature = getCreature(combatant.creatureId); - if (!creature) continue; + if (!creature) { + skippedNoSource++; + continue; + } const { modifier } = calculateInitiative({ dexScore: creature.abilities.dex, @@ -47,5 +56,5 @@ export function rollAllInitiativeUseCase( } store.save(encounter); - return allEvents; + return { events: allEvents, skippedNoSource }; }