feat: add community publishing and news syndication
This commit is contained in:
@@ -6,285 +6,388 @@ import type { AiMechanicAttribute } from './mechanic-authoring-context';
|
||||
export const AI_MECHANIC_PROMPT_VERSION = 7;
|
||||
|
||||
const mechanicsGrammar = {
|
||||
version: 1,
|
||||
simulationReadiness: ['ready', 'requires_assumptions', 'captured_only'],
|
||||
scenarioInput: {
|
||||
required: ['key', 'label', 'kind'],
|
||||
kind: ['toggle', 'slider', 'number'],
|
||||
readiness: ['ready', 'requires_assumption'],
|
||||
optional: ['description', 'defaultValue', 'bestCaseValue', 'min', 'max']
|
||||
},
|
||||
pool: {
|
||||
required: ['key', 'label', 'stateKind', 'scope', 'initial'],
|
||||
stateKind: ['flag', 'count', 'stack', 'duration', 'scalar', 'target_mark', 'resource'],
|
||||
scope: ['agent', 'target'],
|
||||
optional: ['min', 'max', 'bestCaseValue', 'expiresAfterSeconds', 'expiresAfterSecondsVariants'],
|
||||
expirationSemantics:
|
||||
'expiresAfterSeconds is one timer per pool entity. Use expiresAfterSecondsVariants instead when each source variant has a different duration; define every requested variant exactly once. Every write resets that entity timer, and expiry resets only that entity to the pool initial value.',
|
||||
targetScopeSemantics:
|
||||
'A target-scoped pool stores one independent value and expiry timer per resolved entity. Use mutation.entity event_target when a weapon-hit mechanic binds state to the enemy that was hit.'
|
||||
},
|
||||
clause: {
|
||||
required: ['key', 'event', 'if', 'reducer', 'mutations', 'outcomes'],
|
||||
optional: ['label', 'recipeKey', 'cooldownSeconds', 'weight']
|
||||
},
|
||||
predicateKinds: ['true', 'exists', 'compare', 'all', 'any', 'not'],
|
||||
compareOperators: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'],
|
||||
reducerKinds: [
|
||||
'none',
|
||||
'count',
|
||||
'modulo',
|
||||
'distinct_count',
|
||||
'consecutive_count',
|
||||
'sliding_count',
|
||||
'duration',
|
||||
'periodic',
|
||||
'derived_scalar'
|
||||
],
|
||||
mutationKinds: ['none', 'add', 'set', 'consume', 'derive', 'clear', 'transfer'],
|
||||
entitySelectors: ['agent', 'event_actor', 'event_target', 'event_field'],
|
||||
valueReferences: ['event', 'scenario', 'combat', 'equipment', 'attribute', 'reducer', 'pool'],
|
||||
combatReferences: MECHANIC_COMBAT_REFERENCE_REGISTRY,
|
||||
numericExpressionOperators: ['add', 'subtract', 'multiply', 'divide', 'min', 'max', 'clamp', 'floor'],
|
||||
referenceShapes: [
|
||||
{ ref: { source: 'event', path: 'values.distanceMeters' } },
|
||||
{ ref: { source: 'scenario', key: 'input-key' } },
|
||||
{ ref: { source: 'combat', path: 'currentHealth', timing: 'current' } },
|
||||
{ ref: { source: 'combat', path: 'currentHealthPercent', timing: 'current' } },
|
||||
{ ref: { source: 'equipment', path: 'protocolTalentUpgrades' } },
|
||||
{ ref: { source: 'attribute', attributeId: 'replace-with-an-exact-catalog-attribute-id' } },
|
||||
{ ref: { source: 'reducer' } },
|
||||
{ ref: { source: 'pool', pool: 'pool-key', entity: { kind: 'agent' } } }
|
||||
],
|
||||
expressionShapes: [
|
||||
{ op: 'multiply', left: 3, right: { ref: { source: 'pool', pool: 'stacks' } } },
|
||||
{ op: 'min', left: { ref: { source: 'reducer' } }, right: 5 },
|
||||
{ op: 'clamp', value: { ref: { source: 'event', path: 'values.enemyCount' } }, min: 0, max: 5 },
|
||||
{ op: 'floor', value: { op: 'divide', left: { ref: { source: 'event', path: 'values.distanceMeters' } }, right: 5 } }
|
||||
],
|
||||
predicateShapes: [
|
||||
{ kind: 'true' },
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.distanceMeters' },
|
||||
operator: 'lte',
|
||||
right: 10
|
||||
},
|
||||
{ kind: 'all', predicates: [{ kind: 'true' }] },
|
||||
{ kind: 'any', predicates: [{ kind: 'true' }] },
|
||||
{ kind: 'not', predicate: { kind: 'true' } }
|
||||
],
|
||||
reducerShapes: [
|
||||
{ kind: 'none' },
|
||||
{ kind: 'count', threshold: 9, reset: 'zero', groupBy: { source: 'event', path: 'targetId' } },
|
||||
{ kind: 'modulo', every: 5 },
|
||||
{
|
||||
kind: 'distinct_count',
|
||||
threshold: 3,
|
||||
windowSeconds: 5,
|
||||
distinctBy: { source: 'event', path: 'targetId' }
|
||||
},
|
||||
{ kind: 'consecutive_count', threshold: 9, groupBy: { source: 'event', path: 'targetId' } },
|
||||
{
|
||||
kind: 'sliding_count',
|
||||
threshold: 9,
|
||||
windowSeconds: 3,
|
||||
reset: 'zero',
|
||||
groupBy: { source: 'event', path: 'targetId' }
|
||||
},
|
||||
{ kind: 'duration', seconds: 3, repeat: false },
|
||||
{ kind: 'periodic', intervalSeconds: 1, immediate: false },
|
||||
{ kind: 'derived_scalar', value: { ref: { source: 'event', path: 'values.enemyCount' } } }
|
||||
],
|
||||
mutationShapes: [
|
||||
{ kind: 'none' },
|
||||
{ kind: 'add', pool: 'stacks', value: 1 },
|
||||
{
|
||||
kind: 'add',
|
||||
pool: 'target-stacks',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 1
|
||||
},
|
||||
{ kind: 'set', pool: 'active', value: 1 },
|
||||
{ kind: 'consume', pool: 'stacks', value: 1 },
|
||||
{ kind: 'derive', pool: 'nearby-enemies', value: { ref: { source: 'reducer' } } },
|
||||
{ kind: 'clear', pool: 'active' },
|
||||
{
|
||||
kind: 'transfer',
|
||||
pool: 'mark',
|
||||
from: { kind: 'event_target' },
|
||||
to: { kind: 'event_field', path: 'values.transferTargetId' },
|
||||
value: 'all',
|
||||
clearSource: true
|
||||
}
|
||||
],
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
preciseTraumaShape: {
|
||||
version: 1,
|
||||
simulationReadiness: 'ready',
|
||||
scenarioInputs: [],
|
||||
pools: [{
|
||||
key: 'trauma',
|
||||
label: 'Trauma',
|
||||
stateKind: 'stack',
|
||||
scope: 'target',
|
||||
initial: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
bestCaseValue: 10,
|
||||
expiresAfterSeconds: 1
|
||||
}],
|
||||
clauses: [
|
||||
{
|
||||
key: 'body-shot-trauma',
|
||||
label: 'Body shot adds one Trauma stack',
|
||||
recipeKey: 'body_shot',
|
||||
event: 'weapon_hit',
|
||||
if: {
|
||||
kind: 'all',
|
||||
predicates: [
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{ kind: 'exists', value: { source: 'event', path: 'values.headshot' } },
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.headshot' },
|
||||
operator: 'eq',
|
||||
right: false
|
||||
}
|
||||
]
|
||||
},
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [{
|
||||
kind: 'add',
|
||||
pool: 'trauma',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 1
|
||||
}],
|
||||
outcomes: [{
|
||||
key: 'trauma-skill-damage',
|
||||
label: 'Trauma Skill Damage',
|
||||
phase: 'on_exit',
|
||||
kind: 'direct_damage',
|
||||
scope: 'target',
|
||||
basis: 'flat',
|
||||
statePool: 'trauma',
|
||||
perStack: true,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': {
|
||||
value: 0,
|
||||
expression: {
|
||||
ref: {
|
||||
source: 'attribute',
|
||||
attributeId: 'replace-with-the-exact-skill-damage-catalog-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
key: 'headshot-trauma',
|
||||
label: 'Headshot adds two Trauma stacks',
|
||||
recipeKey: 'headshot',
|
||||
event: 'weapon_hit',
|
||||
if: {
|
||||
kind: 'all',
|
||||
predicates: [
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{ kind: 'exists', value: { source: 'event', path: 'values.headshot' } },
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.headshot' },
|
||||
operator: 'eq',
|
||||
right: true
|
||||
}
|
||||
]
|
||||
},
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [{
|
||||
kind: 'add',
|
||||
pool: 'trauma',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 2
|
||||
}],
|
||||
outcomes: []
|
||||
}
|
||||
]
|
||||
},
|
||||
variantValue: {
|
||||
required: ['value'],
|
||||
optional: ['expression']
|
||||
},
|
||||
completeDraftShape: {
|
||||
version: 1,
|
||||
simulationReadiness: 'ready',
|
||||
scenarioInputs: [],
|
||||
pools: [{
|
||||
key: 'stacks',
|
||||
label: 'Stacks',
|
||||
stateKind: 'stack',
|
||||
scope: 'agent',
|
||||
initial: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
bestCaseValue: 10,
|
||||
expiresAfterSeconds: 10
|
||||
}],
|
||||
clauses: [{
|
||||
key: 'gain-stack',
|
||||
label: 'Gain one stack',
|
||||
event: 'weapon_hit',
|
||||
if: { kind: 'true' },
|
||||
reducer: { kind: 'count', threshold: 9, reset: 'zero' },
|
||||
mutations: [{ kind: 'add', pool: 'stacks', value: 1 }],
|
||||
outcomes: [{
|
||||
key: 'weapon-damage',
|
||||
label: 'Weapon Damage',
|
||||
phase: 'while_active',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'self',
|
||||
basis: 'flat',
|
||||
attributeId: 'replace-with-an-exact-catalog-attribute-id',
|
||||
statePool: 'stacks',
|
||||
perStack: true,
|
||||
descriptionSlot: 1,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': { value: 3 }
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
version: 1,
|
||||
simulationReadiness: ['ready', 'requires_assumptions', 'captured_only'],
|
||||
scenarioInput: {
|
||||
required: ['key', 'label', 'kind'],
|
||||
kind: ['toggle', 'slider', 'number'],
|
||||
readiness: ['ready', 'requires_assumption'],
|
||||
optional: ['description', 'defaultValue', 'bestCaseValue', 'min', 'max'],
|
||||
},
|
||||
pool: {
|
||||
required: ['key', 'label', 'stateKind', 'scope', 'initial'],
|
||||
stateKind: [
|
||||
'flag',
|
||||
'count',
|
||||
'stack',
|
||||
'duration',
|
||||
'scalar',
|
||||
'target_mark',
|
||||
'resource',
|
||||
],
|
||||
scope: ['agent', 'target'],
|
||||
optional: [
|
||||
'min',
|
||||
'max',
|
||||
'bestCaseValue',
|
||||
'expiresAfterSeconds',
|
||||
'expiresAfterSecondsVariants',
|
||||
],
|
||||
expirationSemantics:
|
||||
'expiresAfterSeconds is one timer per pool entity. Use expiresAfterSecondsVariants instead when each source variant has a different duration; define every requested variant exactly once. Every write resets that entity timer, and expiry resets only that entity to the pool initial value.',
|
||||
targetScopeSemantics:
|
||||
'A target-scoped pool stores one independent value and expiry timer per resolved entity. Use mutation.entity event_target when a weapon-hit mechanic binds state to the enemy that was hit.',
|
||||
},
|
||||
clause: {
|
||||
required: ['key', 'event', 'if', 'reducer', 'mutations', 'outcomes'],
|
||||
optional: ['label', 'recipeKey', 'cooldownSeconds', 'weight'],
|
||||
},
|
||||
predicateKinds: ['true', 'exists', 'compare', 'all', 'any', 'not'],
|
||||
compareOperators: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'],
|
||||
reducerKinds: [
|
||||
'none',
|
||||
'count',
|
||||
'modulo',
|
||||
'distinct_count',
|
||||
'consecutive_count',
|
||||
'sliding_count',
|
||||
'duration',
|
||||
'periodic',
|
||||
'derived_scalar',
|
||||
],
|
||||
mutationKinds: [
|
||||
'none',
|
||||
'add',
|
||||
'set',
|
||||
'consume',
|
||||
'derive',
|
||||
'clear',
|
||||
'transfer',
|
||||
],
|
||||
entitySelectors: ['agent', 'event_actor', 'event_target', 'event_field'],
|
||||
valueReferences: [
|
||||
'event',
|
||||
'scenario',
|
||||
'combat',
|
||||
'equipment',
|
||||
'attribute',
|
||||
'reducer',
|
||||
'pool',
|
||||
],
|
||||
combatReferences: MECHANIC_COMBAT_REFERENCE_REGISTRY,
|
||||
numericExpressionOperators: [
|
||||
'add',
|
||||
'subtract',
|
||||
'multiply',
|
||||
'divide',
|
||||
'min',
|
||||
'max',
|
||||
'clamp',
|
||||
'floor',
|
||||
],
|
||||
referenceShapes: [
|
||||
{ ref: { source: 'event', path: 'values.distanceMeters' } },
|
||||
{ ref: { source: 'scenario', key: 'input-key' } },
|
||||
{ ref: { source: 'combat', path: 'currentHealth', timing: 'current' } },
|
||||
{
|
||||
ref: {
|
||||
source: 'combat',
|
||||
path: 'currentHealthPercent',
|
||||
timing: 'current',
|
||||
},
|
||||
},
|
||||
{ ref: { source: 'equipment', path: 'protocolTalentUpgrades' } },
|
||||
{
|
||||
ref: {
|
||||
source: 'attribute',
|
||||
attributeId: 'replace-with-an-exact-catalog-attribute-id',
|
||||
},
|
||||
},
|
||||
{ ref: { source: 'reducer' } },
|
||||
{ ref: { source: 'pool', pool: 'pool-key', entity: { kind: 'agent' } } },
|
||||
],
|
||||
expressionShapes: [
|
||||
{
|
||||
op: 'multiply',
|
||||
left: 3,
|
||||
right: { ref: { source: 'pool', pool: 'stacks' } },
|
||||
},
|
||||
{ op: 'min', left: { ref: { source: 'reducer' } }, right: 5 },
|
||||
{
|
||||
op: 'clamp',
|
||||
value: { ref: { source: 'event', path: 'values.enemyCount' } },
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
{
|
||||
op: 'floor',
|
||||
value: {
|
||||
op: 'divide',
|
||||
left: { ref: { source: 'event', path: 'values.distanceMeters' } },
|
||||
right: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
predicateShapes: [
|
||||
{ kind: 'true' },
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.distanceMeters' },
|
||||
operator: 'lte',
|
||||
right: 10,
|
||||
},
|
||||
{ kind: 'all', predicates: [{ kind: 'true' }] },
|
||||
{ kind: 'any', predicates: [{ kind: 'true' }] },
|
||||
{ kind: 'not', predicate: { kind: 'true' } },
|
||||
],
|
||||
reducerShapes: [
|
||||
{ kind: 'none' },
|
||||
{
|
||||
kind: 'count',
|
||||
threshold: 9,
|
||||
reset: 'zero',
|
||||
groupBy: { source: 'event', path: 'targetId' },
|
||||
},
|
||||
{ kind: 'modulo', every: 5 },
|
||||
{
|
||||
kind: 'distinct_count',
|
||||
threshold: 3,
|
||||
windowSeconds: 5,
|
||||
distinctBy: { source: 'event', path: 'targetId' },
|
||||
},
|
||||
{
|
||||
kind: 'consecutive_count',
|
||||
threshold: 9,
|
||||
groupBy: { source: 'event', path: 'targetId' },
|
||||
},
|
||||
{
|
||||
kind: 'sliding_count',
|
||||
threshold: 9,
|
||||
windowSeconds: 3,
|
||||
reset: 'zero',
|
||||
groupBy: { source: 'event', path: 'targetId' },
|
||||
},
|
||||
{ kind: 'duration', seconds: 3, repeat: false },
|
||||
{ kind: 'periodic', intervalSeconds: 1, immediate: false },
|
||||
{
|
||||
kind: 'derived_scalar',
|
||||
value: { ref: { source: 'event', path: 'values.enemyCount' } },
|
||||
},
|
||||
],
|
||||
mutationShapes: [
|
||||
{ kind: 'none' },
|
||||
{ kind: 'add', pool: 'stacks', value: 1 },
|
||||
{
|
||||
kind: 'add',
|
||||
pool: 'target-stacks',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 1,
|
||||
},
|
||||
{ kind: 'set', pool: 'active', value: 1 },
|
||||
{ kind: 'consume', pool: 'stacks', value: 1 },
|
||||
{
|
||||
kind: 'derive',
|
||||
pool: 'nearby-enemies',
|
||||
value: { ref: { source: 'reducer' } },
|
||||
},
|
||||
{ kind: 'clear', pool: 'active' },
|
||||
{
|
||||
kind: 'transfer',
|
||||
pool: 'mark',
|
||||
from: { kind: 'event_target' },
|
||||
to: { kind: 'event_field', path: 'values.transferTargetId' },
|
||||
value: 'all',
|
||||
clearSource: true,
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
preciseTraumaShape: {
|
||||
version: 1,
|
||||
simulationReadiness: 'ready',
|
||||
scenarioInputs: [],
|
||||
pools: [
|
||||
{
|
||||
key: 'trauma',
|
||||
label: 'Trauma',
|
||||
stateKind: 'stack',
|
||||
scope: 'target',
|
||||
initial: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
bestCaseValue: 10,
|
||||
expiresAfterSeconds: 1,
|
||||
},
|
||||
],
|
||||
clauses: [
|
||||
{
|
||||
key: 'body-shot-trauma',
|
||||
label: 'Body shot adds one Trauma stack',
|
||||
recipeKey: 'body_shot',
|
||||
event: 'weapon_hit',
|
||||
if: {
|
||||
kind: 'all',
|
||||
predicates: [
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{
|
||||
kind: 'exists',
|
||||
value: { source: 'event', path: 'values.headshot' },
|
||||
},
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.headshot' },
|
||||
operator: 'eq',
|
||||
right: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [
|
||||
{
|
||||
kind: 'add',
|
||||
pool: 'trauma',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
outcomes: [
|
||||
{
|
||||
key: 'trauma-skill-damage',
|
||||
label: 'Trauma Skill Damage',
|
||||
phase: 'on_exit',
|
||||
kind: 'direct_damage',
|
||||
scope: 'target',
|
||||
basis: 'flat',
|
||||
statePool: 'trauma',
|
||||
perStack: true,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': {
|
||||
value: 0,
|
||||
expression: {
|
||||
ref: {
|
||||
source: 'attribute',
|
||||
attributeId:
|
||||
'replace-with-the-exact-skill-damage-catalog-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'headshot-trauma',
|
||||
label: 'Headshot adds two Trauma stacks',
|
||||
recipeKey: 'headshot',
|
||||
event: 'weapon_hit',
|
||||
if: {
|
||||
kind: 'all',
|
||||
predicates: [
|
||||
{ kind: 'exists', value: { source: 'event', path: 'targetId' } },
|
||||
{
|
||||
kind: 'exists',
|
||||
value: { source: 'event', path: 'values.headshot' },
|
||||
},
|
||||
{
|
||||
kind: 'compare',
|
||||
left: { source: 'event', path: 'values.headshot' },
|
||||
operator: 'eq',
|
||||
right: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [
|
||||
{
|
||||
kind: 'add',
|
||||
pool: 'trauma',
|
||||
entity: { kind: 'event_target' },
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
outcomes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
variantValue: {
|
||||
required: ['value'],
|
||||
optional: ['expression'],
|
||||
},
|
||||
completeDraftShape: {
|
||||
version: 1,
|
||||
simulationReadiness: 'ready',
|
||||
scenarioInputs: [],
|
||||
pools: [
|
||||
{
|
||||
key: 'stacks',
|
||||
label: 'Stacks',
|
||||
stateKind: 'stack',
|
||||
scope: 'agent',
|
||||
initial: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
bestCaseValue: 10,
|
||||
expiresAfterSeconds: 10,
|
||||
},
|
||||
],
|
||||
clauses: [
|
||||
{
|
||||
key: 'gain-stack',
|
||||
label: 'Gain one stack',
|
||||
event: 'weapon_hit',
|
||||
if: { kind: 'true' },
|
||||
reducer: { kind: 'count', threshold: 9, reset: 'zero' },
|
||||
mutations: [{ kind: 'add', pool: 'stacks', value: 1 }],
|
||||
outcomes: [
|
||||
{
|
||||
key: 'weapon-damage',
|
||||
label: 'Weapon Damage',
|
||||
phase: 'while_active',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'self',
|
||||
basis: 'flat',
|
||||
attributeId: 'replace-with-an-exact-catalog-attribute-id',
|
||||
statePool: 'stacks',
|
||||
perStack: true,
|
||||
descriptionSlot: 1,
|
||||
variants: {
|
||||
'replace-with-each-required-variant-key': { value: 3 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const AI_MECHANIC_AUTHORING_INSTRUCTIONS = `You are the SHD mechanic authoring assistant for The Division Resurgence.
|
||||
@@ -314,49 +417,49 @@ Response rules:
|
||||
The response is machine-validated after generation. Invalid or unsupported drafts are rejected and are never saved.`;
|
||||
|
||||
export function buildAiMechanicAuthoringInput(input: {
|
||||
request: AiMechanicAuthoringRequest;
|
||||
attributes: AiMechanicAttribute[];
|
||||
events: unknown[];
|
||||
actions: unknown[];
|
||||
recipeKeys: string[];
|
||||
request: AiMechanicAuthoringRequest;
|
||||
attributes: AiMechanicAttribute[];
|
||||
events: unknown[];
|
||||
actions: unknown[];
|
||||
recipeKeys: string[];
|
||||
}) {
|
||||
const { request } = input;
|
||||
return JSON.stringify({
|
||||
task: 'Author or revise a mechanic draft from the exact in-game wording.',
|
||||
source: request.context,
|
||||
sourcePolicy: {
|
||||
variantKeys: request.valueColumns.map((column) => column.variantKey),
|
||||
valueColumns: request.valueColumns,
|
||||
allowAssignedAbility: request.allowAssignedAbility,
|
||||
allowProtocolUpgradeScaling: request.allowProtocolUpgradeScaling
|
||||
},
|
||||
clarificationTranscript: request.transcript,
|
||||
currentDraft: request.currentDraft,
|
||||
canonicalCatalog: {
|
||||
grammar: mechanicsGrammar,
|
||||
events: input.events,
|
||||
actions: input.actions,
|
||||
recipeKeys: input.recipeKeys,
|
||||
attributes: input.attributes
|
||||
},
|
||||
examplesAndRules: [
|
||||
'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%.',
|
||||
'combat.currentHealthPercent with timing:"current" resolves authoritative current Health divided by resolved maximum Health on a 0–100 scale. Use operator:"lt" for exact “below” wording.',
|
||||
'Health threshold bands must be mutually exclusive. For thresholds 25, 50, and 75, use <25, >=25 and <50, and >=50 and <75; exact threshold values enter the next weaker band.',
|
||||
'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.',
|
||||
'A target-scoped pool owns an independent value and expiry timer for each entity. Bind hit-created target state with entity:{kind:"event_target"}; do not collapse different enemies into one Agent-scoped pool.',
|
||||
'Writing a target-scoped pool refreshes only that target entity timer. An on_exit outcome runs when that entity expires and uses the entity previous state for stack bands and perStack multiplication, even though the stored pool value has already reset.',
|
||||
'When an outcome value is exactly a live Attribute amount, use the Attribute reference directly as the variant expression. Divide by 100 only when a displayed percentage is being used as a multiplier.',
|
||||
'Precise Trauma is representable as one target-scoped stack pool with a one-second expiry: body shots add one stack, headshots add two, and one on_exit direct-damage outcome uses the expiring target previous stack count. This models the stated one-second debuff without inventing generic repeated target ticks.',
|
||||
'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.',
|
||||
'If wording contains separate actions such as restoration on a periodic snapshot and a health cost on every shot, represent them as separate clauses.',
|
||||
'A pool with expiresAfterSeconds refreshes the shared expiry for every current stack whenever any mutation writes that pool. Use it for mechanics where gaining a stack refreshes the duration of the full stack pool.',
|
||||
'Manual editing remains authoritative. Preserve a valid current draft only when it agrees with the supplied wording and answers.'
|
||||
]
|
||||
});
|
||||
const { request } = input;
|
||||
return JSON.stringify({
|
||||
task: 'Author or revise a mechanic draft from the exact in-game wording.',
|
||||
source: request.context,
|
||||
sourcePolicy: {
|
||||
variantKeys: request.valueColumns.map((column) => column.variantKey),
|
||||
valueColumns: request.valueColumns,
|
||||
allowAssignedAbility: request.allowAssignedAbility,
|
||||
allowProtocolUpgradeScaling: request.allowProtocolUpgradeScaling,
|
||||
},
|
||||
clarificationTranscript: request.transcript,
|
||||
currentDraft: request.currentDraft,
|
||||
canonicalCatalog: {
|
||||
grammar: mechanicsGrammar,
|
||||
events: input.events,
|
||||
actions: input.actions,
|
||||
recipeKeys: input.recipeKeys,
|
||||
attributes: input.attributes,
|
||||
},
|
||||
examplesAndRules: [
|
||||
'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%.',
|
||||
'combat.currentHealthPercent with timing:"current" resolves authoritative current Health divided by resolved maximum Health on a 0–100 scale. Use operator:"lt" for exact “below” wording.',
|
||||
'Health threshold bands must be mutually exclusive. For thresholds 25, 50, and 75, use <25, >=25 and <50, and >=50 and <75; exact threshold values enter the next weaker band.',
|
||||
'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.',
|
||||
'A target-scoped pool owns an independent value and expiry timer for each entity. Bind hit-created target state with entity:{kind:"event_target"}; do not collapse different enemies into one Agent-scoped pool.',
|
||||
'Writing a target-scoped pool refreshes only that target entity timer. An on_exit outcome runs when that entity expires and uses the entity previous state for stack bands and perStack multiplication, even though the stored pool value has already reset.',
|
||||
'When an outcome value is exactly a live Attribute amount, use the Attribute reference directly as the variant expression. Divide by 100 only when a displayed percentage is being used as a multiplier.',
|
||||
'Precise Trauma is representable as one target-scoped stack pool with a one-second expiry: body shots add one stack, headshots add two, and one on_exit direct-damage outcome uses the expiring target previous stack count. This models the stated one-second debuff without inventing generic repeated target ticks.',
|
||||
'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.',
|
||||
'If wording contains separate actions such as restoration on a periodic snapshot and a health cost on every shot, represent them as separate clauses.',
|
||||
'A pool with expiresAfterSeconds refreshes the shared expiry for every current stack whenever any mutation writes that pool. Use it for mechanics where gaining a stack refreshes the duration of the full stack pool.',
|
||||
'Manual editing remains authoritative. Preserve a valid current draft only when it agrees with the supplied wording and answers.',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user