Add Pathfinder 2e game system mode
Implements PF2e as an alternative game system alongside D&D 5e/5.5e. Settings modal "Game System" selector switches conditions, bestiary, stat block layout, and initiative calculation between systems. - Valued conditions with increment/decrement UX (Clumsy 2, Frightened 3) - 2,502 PF2e creatures from bundled search index (77 sources) - PF2e stat block: level, traits, Perception, Fort/Ref/Will, ability mods - Perception-based initiative rolling - System-scoped source cache (D&D and PF2e sources don't collide) - Backwards-compatible condition rehydration (ConditionId[] → ConditionEntry[]) - Difficulty indicator hidden in PF2e mode (excluded from MVP) Closes dostulata/initiative#19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -292,9 +292,9 @@ describe("toggleConditionUseCase", () => {
|
||||
);
|
||||
|
||||
expect(isDomainError(result)).toBe(false);
|
||||
expect(requireSaved(store.saved).combatants[0].conditions).toContain(
|
||||
"blinded",
|
||||
);
|
||||
expect(requireSaved(store.saved).combatants[0].conditions).toContainEqual({
|
||||
id: "blinded",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns domain error for unknown combatant", () => {
|
||||
|
||||
21
packages/application/src/creature-initiative-modifier.ts
Normal file
21
packages/application/src/creature-initiative-modifier.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
type AnyCreature,
|
||||
calculateInitiative,
|
||||
calculatePf2eInitiative,
|
||||
} from "@initiative/domain";
|
||||
|
||||
export function creatureInitiativeModifier(creature: AnyCreature): number {
|
||||
if ("system" in creature && creature.system === "pf2e") {
|
||||
return calculatePf2eInitiative(creature.perception).modifier;
|
||||
}
|
||||
const c = creature as {
|
||||
abilities: { dex: number };
|
||||
cr: string;
|
||||
initiativeProficiency: number;
|
||||
};
|
||||
return calculateInitiative({
|
||||
dexScore: c.abilities.dex,
|
||||
cr: c.cr,
|
||||
initiativeProficiency: c.initiativeProficiency,
|
||||
}).modifier;
|
||||
}
|
||||
19
packages/application/src/decrement-condition-use-case.ts
Normal file
19
packages/application/src/decrement-condition-use-case.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
type CombatantId,
|
||||
type ConditionId,
|
||||
type DomainError,
|
||||
type DomainEvent,
|
||||
decrementCondition,
|
||||
} from "@initiative/domain";
|
||||
import type { EncounterStore } from "./ports.js";
|
||||
import { runEncounterAction } from "./run-encounter-action.js";
|
||||
|
||||
export function decrementConditionUseCase(
|
||||
store: EncounterStore,
|
||||
combatantId: CombatantId,
|
||||
conditionId: ConditionId,
|
||||
): DomainEvent[] | DomainError {
|
||||
return runEncounterAction(store, (encounter) =>
|
||||
decrementCondition(encounter, combatantId, conditionId),
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export { adjustHpUseCase } from "./adjust-hp-use-case.js";
|
||||
export { advanceTurnUseCase } from "./advance-turn-use-case.js";
|
||||
export { clearEncounterUseCase } from "./clear-encounter-use-case.js";
|
||||
export { createPlayerCharacterUseCase } from "./create-player-character-use-case.js";
|
||||
export { decrementConditionUseCase } from "./decrement-condition-use-case.js";
|
||||
export { deletePlayerCharacterUseCase } from "./delete-player-character-use-case.js";
|
||||
export { editCombatantUseCase } from "./edit-combatant-use-case.js";
|
||||
export { editPlayerCharacterUseCase } from "./edit-player-character-use-case.js";
|
||||
@@ -21,6 +22,7 @@ export {
|
||||
} from "./roll-all-initiative-use-case.js";
|
||||
export { rollInitiativeUseCase } from "./roll-initiative-use-case.js";
|
||||
export { setAcUseCase } from "./set-ac-use-case.js";
|
||||
export { setConditionValueUseCase } from "./set-condition-value-use-case.js";
|
||||
export { setCrUseCase } from "./set-cr-use-case.js";
|
||||
export { setHpUseCase } from "./set-hp-use-case.js";
|
||||
export { setInitiativeUseCase } from "./set-initiative-use-case.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
Creature,
|
||||
AnyCreature,
|
||||
CreatureId,
|
||||
Encounter,
|
||||
PlayerCharacter,
|
||||
@@ -12,7 +12,7 @@ export interface EncounterStore {
|
||||
}
|
||||
|
||||
export interface BestiarySourceCache {
|
||||
getCreature(creatureId: CreatureId): Creature | undefined;
|
||||
getCreature(creatureId: CreatureId): AnyCreature | undefined;
|
||||
isSourceCached(sourceCode: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
type Creature,
|
||||
type AnyCreature,
|
||||
type CreatureId,
|
||||
calculateInitiative,
|
||||
type DomainError,
|
||||
type DomainEvent,
|
||||
isDomainError,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
selectRoll,
|
||||
setInitiative,
|
||||
} from "@initiative/domain";
|
||||
import { creatureInitiativeModifier } from "./creature-initiative-modifier.js";
|
||||
import type { EncounterStore } from "./ports.js";
|
||||
|
||||
export interface RollAllResult {
|
||||
@@ -20,7 +20,7 @@ export interface RollAllResult {
|
||||
export function rollAllInitiativeUseCase(
|
||||
store: EncounterStore,
|
||||
rollDice: () => number,
|
||||
getCreature: (id: CreatureId) => Creature | undefined,
|
||||
getCreature: (id: CreatureId) => AnyCreature | undefined,
|
||||
mode: RollMode = "normal",
|
||||
): RollAllResult | DomainError {
|
||||
let encounter = store.get();
|
||||
@@ -37,11 +37,7 @@ export function rollAllInitiativeUseCase(
|
||||
continue;
|
||||
}
|
||||
|
||||
const { modifier } = calculateInitiative({
|
||||
dexScore: creature.abilities.dex,
|
||||
cr: creature.cr,
|
||||
initiativeProficiency: creature.initiativeProficiency,
|
||||
});
|
||||
const modifier = creatureInitiativeModifier(creature);
|
||||
const roll1 = rollDice();
|
||||
const effectiveRoll =
|
||||
mode === "normal" ? roll1 : selectRoll(roll1, rollDice(), mode);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
type AnyCreature,
|
||||
type CombatantId,
|
||||
type Creature,
|
||||
type CreatureId,
|
||||
calculateInitiative,
|
||||
type DomainError,
|
||||
type DomainEvent,
|
||||
isDomainError,
|
||||
@@ -11,13 +10,14 @@ import {
|
||||
selectRoll,
|
||||
setInitiative,
|
||||
} from "@initiative/domain";
|
||||
import { creatureInitiativeModifier } from "./creature-initiative-modifier.js";
|
||||
import type { EncounterStore } from "./ports.js";
|
||||
|
||||
export function rollInitiativeUseCase(
|
||||
store: EncounterStore,
|
||||
combatantId: CombatantId,
|
||||
diceRolls: readonly [number, ...number[]],
|
||||
getCreature: (id: CreatureId) => Creature | undefined,
|
||||
getCreature: (id: CreatureId) => AnyCreature | undefined,
|
||||
mode: RollMode = "normal",
|
||||
): DomainEvent[] | DomainError {
|
||||
const encounter = store.get();
|
||||
@@ -48,11 +48,7 @@ export function rollInitiativeUseCase(
|
||||
};
|
||||
}
|
||||
|
||||
const { modifier } = calculateInitiative({
|
||||
dexScore: creature.abilities.dex,
|
||||
cr: creature.cr,
|
||||
initiativeProficiency: creature.initiativeProficiency,
|
||||
});
|
||||
const modifier = creatureInitiativeModifier(creature);
|
||||
const effectiveRoll =
|
||||
mode === "normal"
|
||||
? diceRolls[0]
|
||||
|
||||
20
packages/application/src/set-condition-value-use-case.ts
Normal file
20
packages/application/src/set-condition-value-use-case.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
type CombatantId,
|
||||
type ConditionId,
|
||||
type DomainError,
|
||||
type DomainEvent,
|
||||
setConditionValue,
|
||||
} from "@initiative/domain";
|
||||
import type { EncounterStore } from "./ports.js";
|
||||
import { runEncounterAction } from "./run-encounter-action.js";
|
||||
|
||||
export function setConditionValueUseCase(
|
||||
store: EncounterStore,
|
||||
combatantId: CombatantId,
|
||||
conditionId: ConditionId,
|
||||
value: number,
|
||||
): DomainEvent[] | DomainError {
|
||||
return runEncounterAction(store, (encounter) =>
|
||||
setConditionValue(encounter, combatantId, conditionId, value),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user