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:
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface AdjustHpSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -17,17 +23,9 @@ export function adjustHp(
|
||||
combatantId: CombatantId,
|
||||
delta: number,
|
||||
): AdjustHpSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
const { combatant: target } = found;
|
||||
|
||||
if (target.maxHp === undefined || target.currentHp === undefined) {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface EditCombatantSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -30,17 +36,9 @@ export function editCombatant(
|
||||
};
|
||||
}
|
||||
|
||||
const index = encounter.combatants.findIndex((c) => c.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${id}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const oldName = encounter.combatants[index].name;
|
||||
const found = findCombatant(encounter, id);
|
||||
if (isDomainError(found)) return found;
|
||||
const oldName = found.combatant.name;
|
||||
|
||||
return {
|
||||
encounter: {
|
||||
|
||||
@@ -126,6 +126,7 @@ export {
|
||||
createEncounter,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
export {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface RemoveCombatantSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -22,17 +28,10 @@ export function removeCombatant(
|
||||
encounter: Encounter,
|
||||
id: CombatantId,
|
||||
): RemoveCombatantSuccess | DomainError {
|
||||
const removedIdx = encounter.combatants.findIndex((c) => c.id === id);
|
||||
const found = findCombatant(encounter, id);
|
||||
if (isDomainError(found)) return found;
|
||||
|
||||
if (removedIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${id}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const removed = encounter.combatants[removedIdx];
|
||||
const { index: removedIdx, combatant: removed } = found;
|
||||
const newCombatants = encounter.combatants.filter((_, i) => i !== removedIdx);
|
||||
|
||||
let newActiveIndex: number;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface SetAcSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -11,15 +17,8 @@ export function setAc(
|
||||
combatantId: CombatantId,
|
||||
value: number | undefined,
|
||||
): SetAcSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
|
||||
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
||||
return {
|
||||
@@ -29,8 +28,7 @@ export function setAc(
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const previousAc = target.ac;
|
||||
const previousAc = found.combatant.ac;
|
||||
|
||||
const updatedCombatants = encounter.combatants.map((c) =>
|
||||
c.id === combatantId ? { ...c, ac: value } : c,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface SetHpSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -18,15 +24,8 @@ export function setHp(
|
||||
combatantId: CombatantId,
|
||||
maxHp: number | undefined,
|
||||
): SetHpSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
|
||||
if (maxHp !== undefined && (!Number.isInteger(maxHp) || maxHp < 1)) {
|
||||
return {
|
||||
@@ -36,9 +35,8 @@ export function setHp(
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const previousMaxHp = target.maxHp;
|
||||
const previousCurrentHp = target.currentHp;
|
||||
const previousMaxHp = found.combatant.maxHp;
|
||||
const previousCurrentHp = found.combatant.currentHp;
|
||||
|
||||
let newMaxHp: number | undefined;
|
||||
let newCurrentHp: number | undefined;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import { sortByInitiative } from "./initiative-sort.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface SetInitiativeSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -24,15 +30,8 @@ export function setInitiative(
|
||||
combatantId: CombatantId,
|
||||
value: number | undefined,
|
||||
): SetInitiativeSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
|
||||
if (value !== undefined && !Number.isInteger(value)) {
|
||||
return {
|
||||
@@ -42,8 +41,7 @@ export function setInitiative(
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const previousValue = target.initiative;
|
||||
const previousValue = found.combatant.initiative;
|
||||
|
||||
// Create new combatants array with updated initiative
|
||||
const updated = encounter.combatants.map((c) =>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface SetTempHpSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -18,17 +24,9 @@ export function setTempHp(
|
||||
combatantId: CombatantId,
|
||||
tempHp: number | undefined,
|
||||
): SetTempHpSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
const { combatant: target } = found;
|
||||
|
||||
if (target.maxHp === undefined || target.currentHp === undefined) {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface ToggleConcentrationSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -10,17 +16,9 @@ export function toggleConcentration(
|
||||
encounter: Encounter,
|
||||
combatantId: CombatantId,
|
||||
): ToggleConcentrationSuccess | DomainError {
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
const { combatant: target } = found;
|
||||
const wasConcentrating = target.isConcentrating === true;
|
||||
|
||||
const event: DomainEvent = wasConcentrating
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { ConditionId } from "./conditions.js";
|
||||
import { CONDITION_DEFINITIONS, VALID_CONDITION_IDS } from "./conditions.js";
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
import {
|
||||
type CombatantId,
|
||||
type DomainError,
|
||||
type Encounter,
|
||||
findCombatant,
|
||||
isDomainError,
|
||||
} from "./types.js";
|
||||
|
||||
export interface ToggleConditionSuccess {
|
||||
readonly encounter: Encounter;
|
||||
@@ -21,17 +27,9 @@ export function toggleCondition(
|
||||
};
|
||||
}
|
||||
|
||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "combatant-not-found",
|
||||
message: `No combatant found with ID "${combatantId}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const target = encounter.combatants[targetIdx];
|
||||
const found = findCombatant(encounter, combatantId);
|
||||
if (isDomainError(found)) return found;
|
||||
const { combatant: target } = found;
|
||||
const current = target.conditions ?? [];
|
||||
const isActive = current.includes(conditionId);
|
||||
|
||||
|
||||
@@ -70,6 +70,20 @@ export function createEncounter(
|
||||
return { combatants, activeIndex, roundNumber };
|
||||
}
|
||||
|
||||
export function findCombatant(
|
||||
encounter: Encounter,
|
||||
id: CombatantId,
|
||||
): { index: number; combatant: Combatant } | DomainError {
|
||||
const index = encounter.combatants.findIndex((c) => c.id === id);
|
||||
if (index === -1) {
|
||||
return domainError(
|
||||
"combatant-not-found",
|
||||
`No combatant found with ID "${id}"`,
|
||||
);
|
||||
}
|
||||
return { index, combatant: encounter.combatants[index] };
|
||||
}
|
||||
|
||||
export function isDomainError(value: unknown): value is DomainError {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
|
||||
Reference in New Issue
Block a user