Adding skills & isac talent updates
This commit is contained in:
@@ -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.'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user