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
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';
import {
currentHealthPercentBandPredicate,
eventFieldComparisonPredicate,
parseConditionalMechanicDraft,
readCurrentHealthPercentBand,
readEventFieldComparison,
readStatePoolThreshold,
statePoolThresholdPredicate,
type ConditionalMechanicDraftV1
} from './mechanic-editor.js';
@@ -54,6 +60,116 @@ const draft = (): ConditionalMechanicDraftV1 => ({
});
describe('conditional mechanic authoring payload', () => {
it('resolves and validates variant-specific pool expiration durations', () => {
const value = draft();
value.pools[0] = {
...value.pools[0],
expiresAfterSeconds: undefined,
expiresAfterSecondsVariants: { I: 12, II: 18 }
};
const result = parseConditionalMechanicDraft(value, { variantKeys: ['I', 'II'] });
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.programs.I.pools?.[0].expiresAfterSeconds).toBe(12);
expect(result.value.programs.II.pools?.[0].expiresAfterSeconds).toBe(18);
value.pools[0].expiresAfterSecondsVariants = { I: 12 };
const invalid = parseConditionalMechanicDraft(value, { variantKeys: ['I', 'II'] });
expect(invalid.ok).toBe(false);
if (!invalid.ok) {
expect(invalid.errors).toContain(
'State pool 1 variant expiration durations must define one positive number for each variant.'
);
}
});
it('round-trips strict manual current Health percentage band editor state', () => {
const predicate = currentHealthPercentBandPredicate({ minimum: 25, maximum: 50 });
expect(readCurrentHealthPercentBand(predicate)).toEqual({ minimum: 25, maximum: 50 });
expect(predicate).toEqual({
kind: 'all',
predicates: [{
kind: 'compare',
left: { source: 'combat', path: 'currentHealthPercent', timing: 'current' },
operator: 'gte',
right: 25
}, {
kind: 'compare',
left: { source: 'combat', path: 'currentHealthPercent', timing: 'current' },
operator: 'lt',
right: 50
}]
});
expect(readCurrentHealthPercentBand({
kind: 'compare',
left: { source: 'event', path: 'values.healthPercent' },
operator: 'lt',
right: 50
})).toBeNull();
});
it('round-trips editable event scalar comparisons without claiming advanced predicates', () => {
for (const value of ['moving', 4, true]) {
const comparison = { path: 'values.state', operator: 'eq' as const, value };
const predicate = eventFieldComparisonPredicate(comparison);
expect(readEventFieldComparison(predicate)).toEqual(comparison);
}
expect(readEventFieldComparison({
kind: 'compare',
left: { source: 'event', path: 'values.state' },
operator: 'eq',
right: { source: 'event', path: 'values.previousState' }
})).toBeNull();
expect(readEventFieldComparison({
kind: 'all',
predicates: []
})).toBeNull();
});
it('round-trips state-pool thresholds and preserves unsupported targeted pools as advanced', () => {
const comparison = { pool: 'stacks', operator: 'gte' as const, value: 10 };
const predicate = statePoolThresholdPredicate(comparison);
expect(readStatePoolThreshold(predicate)).toEqual(comparison);
expect(readStatePoolThreshold({
kind: 'compare',
left: { source: 'pool', pool: 'stacks', entity: { kind: 'event_target' } },
operator: 'gt',
right: 0
})).toBeNull();
});
it('parses movement-state and stack-threshold authoring predicates unchanged', () => {
const value = draft();
value.clauses[0].event = 'movement_snapshot';
value.clauses[0].if = eventFieldComparisonPredicate({
path: 'values.state',
operator: 'eq',
value: 'sprinting'
});
value.clauses.push({
...structuredClone(value.clauses[0]),
key: 'consume-stacks',
event: 'shot_fired',
if: statePoolThresholdPredicate({
pool: 'effect',
operator: 'gt',
value: 0
})
});
const result = parseConditionalMechanicDraft(value, {
variantKeys: ['I', 'II'],
allowedAttributeIds: new Set(['weapon-damage'])
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.draft.clauses.map((clause) => clause.if)).toEqual(
value.clauses.map((clause) => clause.if)
);
expect(result.value.programs.I.clauses.map((clause) => clause.if)).toEqual(
value.clauses.map((clause) => clause.if)
);
});
it('accepts an empty captured-only program for deferred modeling', () => {
const partial: ConditionalMechanicDraftV1 = {
version: 1,
@@ -427,4 +543,51 @@ describe('conditional mechanic authoring payload', () => {
errors: ['The conditional mechanic contains malformed JSON.']
});
});
it('rejects invalid pre-event timing for current Health percentage references', () => {
const invalid = draft();
invalid.clauses[0].if = {
kind: 'compare',
left: { source: 'combat', path: 'currentHealthPercent', timing: 'pre_event' },
operator: 'lt',
right: 50
};
const result = parseConditionalMechanicDraft(invalid, {
variantKeys: ['I', 'II'],
allowedAttributeIds: new Set(['weapon-damage'])
});
expect(result.ok).toBe(false);
});
it('accepts compatible radius targeting and rejects incompatible scopes', () => {
const valid = draft();
valid.clauses[0].outcomes[0].scope = 'allies';
valid.clauses[0].outcomes[0].targeting = {
kind: 'within_radius',
population: 'allies',
radiusMeters: 40
};
const parsed = parseConditionalMechanicDraft(valid, {
variantKeys: ['I', 'II'],
allowedAttributeIds: new Set(['weapon-damage'])
});
expect(parsed.ok).toBe(true);
if (parsed.ok) {
expect(parsed.value.draft.clauses[0].outcomes[0].targeting).toEqual(
valid.clauses[0].outcomes[0].targeting
);
}
valid.clauses[0].outcomes[0].scope = 'self';
const incompatible = parseConditionalMechanicDraft(valid, {
variantKeys: ['I', 'II'],
allowedAttributeIds: new Set(['weapon-damage'])
});
expect(incompatible.ok).toBe(false);
if (!incompatible.ok) {
expect(incompatible.errors).toContain(
'Clause 1 outcome 1 ally radius targeting requires allies or self-and-allies scope.'
);
}
});
});