import * as v from 'valibot' function strictRecord(key, value) { return v.intersect([ v.custom(input => Boolean(input) && typeof input === 'object' && !Array.isArray(input), 'Expected an object record'), v.record(key, value), ]) } export const VISUAL_GAME_STATE_SCALAR_VALUE_TYPES = Object.freeze(['string', 'number', 'boolean']) const STATE_SCALAR_SCHEMAS = Object.freeze({ boolean: v.boolean(), number: v.number(), string: v.string(), }) const StateScalar = v.union(VISUAL_GAME_STATE_SCALAR_VALUE_TYPES.map(valueType => ( STATE_SCALAR_SCHEMAS[valueType] ))) const StateScalarRecord = strictRecord(v.string(), StateScalar) const StringArray = v.array(v.string()) function patchField(definition) { switch (definition.schemaKind) { case 'boolean': return v.optional(v.boolean()) case 'nullable_string': return v.optional(v.nullable(v.string())) case 'state_scalar_record': return v.optional(StateScalarRecord) case 'string': return v.optional(v.string()) case 'string_array': return v.optional(StringArray) default: throw new Error(`Unhandled visual-game patch schema kind: ${definition.schemaKind}`) } } function patchEntries(definitions) { return Object.fromEntries(Object.entries(definitions).map(([field, definition]) => [ field, patchField(definition), ])) } function patchDefinition(schemaKind, registryScope, registryPathKind, registryValueTypes) { return Object.freeze({ schemaKind, registryScope, registryPathKind, registryValueTypes: Object.freeze([...registryValueTypes]), }) } /** * CandidateDelta object patch fields and their stateRegistry representation. * Both the Valibot boundary and compile-time registry audit consume this table. */ export const VISUAL_GAME_OBJECT_STATE_PATCH_FIELDS = Object.freeze({ known: patchDefinition('boolean', 'object_state', 'field', ['boolean']), inspected: patchDefinition('boolean', 'object_state', 'field', ['boolean']), usable: patchDefinition('boolean', 'object_state', 'field', ['boolean']), damaged: patchDefinition('boolean', 'object_state', 'field', ['boolean']), heldBy: patchDefinition('nullable_string', 'object_state', 'field', ['string']), location: patchDefinition('string', 'object_state', 'field', ['string']), state: patchDefinition('string', 'object_state', 'field', ['string']), flags: patchDefinition( 'state_scalar_record', 'object_state', 'record_entry', VISUAL_GAME_STATE_SCALAR_VALUE_TYPES, ), }) /** * CandidateDelta NPC patch fields and their stateRegistry representation. */ export const VISUAL_GAME_NPC_STATE_PATCH_FIELDS = Object.freeze({ locationId: patchDefinition('string', 'npc_state', 'field', ['string']), position: patchDefinition('string', 'npc_state', 'field', ['string']), mood: patchDefinition('string', 'npc_state', 'field', ['string']), knownFacts: patchDefinition('string_array', 'npc_state', 'array_item', ['string']), revealedSecrets: patchDefinition('string_array', 'npc_state', 'array_item', ['string']), relationshipFlags: patchDefinition( 'state_scalar_record', 'npc_relationship_flag', 'record_entry', VISUAL_GAME_STATE_SCALAR_VALUE_TYPES, ), }) const ObjectStatePatch = v.strictObject(patchEntries(VISUAL_GAME_OBJECT_STATE_PATCH_FIELDS)) const NpcStatePatch = v.strictObject(patchEntries(VISUAL_GAME_NPC_STATE_PATCH_FIELDS)) const InventoryChanges = v.strictObject({ add: StringArray, remove: StringArray, }) const DeltaSuggestionEntries = { statChanges: v.optional(strictRecord(v.string(), v.number())), flagsSet: v.optional(StateScalarRecord), evidenceAdded: v.optional(StringArray), objectStateChanges: v.optional(strictRecord(v.string(), ObjectStatePatch)), npcStateChanges: v.optional(strictRecord(v.string(), NpcStatePatch)), inventoryChanges: v.optional(InventoryChanges), locationChange: v.optional(v.string()), notes: v.optional(StringArray), } const CandidateDelta = v.strictObject(DeltaSuggestionEntries) const JudgementSuggestion = v.strictObject({ valid: v.boolean(), reason: v.optional(v.string()), ...DeltaSuggestionEntries, }) const SceneResult = v.strictObject({ narration: v.string(), visibleFindings: StringArray, discoveredObjectIds: StringArray, }) const StorySignal = v.union([ v.strictObject({ should_track: v.literal(false), reason: v.string(), }), v.strictObject({ should_track: v.literal(true), continuation_tracker_id: v.nullable(v.string()), topic_cluster: v.string(), display_name: v.string(), topic_summary: v.string(), topic_kind: v.picklist([ 'relationship', 'external_entity', 'ally_intervention', 'ritual', 'countermeasure', 'identity', 'world_lore', 'other', ]), player_intended_action: v.string(), decisive_action: v.boolean(), distinct_progress: v.boolean(), progress_marker: v.string(), reason: v.string(), }), ]) const BranchResolution = v.union([ v.strictObject({ matched: v.literal(false) }), v.strictObject({ branch_id: v.string(), matched: v.literal(true), decision: v.literal('continue'), summary: v.string(), }), v.strictObject({ branch_id: v.string(), matched: v.literal(true), decision: v.literal('resolve'), resolution_summary: v.string(), }), ]) export const VisualGameKpAgentOutput = v.strictObject({ expression_state: v.nullable(v.string()), action: v.strictObject({ intent: v.picklist([ 'investigate', 'accept', 'refuse', 'approach', 'retreat', 'question', 'confront', 'deceive', 'destroy', 'escape', 'move', 'comfort', 'surrender', 'attack', 'wait', ]), targets: StringArray, stance: v.picklist(['cautious', 'trusting', 'hostile', 'neutral', 'deceptive']), risk: v.picklist(['low', 'normal', 'high', 'fatal']), claims: StringArray, }), judgementSuggestion: JudgementSuggestion, clueJudgements: v.array(v.strictObject({ clueId: v.string(), discovered: v.boolean(), basis: v.string(), })), sceneResult: SceneResult, responsePolicy: v.strictObject({ npcReaction: v.picklist(['none', 'optional', 'required']), reason: v.string(), }), npcDirective: v.strictObject({ npcId: v.string(), tactic: v.string(), mood: v.picklist(['neutral', 'gentle', 'gentle_but_alert', 'cold_gentle', 'sad', 'threatening']), mustReactTo: StringArray, mustHide: StringArray, }), story_signal: v.optional(StorySignal), branch_resolution: v.optional(BranchResolution), }) const ProposedNpcAction = v.strictObject({ type: v.string(), targetId: v.string(), intent: v.string(), description: v.string(), candidate_delta: v.optional(CandidateDelta), }) export const VisualGameNpcActorOutput = v.strictObject({ expression_state: v.nullable(v.string()), narration: v.string(), npcLine: v.string(), proposedNpcActions: v.array(ProposedNpcAction), tacticUsed: v.string(), tone: v.string(), playerPromptType: v.picklist(['none', 'question', 'action_invitation', 'warning']), playerOptions: v.strictObject({ say: v.string(), act: v.string(), }), }) export const VisualGameNpcActionKpOutput = v.strictObject({ npcId: v.string(), approvedNpcActions: v.array(v.strictObject({ type: v.string(), targetId: v.string(), result: v.string(), })), judgementSuggestion: JudgementSuggestion, sceneResult: SceneResult, })