Keep AI mechanic capabilities in sync
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { AiMechanicAuthoringRequest } from '$lib/ai/mechanic-authoring';
|
||||
import { parseConditionalMechanicDraft } from '$lib/calculations/mechanic-editor';
|
||||
import {
|
||||
AI_MECHANIC_AUTHORING_INSTRUCTIONS,
|
||||
AI_MECHANIC_PROMPT_VERSION,
|
||||
buildAiMechanicAuthoringInput
|
||||
} from './mechanic-authoring-prompt';
|
||||
|
||||
@@ -58,4 +60,100 @@ describe('AI mechanic authoring prompt', () => {
|
||||
]));
|
||||
expect(AI_MECHANIC_AUTHORING_INSTRUCTIONS).toContain('Never invent an Attribute ID');
|
||||
});
|
||||
|
||||
it('advertises continuous restoration as a supported outcome capability', () => {
|
||||
const input = JSON.parse(buildAiMechanicAuthoringInput({
|
||||
request,
|
||||
attributes: [{
|
||||
id: '1bce573c-80f7-4487-b1d3-c4dc598b5244',
|
||||
name: 'Firepower Multiplier',
|
||||
description: null,
|
||||
unit: '%',
|
||||
type: null,
|
||||
category: null,
|
||||
calculationType: null
|
||||
}],
|
||||
events: [{ key: 'enemy_killed', label: 'Enemy killed' }],
|
||||
actions: [],
|
||||
recipeKeys: []
|
||||
}));
|
||||
|
||||
expect(AI_MECHANIC_PROMPT_VERSION).toBe(4);
|
||||
expect(input.canonicalCatalog.grammar.outcome.optional).toEqual(
|
||||
expect.arrayContaining(['delivery', 'durationSeconds'])
|
||||
);
|
||||
expect(input.canonicalCatalog.grammar.outcome.delivery).toEqual([
|
||||
'instant',
|
||||
'continuous'
|
||||
]);
|
||||
expect(input.canonicalCatalog.grammar.continuousRestorationOutcomeShape)
|
||||
.toMatchObject({
|
||||
kind: 'restoration',
|
||||
resource: 'health',
|
||||
delivery: 'continuous',
|
||||
durationSeconds: 3,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': {
|
||||
value: 240,
|
||||
expression: {
|
||||
left: 240,
|
||||
right: {
|
||||
left: {
|
||||
ref: {
|
||||
source: 'attribute'
|
||||
}
|
||||
},
|
||||
right: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(input.examplesAndRules).toEqual(expect.arrayContaining([
|
||||
expect.stringContaining('Continuous Health restoration is supported'),
|
||||
expect.stringContaining('not a per-second value'),
|
||||
expect.stringContaining('Do not replace continuous delivery with a periodic reducer')
|
||||
]));
|
||||
expect(AI_MECHANIC_AUTHORING_INSTRUCTIONS).toContain(
|
||||
'Prompt contract version: 4'
|
||||
);
|
||||
|
||||
const advertised = structuredClone(
|
||||
input.canonicalCatalog.grammar.continuousRestorationOutcomeShape
|
||||
);
|
||||
const tierI = JSON.parse(JSON.stringify(
|
||||
advertised.variants['replace-with-each-required-variant-key']
|
||||
).replaceAll(
|
||||
'replace-with-an-exact-catalog-attribute-id',
|
||||
'1bce573c-80f7-4487-b1d3-c4dc598b5244'
|
||||
));
|
||||
const tierII = structuredClone(tierI);
|
||||
tierII.value = 336;
|
||||
tierII.expression.left = 336;
|
||||
advertised.variants = { I: tierI, II: tierII };
|
||||
const parsed = parseConditionalMechanicDraft({
|
||||
version: 1,
|
||||
simulationReadiness: 'ready',
|
||||
scenarioInputs: [],
|
||||
pools: [],
|
||||
clauses: [{
|
||||
key: 'predator-aura',
|
||||
label: 'Predator Aura',
|
||||
event: 'enemy_killed',
|
||||
if: {
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.distanceMeters' },
|
||||
operator: 'lte',
|
||||
right: 20
|
||||
},
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [],
|
||||
outcomes: [advertised]
|
||||
}]
|
||||
}, {
|
||||
variantKeys: ['I', 'II'],
|
||||
allowedAttributeIds: new Set(['1bce573c-80f7-4487-b1d3-c4dc598b5244'])
|
||||
});
|
||||
expect(parsed.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AiMechanicAuthoringRequest } from '$lib/ai/mechanic-authoring';
|
||||
import { CONDITIONAL_MECHANIC_OUTCOME_CAPABILITIES } from '$lib/calculations/mechanic-editor';
|
||||
import type { AiMechanicAttribute } from './mechanic-authoring-context';
|
||||
|
||||
export const AI_MECHANIC_PROMPT_VERSION = 3;
|
||||
export const AI_MECHANIC_PROMPT_VERSION = 4;
|
||||
|
||||
const mechanicsGrammar = {
|
||||
version: 1,
|
||||
@@ -106,31 +107,36 @@ const mechanicsGrammar = {
|
||||
clearSource: true
|
||||
}
|
||||
],
|
||||
outcome: {
|
||||
required: ['key', 'label', 'phase', 'kind', 'scope', 'basis', 'variants'],
|
||||
phase: ['on_event', 'after_event', 'on_enter', 'on_exit', 'while_active'],
|
||||
kind: ['attribute_modifier', 'direct_damage', 'restoration', 'resource_cost', 'marker'],
|
||||
scope: [
|
||||
'self',
|
||||
'equipped_weapon',
|
||||
'target',
|
||||
'allies',
|
||||
'self_and_allies',
|
||||
'assigned_ability',
|
||||
'all_skills'
|
||||
],
|
||||
basis: ['flat', 'max_health_percent', 'current_health_percent', 'weapon_base_percent'],
|
||||
resource: ['health', 'armor', 'ammo'],
|
||||
optional: [
|
||||
'resource',
|
||||
'combatTiming',
|
||||
'attributeId',
|
||||
'statePool',
|
||||
'perStack',
|
||||
'minStacks',
|
||||
'maxStacks',
|
||||
'descriptionSlot'
|
||||
]
|
||||
outcome: CONDITIONAL_MECHANIC_OUTCOME_CAPABILITIES,
|
||||
continuousRestorationOutcomeShape: {
|
||||
key: 'restore-health',
|
||||
label: 'Restore Health',
|
||||
phase: 'after_event',
|
||||
kind: 'restoration',
|
||||
scope: 'self',
|
||||
basis: 'flat',
|
||||
resource: 'health',
|
||||
delivery: 'continuous',
|
||||
durationSeconds: 3,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': {
|
||||
value: 240,
|
||||
expression: {
|
||||
op: 'multiply',
|
||||
left: 240,
|
||||
right: {
|
||||
op: 'divide',
|
||||
left: {
|
||||
ref: {
|
||||
source: 'attribute',
|
||||
attributeId: 'replace-with-an-exact-catalog-attribute-id'
|
||||
}
|
||||
},
|
||||
right: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
variantValue: {
|
||||
required: ['value'],
|
||||
@@ -233,6 +239,8 @@ export function buildAiMechanicAuthoringInput(input: {
|
||||
'Percent values use the numeric percentage shown in game, such as 3 for +3%, rather than 0.03.',
|
||||
'Live Attribute references must use {source:"attribute",attributeId:"<exact supplied catalog ID>"}. Never use an Attribute name or invent an ID.',
|
||||
'An Attribute whose catalog unit is % resolves to its displayed percentage number, such as 20 for 20%. Divide that reference by 100 when wording applies it as a percentage multiplier, such as 240 × 20%.',
|
||||
'Continuous Health restoration is supported. Use kind:"restoration", resource:"health", delivery:"continuous", and a positive durationSeconds. The variant expression resolves the total restoration across that duration, not a per-second value. Do not replace continuous delivery with a periodic reducer.',
|
||||
'Each continuous-restoration activation creates an independent constant-rate stream. Overlapping streams add, and restoration above the resource maximum is lost when it occurs.',
|
||||
'OS Protocol upgrade scaling may use equipment.protocolTalentUpgrades only when allowProtocolUpgradeScaling is true.',
|
||||
'Assigned-ability scope may be used only when allowAssignedAbility is true.',
|
||||
'If cadence is not stated, ask rather than assuming a one-second interval.',
|
||||
|
||||
Reference in New Issue
Block a user