AbstractProcedure
AbstractProcedure
is the abstract base class for all procedures in SR3E.
It provides the scaffolding needed to formalize advanced and opposed rolls:
how they are set up, how modifiers are applied, how dice are rolled, and how
outcomes are published.
Concrete subclasses (e.g. FirearmProcedure, MeleeProcedure, SpellcastingProcedure) inherit from this base and override specific hooks.
Responsibilities
- Registration & serialization
- Each subclass registers itself under a
kind
string. - Procedures can be serialized to JSON and deserialized back, including actor/item references and internal state.
- Each subclass registers itself under a
- State management
- Writable Svelte stores for title, target number, dice, pool dice, karma dice.
- Derived stores for total modifiers, final target number, difficulty label.
- Tracks whether the roll is defaulting, what attribute it links to, and contest identifiers for opposed rolls.
- Locking
- Integrates with
ProcedureLock
to ensure only one procedure flow per actor at a time.
- Integrates with
- Contest support
- Knows whether it is opposed (
hasTargets
). - Maintains contest identifiers and can deliver contest responses through
OpposeRollService
. - Can build a matching defense procedure on the target side.
- Knows whether it is opposed (
- Hooks for subclasses
execute()
— must be overridden; drives the actual roll.onChallengeWillRoll()
— optional pre-roll hook (attach metadata, adjust dice).onChallengeResolved()
— optional post-roll hook.buildResistancePrep()
— subclasses can return structured data for follow-up resistance rolls.getResponderPromptHTML()
— customize defender prompts in opposed rolls.
State model
Store | Type | Purpose / Notes |
---|---|---|
targetNumberStore | writable<number | null> | Base TN before modifiers; finalTNStore clamps to ≥ 2. |
modifiersArrayStore | writable<Array<{id?, name, value, poolCap?, forbidPool?, meta?}>> | Source of TN adjustments and pool caps/forbid flags. |
modifiersTotalStore | derived<number> | Sum of modifier values. |
finalTNStore | derived<number | null> | max(2, target + sum(mods)) ; null if no base. |
difficultyStore | derived<string> | Localized label from TN (Simple/Routine/Hard/etc.). |
titleStore | writable<string> | Panel/roll title; defaults to item name if present. |
diceStore | writable<number> | Base dice (skill/attr/spec/default). |
poolDiceStore | writable<number> | User-selected pool dice; clamped by availability and poolCap/forbidPool . |
karmaDiceStore | writable<number> | Bonus dice from Karma. |
linkedAttributeStore | writable<string | null> | For defaulting and labeling (e.g., Reaction). |
hasTargetsStore | readable<boolean> | Live "do I have targets selected?" signal. |
Helper: getTotalDiceBreakdown()
returns { baseDice, poolDice, karmaDice, totalDice }
.
Roll lifecycle
execute({ OnClose?, CommitEffects? })
→ OnClose?.() // close composer UI
→ baseRoll = SR3ERoll.create(buildFormula(true), { actor })
→ onChallengeWillRoll({ baseRoll, actor }) // attach options: dice, pools, TN, basis
→ roll = await baseRoll.evaluate(this)
→ await baseRoll.waitForResolution()
→ CommitEffects?.() // apply ammo/recoil/etc. in subclasses
→ deliverContestResponse(roll) // if contestId is set
→ onChallengeResolved({ roll, actor })
→ return roll
API reference
Construction
ProcedureFactory.Create(kind, { actor, item?, args? }) → AbstractProcedure | null
: Create the correct subclass instance; used by the Composer, not directly.AbstractProcedure.registerSubclass(kind, Ctor)
: Register a subclass so it can be deserialized and used in contests.AbstractProcedure.getCtor(kind)
: Look up a registered subclass constructor.AbstractProcedure.listKinds()
: List all currently registered kinds.
Targeting & contests
hasTargets: boolean
: Snapshot — whether the user currently has targets selected.hasTargetsStore: Readable<boolean>
: Reactive store that updates on targeting changes.contestId: string | null
: Current contest identifier, if attached.setContestId(id)
: Bind this procedure to a contest.setContestIds(ids)
: Replace the local list of contest ids (initiator side).appendContestId(id)
: Add another contest id.clearContests()
: Clear all locally tracked contest ids.deliverContestResponse(rollOrJson)
: Send roll results back into the contest service.
Core stores & numbers
targetNumberStore: Writable<number | null>
: Base target number before modifiers.modifiersArrayStore: Writable<Array>
: Collection of TN mods and pool restrictions.finalTNStore: Derived<number | null>
: Base + modifiers, clamped at minimum 2.difficultyStore: Derived<string>
: Localized label for the TN (Simple, Hard, etc.).dice: number
/diceStore: Writable<number>
: Base dice (skill, attribute, specialization).poolDice: number
/poolDiceStore: Writable<number>
: User-selected pool dice (clamped).karmaDice: number
/karmaDiceStore: Writable<number>
: Bonus dice from Karma.linkedAttribute: string | null
: Governing attribute, if any.isPrimaryActionEnabled()
: Checks if the roll button should be active (TN ≥ 2).finalTN({ floor? })
: Compute the effective TN with optional floor.getTotalDiceBreakdown()
: Return{ baseDice, poolDice, karmaDice, totalDice }
.
Modifiers & pools
upsertMod({ id?, name, value, poolCap?, forbidPool? })
: Add or replace a modifier entry.removeModByIndex(i)
: Remove a modifier by array index.markModTouchedAt(i)
: Mark a modifier as user-touched.setSelectedPoolKey(key)
: Hint which named pool the pool dice come from.
Roll orchestration
buildFormula(explodes = true) → "Xd6xTN"
: Construct the dice formula string.async execute({ OnClose?, CommitEffects? })
: Entry point for running a roll; must be implemented by subclasses.async onChallengeWillRoll({ baseRoll, actor })
: Pre-roll hook to attach metadata/options.async onChallengeResolved({ roll, actor })
: Post-roll hook to apply side-effects.shouldSelfPublish()
: Whether this roll should be self-published to chat/logs.
Opposed flow helpers
exportForContest()
: Package state into a payload for building a defense step.async buildDefenseProcedure(exportCtx, { defender, contestId })
: Construct the matching defense procedure on the responder side.async getResponderPromptHTML(exportCtx, { contest })
: Build the HTML prompt for the responder; default is yes/no.async renderContestOutcome(exportCtx, ctx)
: Render contested outcome; subclasses override for detailed breakdowns.buildResistancePrep(exportCtx, { initiator, target })
: Return structured data for resistance rolls (damage soak, etc.).
Serialization
toJSON()
: Serialize to a plain JS object.serialize()
: Serialize to a JSON string.static fromJSON(obj, { resolveActor?, resolveItem? })
: Deserialize from a JSON object.static deserialize(json, opts?)
: Deserialize from a JSON string.static registerSubclass(kind, Ctor)
: Register a new subclass (needed for deserialization).static getCtor(kind)
: Look up a registered subclass.static listKinds()
: List all registered kinds.