Adding skills & isac talent updates

This commit is contained in:
jb
2026-07-29 20:56:22 -04:00
parent f1b473ffa8
commit 65a4987cad
68 changed files with 5486 additions and 569 deletions
+43 -15
View File
@@ -84,6 +84,7 @@ export type ConditionalEffectOutcomeRow = {
combat_timing?: DatabaseValue;
delivery_mode?: DatabaseValue;
delivery_duration_seconds?: DatabaseValue;
targeting_json?: DatabaseValue;
value_scaling: DatabaseValue;
unit: DatabaseValue;
damage_basis: DatabaseValue;
@@ -192,7 +193,8 @@ function isValueReference(value: unknown) {
if (value.source === 'combat') {
return typeof value.path === 'string'
&& Boolean(value.path.trim())
&& (value.timing === undefined || value.timing === 'pre_event' || value.timing === 'current');
&& (value.timing === undefined || value.timing === 'pre_event' || value.timing === 'current')
&& !(value.path === 'currentHealthPercent' && value.timing === 'pre_event');
}
if (value.source === 'attribute') {
return typeof value.attributeId === 'string' && Boolean(value.attributeId.trim());
@@ -211,6 +213,7 @@ function isNumericExpression(value: unknown): value is NumericExpression {
if (finite(value)) return true;
if (!object(value)) return false;
if ('ref' in value) return isValueReference(value.ref);
if (value.op === 'floor') return isNumericExpression(value.value);
if (value.op === 'clamp') {
return isNumericExpression(value.value)
&& isNumericExpression(value.min)
@@ -441,6 +444,7 @@ function parsePool(
row: ConditionalEffectStatePoolRow,
ruleId: string,
index: number,
selectedVariant: string,
errors: string[]
): { id: string; definition: PoolDefinition } | null {
const path = `State pool ${index + 1}`;
@@ -460,6 +464,14 @@ function parsePool(
const duration = number(row.duration_seconds);
const parameters = parseJson(row.parameters_json, `${path} parameters`, errors);
if (parameters.ok && !object(parameters.value)) errors.push(`${path} parameters must be a JSON object.`);
const durationVariants = parameters.ok && object(parameters.value)
&& object(parameters.value.expiresAfterSecondsVariants)
? parameters.value.expiresAfterSecondsVariants
: null;
const variantDuration = durationVariants ? number(durationVariants[selectedVariant] as DatabaseValue) : null;
if (durationVariants && variantDuration === null) {
errors.push(`${path} has no valid expiration duration for variant "${selectedVariant}".`);
}
return {
id,
definition: {
@@ -470,7 +482,9 @@ function parsePool(
...(minimum === null ? {} : { min: minimum }),
max: maximum,
...(bestCase === null ? {} : { bestCaseValue: bestCase }),
...(duration === null ? {} : { expiresAfterSeconds: duration })
...(variantDuration !== null
? { expiresAfterSeconds: variantDuration }
: duration === null ? {} : { expiresAfterSeconds: duration })
}
};
}
@@ -478,10 +492,14 @@ function parsePool(
function outcomeKind(value: string): MechanicOutcome['kind'] | null {
if (value === 'active_modifier') return 'attribute_modifier';
if (value === 'direct_damage') return 'direct_damage';
if (value === 'restoration' || ['restore_health', 'restore_armor', 'restore_ammo'].includes(value)) {
if (
value === 'restoration'
|| ['restore_health', 'restore_armor', 'restore_ammo', 'restore_signature_ability_charge'].includes(value)
) {
return 'restoration';
}
if (value === 'resource_cost') return 'resource_cost';
if (value === 'weapon_reload') return 'weapon_reload';
if (value === 'marker') return 'marker';
return null;
}
@@ -489,6 +507,7 @@ function outcomeKind(value: string): MechanicOutcome['kind'] | null {
function outcomeScope(value: string): MechanicOutcome['scope'] | null {
if (value === 'self') return 'self';
if (value === 'equipped_weapon') return 'equipped_weapon';
if (value === 'inactive_weapon') return 'inactive_weapon';
if (value === 'target') return 'target';
if (value === 'allies') return 'allies';
if (value === 'self_and_allies') return 'self_and_allies';
@@ -547,15 +566,17 @@ function parseOutcome(
? 'armor'
: text(row.application_kind) === 'restore_ammo'
? 'ammo'
: text(row.application_kind) === 'restore_signature_ability_charge'
? 'signature_ability_charge'
: text(row.application_kind) === 'resource_cost'
? 'health'
: null;
const resource = nullableText(row.resource_kind) ?? legacyResource;
if (
(kind === 'restoration' || kind === 'resource_cost')
&& !['health', 'armor', 'ammo'].includes(String(resource))
&& !['health', 'armor', 'ammo', 'signature_ability_charge'].includes(String(resource))
) {
errors.push(`${path} requires an explicit health, armor, or ammo resource.`);
errors.push(`${path} requires an explicit supported resource.`);
}
const combatTiming = nullableText(row.combat_timing);
if (combatTiming && combatTiming !== 'pre_event' && combatTiming !== 'current') {
@@ -572,6 +593,13 @@ function parseOutcome(
if (delivery === 'instant' && durationSeconds !== null) {
errors.push(`${path} instant delivery cannot set a duration.`);
}
let targeting: MechanicOutcome['targeting'] | undefined;
if (row.targeting_json !== null && row.targeting_json !== undefined && row.targeting_json !== '') {
const parsed = parseJson(row.targeting_json, `${path} targeting`, errors);
if (parsed.ok && typeof parsed.value === 'object' && parsed.value !== null) {
targeting = parsed.value as MechanicOutcome['targeting'];
}
}
return {
clauseId: nullableText(row.clause_id),
outcome: {
@@ -591,6 +619,7 @@ function parseOutcome(
...(delivery === 'continuous' && durationSeconds !== null
? { delivery, durationSeconds }
: {}),
...(targeting ? { targeting } : {}),
...(kind === 'attribute_modifier' && nullableText(row.stat_attribute_id)
? { attributeId: text(row.stat_attribute_id) }
: {}),
@@ -627,16 +656,6 @@ export function adaptConditionalEffectProgram(
}
const scenarioInputs = parseScenarioInputs(input.rule.scenario_inputs_json, errors);
const parsedPools = input.statePools
.slice()
.sort((left, right) => (number(left.sort_order) ?? 0) - (number(right.sort_order) ?? 0))
.map((row, index) => parsePool(row, ruleId, index, errors))
.filter((value): value is NonNullable<typeof value> => value !== null);
const poolKeys = new Map(parsedPools.map((pool) => [pool.id, pool.definition.key]));
const triggerMap = new Map(
input.triggers.map((trigger) => [text(trigger.id), trigger])
);
const variantKeys = [...new Set(input.outcomes.map((row) => text(row.variant_key)).filter(Boolean))];
let selectedVariant = options.variantKey?.trim() || '';
if (!selectedVariant) {
@@ -648,6 +667,15 @@ export function adaptConditionalEffectProgram(
} else if (variantKeys.length && !variantKeys.includes(selectedVariant)) {
errors.push(`Variant "${selectedVariant}" does not exist for this rule.`);
}
const parsedPools = input.statePools
.slice()
.sort((left, right) => (number(left.sort_order) ?? 0) - (number(right.sort_order) ?? 0))
.map((row, index) => parsePool(row, ruleId, index, selectedVariant, errors))
.filter((value): value is NonNullable<typeof value> => value !== null);
const poolKeys = new Map(parsedPools.map((pool) => [pool.id, pool.definition.key]));
const triggerMap = new Map(
input.triggers.map((trigger) => [text(trigger.id), trigger])
);
const selectedOutcomes = input.outcomes
.filter((row) => text(row.variant_key) === selectedVariant)