Add jsinspect-plus structural duplication gate, extract shared helpers
Add jsinspect-plus (AST-based structural duplication detector) to pnpm check with threshold 50 / min 3 instances. Fix all findings: - Extract condition icon/color maps to shared condition-styles.ts - Extract useClickOutside hook (5 components) - Extract dispatchAction + resolveAndRename in use-encounter - Extract runEncounterAction in application layer (13 use cases) - Extract findCombatant helper in domain (9 functions) - Extract TraitSection in stat-block (4 trait rendering blocks) - Extract DialogHeader in dialog.tsx (4 dialogs) Net result: -263 lines across 40 files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import type {
|
||||
CombatantInit,
|
||||
ConditionId,
|
||||
CreatureId,
|
||||
DomainError,
|
||||
DomainEvent,
|
||||
Encounter,
|
||||
PlayerCharacter,
|
||||
@@ -120,167 +121,90 @@ export function useEncounter() {
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const advanceTurn = useCallback(() => {
|
||||
const result = withUndo(() => advanceTurnUseCase(makeStore()));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
}, [makeStore, withUndo]);
|
||||
|
||||
const retreatTurn = useCallback(() => {
|
||||
const result = withUndo(() => retreatTurnUseCase(makeStore()));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
}, [makeStore, withUndo]);
|
||||
const dispatchAction = useCallback(
|
||||
(action: () => DomainEvent[] | DomainError) => {
|
||||
const result = withUndo(action);
|
||||
if (!isDomainError(result)) {
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
}
|
||||
},
|
||||
[withUndo],
|
||||
);
|
||||
|
||||
const nextId = useRef(deriveNextId(encounter));
|
||||
|
||||
const advanceTurn = useCallback(
|
||||
() => dispatchAction(() => advanceTurnUseCase(makeStore())),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const retreatTurn = useCallback(
|
||||
() => dispatchAction(() => retreatTurnUseCase(makeStore())),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const addCombatant = useCallback(
|
||||
(name: string, init?: CombatantInit) => {
|
||||
const id = combatantId(`c-${++nextId.current}`);
|
||||
const result = withUndo(() =>
|
||||
addCombatantUseCase(makeStore(), id, name, init),
|
||||
);
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
dispatchAction(() => addCombatantUseCase(makeStore(), id, name, init));
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const removeCombatant = useCallback(
|
||||
(id: CombatantId) => {
|
||||
const result = withUndo(() => removeCombatantUseCase(makeStore(), id));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId) =>
|
||||
dispatchAction(() => removeCombatantUseCase(makeStore(), id)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const editCombatant = useCallback(
|
||||
(id: CombatantId, newName: string) => {
|
||||
const result = withUndo(() =>
|
||||
editCombatantUseCase(makeStore(), id, newName),
|
||||
);
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, newName: string) =>
|
||||
dispatchAction(() => editCombatantUseCase(makeStore(), id, newName)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const setInitiative = useCallback(
|
||||
(id: CombatantId, value: number | undefined) => {
|
||||
const result = withUndo(() =>
|
||||
setInitiativeUseCase(makeStore(), id, value),
|
||||
);
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, value: number | undefined) =>
|
||||
dispatchAction(() => setInitiativeUseCase(makeStore(), id, value)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const setHp = useCallback(
|
||||
(id: CombatantId, maxHp: number | undefined) => {
|
||||
const result = withUndo(() => setHpUseCase(makeStore(), id, maxHp));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, maxHp: number | undefined) =>
|
||||
dispatchAction(() => setHpUseCase(makeStore(), id, maxHp)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const adjustHp = useCallback(
|
||||
(id: CombatantId, delta: number) => {
|
||||
const result = withUndo(() => adjustHpUseCase(makeStore(), id, delta));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, delta: number) =>
|
||||
dispatchAction(() => adjustHpUseCase(makeStore(), id, delta)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const setTempHp = useCallback(
|
||||
(id: CombatantId, tempHp: number | undefined) => {
|
||||
const result = withUndo(() => setTempHpUseCase(makeStore(), id, tempHp));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, tempHp: number | undefined) =>
|
||||
dispatchAction(() => setTempHpUseCase(makeStore(), id, tempHp)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const setAc = useCallback(
|
||||
(id: CombatantId, value: number | undefined) => {
|
||||
const result = withUndo(() => setAcUseCase(makeStore(), id, value));
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId, value: number | undefined) =>
|
||||
dispatchAction(() => setAcUseCase(makeStore(), id, value)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const toggleCondition = useCallback(
|
||||
(id: CombatantId, conditionId: ConditionId) => {
|
||||
const result = withUndo(() =>
|
||||
(id: CombatantId, conditionId: ConditionId) =>
|
||||
dispatchAction(() =>
|
||||
toggleConditionUseCase(makeStore(), id, conditionId),
|
||||
);
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const toggleConcentration = useCallback(
|
||||
(id: CombatantId) => {
|
||||
const result = withUndo(() =>
|
||||
toggleConcentrationUseCase(makeStore(), id),
|
||||
);
|
||||
|
||||
if (isDomainError(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore, withUndo],
|
||||
(id: CombatantId) =>
|
||||
dispatchAction(() => toggleConcentrationUseCase(makeStore(), id)),
|
||||
[makeStore, dispatchAction],
|
||||
);
|
||||
|
||||
const clearEncounter = useCallback(() => {
|
||||
@@ -298,16 +222,11 @@ export function useEncounter() {
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
}, [makeStore]);
|
||||
|
||||
const addOneFromBestiary = useCallback(
|
||||
(
|
||||
entry: BestiaryIndexEntry,
|
||||
): { cId: CreatureId; events: DomainEvent[] } | null => {
|
||||
const resolveAndRename = useCallback(
|
||||
(name: string): string => {
|
||||
const store = makeStore();
|
||||
const existingNames = store.get().combatants.map((c) => c.name);
|
||||
const { newName, renames } = resolveCreatureName(
|
||||
entry.name,
|
||||
existingNames,
|
||||
);
|
||||
const { newName, renames } = resolveCreatureName(name, existingNames);
|
||||
|
||||
for (const { from, to } of renames) {
|
||||
const target = store.get().combatants.find((c) => c.name === from);
|
||||
@@ -316,6 +235,17 @@ export function useEncounter() {
|
||||
}
|
||||
}
|
||||
|
||||
return newName;
|
||||
},
|
||||
[makeStore],
|
||||
);
|
||||
|
||||
const addOneFromBestiary = useCallback(
|
||||
(
|
||||
entry: BestiaryIndexEntry,
|
||||
): { cId: CreatureId; events: DomainEvent[] } | null => {
|
||||
const newName = resolveAndRename(entry.name);
|
||||
|
||||
const slug = entry.name
|
||||
.toLowerCase()
|
||||
.replaceAll(/[^a-z0-9]+/g, "-")
|
||||
@@ -333,7 +263,7 @@ export function useEncounter() {
|
||||
|
||||
return { cId, events: result };
|
||||
},
|
||||
[makeStore],
|
||||
[makeStore, resolveAndRename],
|
||||
);
|
||||
|
||||
const addFromBestiary = useCallback(
|
||||
@@ -385,16 +315,7 @@ export function useEncounter() {
|
||||
const addFromPlayerCharacter = useCallback(
|
||||
(pc: PlayerCharacter) => {
|
||||
const snapshot = encounterRef.current;
|
||||
const store = makeStore();
|
||||
const existingNames = store.get().combatants.map((c) => c.name);
|
||||
const { newName, renames } = resolveCreatureName(pc.name, existingNames);
|
||||
|
||||
for (const { from, to } of renames) {
|
||||
const target = store.get().combatants.find((c) => c.name === from);
|
||||
if (target) {
|
||||
editCombatantUseCase(makeStore(), target.id, to);
|
||||
}
|
||||
}
|
||||
const newName = resolveAndRename(pc.name);
|
||||
|
||||
const id = combatantId(`c-${++nextId.current}`);
|
||||
const result = addCombatantUseCase(makeStore(), id, newName, {
|
||||
@@ -406,7 +327,7 @@ export function useEncounter() {
|
||||
});
|
||||
|
||||
if (isDomainError(result)) {
|
||||
store.save(snapshot);
|
||||
makeStore().save(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -416,7 +337,7 @@ export function useEncounter() {
|
||||
|
||||
setEvents((prev) => [...prev, ...result]);
|
||||
},
|
||||
[makeStore],
|
||||
[makeStore, resolveAndRename],
|
||||
);
|
||||
|
||||
const undoAction = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user