Support target-scoped trauma mechanics

This commit is contained in:
JB
2026-07-27 22:20:52 -04:00
parent 7e78d75f85
commit 0b3be3587b
20 changed files with 1032 additions and 93 deletions
+28 -6
View File
@@ -25,6 +25,11 @@ export type MechanicEventDefinition = {
key: MechanicEventKind;
label: string;
description: string;
fields?: readonly {
path: string;
label: string;
type: 'boolean' | 'number' | 'string';
}[];
};
export const MECHANIC_EVENT_REGISTRY: Readonly<Record<MechanicEventKind, MechanicEventDefinition>> = {
@@ -36,7 +41,11 @@ export const MECHANIC_EVENT_REGISTRY: Readonly<Record<MechanicEventKind, Mechani
weapon_hit: {
key: 'weapon_hit',
label: 'Weapon hit',
description: 'A weapon attack connects with a target. Critical and distance details belong in event values.'
description: 'A weapon attack connects with a target. The headshot boolean distinguishes headshots from body shots.',
fields: [
{ path: 'targetId', label: 'Target ID', type: 'string' },
{ path: 'values.headshot', label: 'Headshot', type: 'boolean' }
]
},
skill_deployed: {
key: 'skill_deployed',
@@ -326,6 +335,7 @@ export type MechanicOutcomeEmission = {
clauseId: string;
outcomeId: string;
at: number;
targetId: string | null;
phase: OutcomePhase;
kind: MechanicOutcome['kind'];
scope: MechanicOutcome['scope'];
@@ -401,6 +411,7 @@ type EvaluationContext = {
type ContinuousDelivery = {
clauseId: string;
outcomeId: string;
targetId: string | null;
phase: OutcomePhase;
scope: MechanicOutcomeScope;
resource: MechanicResource;
@@ -895,14 +906,22 @@ function emitOutcomes(
continuousDeliveries?: ContinuousDelivery[]
): MechanicOutcomeEmission[] {
const emissions: MechanicOutcomeEmission[] = [];
const stateValues = (outcome: MechanicOutcome) => {
const current = outcomeState(outcome, context);
const previous = outcome.statePool ? before.get(outcome.statePool) ?? current : current;
return {
current,
previous,
effective: outcome.phase === 'on_exit' ? previous : current
};
};
const eligible = (clause.outcomes ?? []).filter((outcome) => {
if (phase === 'before' && outcome.phase !== 'on_event') return false;
if (phase === 'after' && outcome.phase === 'on_event') return false;
const current = outcomeState(outcome, context);
const previous = outcome.statePool ? before.get(outcome.statePool) ?? current : current;
const { current, previous, effective } = stateValues(outcome);
if (!phaseMatches(outcome.phase, previous, current)) return false;
if (outcome.minStacks !== undefined && current < outcome.minStacks) return false;
if (outcome.maxStacks !== undefined && current > outcome.maxStacks) return false;
if (outcome.minStacks !== undefined && effective < outcome.minStacks) return false;
if (outcome.maxStacks !== undefined && effective > outcome.maxStacks) return false;
return true;
}).sort((left, right) => {
const priority = (outcome: MechanicOutcome) =>
@@ -910,7 +929,7 @@ function emitOutcomes(
return priority(left) - priority(right);
});
for (const outcome of eligible) {
const current = outcomeState(outcome, context);
const { effective: current } = stateValues(outcome);
const baseValue = numericValue(outcome.value, context);
const value = outcome.perStack ? baseValue * current : baseValue;
const resolvedValue = resolveOutcomeValue(outcome, value, context);
@@ -925,6 +944,7 @@ function emitOutcomes(
continuousDeliveries?.push({
clauseId: clause.id,
outcomeId: outcome.id,
targetId: context.event.targetId ?? null,
phase: outcome.phase,
scope: outcome.scope,
resource: outcomeResource(outcome),
@@ -957,6 +977,7 @@ function emitOutcomes(
clauseId: clause.id,
outcomeId: outcome.id,
at,
targetId: context.event.targetId ?? null,
phase: outcome.phase,
kind: outcome.kind,
scope: outcome.scope,
@@ -1007,6 +1028,7 @@ function advanceContinuousDeliveries(
clauseId: delivery.clauseId,
outcomeId: delivery.outcomeId,
at: through,
targetId: delivery.targetId,
phase: delivery.phase,
kind: 'restoration',
scope: delivery.scope,