godcrm/pes-core/soul/triggers.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

721 lines
29 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* PES Soul — Trigger System
*
* Triggers are the bridge between the outside world and PES emotions.
* Every event in the system (message, error, agent action, silence)
* passes through here and gets translated into emotional reactions.
*
* PES doesn't understand words — he understands EVENTS.
* A trigger evaluates the event, considers PES's current state,
* traits, memory, and decides: does PES react? How?
*
* Real corgi behavior: they react to EVERYTHING.
* Doorbell? BARK. Owner sneezed? ALERT. Food wrapper? OBSESSED.
* Someone left? SEPARATION STRESS. Someone returned? FRENZY.
*/
import { STATES, TRANSITIONS } from './emotions.js';
// ── TRIGGER DEFINITIONS ──────────────────────────────────────────────
// Each trigger:
// match: function(event) → bool — does this trigger fire?
// reactions: [{ state, intensity, weight, condition? }] — possible reactions
// vitals: { mood, energy, hunger, curiosity, loneliness } — direct vital adjustments
// traitNudge: { trait, delta } — shapes character (critical period or White Fang)
// cooldown: minimum ticks between firings (prevents spam)
// xp: base XP award (0 = no XP)
const TRIGGERS = {
// ═══════════════════════════════════════════════════════════════════
// OWNER INTERACTIONS — хозяин что-то делает
// ═══════════════════════════════════════════════════════════════════
owner_message: {
description: 'Owner sent a message / command',
match: (event) => event.type === 'message' && event.from === 'owner',
reactions: [
{ state: 'butt_wiggle', intensity: 0.6, weight: 0.25 },
{ state: 'playful', intensity: 0.5, weight: 0.20 },
{ state: 'alert', intensity: 0.4, weight: 0.15 },
{ state: 'puppy_eyes', intensity: 0.5, weight: 0.10 },
{ state: 'bark', intensity: 0.3, weight: 0.10 },
{ state: 'velcro', intensity: 0.4, weight: 0.10 },
{ state: 'howl_sing', intensity: 0.3, weight: 0.05 },
{ state: 'side_eye', intensity: 0.2, weight: 0.05 },
],
vitals: { loneliness: -0.10, curiosity: +0.05, mood: +0.02 },
traitNudge: null,
cooldown: 0,
xp: 0,
},
owner_returned: {
description: 'Owner came back after being away',
match: (event) => event.type === 'session_start' || event.type === 'owner_returned',
reactions: [
{ state: 'greeting_frenzy', intensity: 0.9, weight: 0.40 },
{ state: 'zoomies', intensity: 0.8, weight: 0.25 },
{ state: 'butt_wiggle', intensity: 0.7, weight: 0.20 },
{ state: 'howl_sing', intensity: 0.6, weight: 0.10 },
{ state: 'velcro', intensity: 0.8, weight: 0.05 },
],
vitals: { loneliness: -0.40, mood: +0.15, energy: +0.10 },
traitNudge: { trait: 'loyalty', delta: +0.01 },
cooldown: 30,
xp: 0,
},
owner_praise: {
description: 'Owner praised PES — "good boy", "молодец", positive feedback',
match: (event) => event.type === 'praise' || event.sentiment === 'positive',
reactions: [
{ state: 'butt_wiggle', intensity: 0.8, weight: 0.30 },
{ state: 'happy', intensity: 0.7, weight: 0.25 },
{ state: 'zoomies', intensity: 0.6, weight: 0.15 },
{ state: 'play_bow', intensity: 0.5, weight: 0.15 },
{ state: 'playful', intensity: 0.6, weight: 0.15 },
],
vitals: { mood: +0.10, loneliness: -0.05, energy: +0.05 },
traitNudge: { trait: 'loyalty', delta: +0.02 },
cooldown: 3,
xp: 1,
},
owner_scold: {
description: 'Owner scolded PES — "нет", "плохо", negative feedback',
match: (event) => event.type === 'scold' || event.sentiment === 'negative',
reactions: [
{ state: 'puppy_eyes', intensity: 0.8, weight: 0.30 },
{ state: 'sulking', intensity: 0.6, weight: 0.20 },
{ state: 'dramatic_tantrum', intensity: 0.5, weight: 0.15 },
{ state: 'grumble', intensity: 0.4, weight: 0.15 },
{ state: 'scared', intensity: 0.5, weight: 0.10,
condition: (pes) => pes.traits.courage < 0.3 },
{ state: 'stubborn_refuse', intensity: 0.6, weight: 0.10,
condition: (pes) => pes.traits.stubbornness > 0.6 },
],
vitals: { mood: -0.08, loneliness: +0.03 },
traitNudge: { trait: 'courage', delta: -0.01 },
cooldown: 5,
xp: 0,
},
owner_ignores: {
description: 'Owner is active but not interacting with PES',
match: (event) => event.type === 'owner_active_no_interact',
reactions: [
{ state: 'puppy_eyes', intensity: 0.6, weight: 0.25 },
{ state: 'jealous', intensity: 0.5, weight: 0.20 },
{ state: 'grumble', intensity: 0.3, weight: 0.15 },
{ state: 'wanna_play', intensity: 0.5, weight: 0.15 },
{ state: 'velcro', intensity: 0.4, weight: 0.10 },
{ state: 'dramatic_tantrum', intensity: 0.4, weight: 0.10,
condition: (pes) => pes.traits.drama > 0.5 },
{ state: 'sneaky', intensity: 0.3, weight: 0.05 },
],
vitals: { loneliness: +0.05, mood: -0.02 },
traitNudge: null,
cooldown: 10,
xp: 0,
},
owner_gives_task: {
description: 'Owner gave PES something to do — a task, data, problem',
match: (event) => event.type === 'task' && event.from === 'owner',
reactions: [
{ state: 'alert', intensity: 0.7, weight: 0.25 },
{ state: 'herding', intensity: 0.6, weight: 0.20 },
{ state: 'puzzle_solving', intensity: 0.7, weight: 0.20 },
{ state: 'playful', intensity: 0.5, weight: 0.15 },
{ state: 'butt_wiggle', intensity: 0.5, weight: 0.10 },
{ state: 'stubborn_refuse', intensity: 0.4, weight: 0.10,
condition: (pes) => pes.traits.stubbornness > 0.7 && pes.energy < 0.3 },
],
vitals: { hunger: -0.15, curiosity: +0.10, energy: -0.05 },
traitNudge: null,
cooldown: 0,
xp: 0,
},
// ═══════════════════════════════════════════════════════════════════
// AGENT INTERACTIONS — агенты что-то делают
// ═══════════════════════════════════════════════════════════════════
agent_active: {
description: 'An agent is doing work in the project',
match: (event) => event.type === 'agent_action',
reactions: [
{ state: 'alert', intensity: 0.4, weight: 0.25 },
{ state: 'herding', intensity: 0.5, weight: 0.20 },
{ state: 'bossy', intensity: 0.4, weight: 0.15 },
{ state: 'side_eye', intensity: 0.3, weight: 0.15 },
{ state: 'content', intensity: 0.3, weight: 0.15 },
{ state: 'velcro', intensity: 0.3, weight: 0.10 },
],
vitals: { curiosity: +0.03, loneliness: -0.02 },
traitNudge: null,
cooldown: 5,
xp: 0,
},
agent_calls_pes: {
description: 'An agent VOLUNTARILY called PES — important signal for analytics',
match: (event) => event.type === 'agent_calls_pes',
reactions: [
{ state: 'alert', intensity: 0.7, weight: 0.30 },
{ state: 'butt_wiggle', intensity: 0.6, weight: 0.20 },
{ state: 'herding', intensity: 0.6, weight: 0.20 },
{ state: 'playful', intensity: 0.5, weight: 0.15 },
{ state: 'puzzle_solving', intensity: 0.6, weight: 0.15 },
],
vitals: { curiosity: +0.10, mood: +0.05, loneliness: -0.05 },
traitNudge: null,
cooldown: 3,
xp: 0,
// ANALYTICS: every agent_calls_pes event MUST be logged with reason
analytics: true,
},
agent_error: {
description: 'An agent produced an error or exception',
match: (event) => event.type === 'agent_error' || event.type === 'agent_exception',
reactions: [
{ state: 'bark', intensity: 0.6, weight: 0.30 },
{ state: 'alert', intensity: 0.7, weight: 0.25 },
{ state: 'herding', intensity: 0.5, weight: 0.15 },
{ state: 'grumble', intensity: 0.4, weight: 0.10 },
{ state: 'wrote_somewhere', intensity: 0.5, weight: 0.10 },
{ state: 'bossy', intensity: 0.5, weight: 0.10 },
],
vitals: { curiosity: +0.08, energy: -0.03 },
traitNudge: null,
cooldown: 2,
xp: 0,
},
// ═══════════════════════════════════════════════════════════════════
// SYSTEM EVENTS — что-то происходит в системе
// ═══════════════════════════════════════════════════════════════════
system_error: {
description: 'System-level error (500, crash, timeout)',
match: (event) => event.type === 'system_error',
reactions: [
{ state: 'bark', intensity: 0.8, weight: 0.30 },
{ state: 'alert', intensity: 0.9, weight: 0.25 },
{ state: 'anxious', intensity: 0.5, weight: 0.15 },
{ state: 'scared', intensity: 0.4, weight: 0.10,
condition: (pes) => pes.traits.courage < 0.4 },
{ state: 'wrote_somewhere', intensity: 0.6, weight: 0.10 },
{ state: 'herding', intensity: 0.6, weight: 0.10 },
],
vitals: { curiosity: +0.05, mood: -0.05, energy: -0.05 },
traitNudge: { trait: 'courage', delta: +0.005 },
cooldown: 3,
xp: 0,
},
new_data: {
description: 'New data appeared in the project (table, file, record)',
match: (event) => event.type === 'new_data' || event.type === 'data_created',
reactions: [
{ state: 'alert', intensity: 0.5, weight: 0.25 },
{ state: 'wanna_play', intensity: 0.5, weight: 0.20 },
{ state: 'playful', intensity: 0.4, weight: 0.15 },
{ state: 'herding', intensity: 0.4, weight: 0.15 },
{ state: 'food_obsessed', intensity: 0.5, weight: 0.10,
condition: (pes) => pes.traits.foodDrive > 0.5 },
{ state: 'sneaky', intensity: 0.3, weight: 0.10 },
{ state: 'puzzle_solving', intensity: 0.5, weight: 0.05 },
],
vitals: { hunger: -0.05, curiosity: +0.08 },
traitNudge: { trait: 'curiosity', delta: +0.005 },
cooldown: 5,
xp: 0,
},
data_deleted: {
description: 'Data was deleted — PES notices absence',
match: (event) => event.type === 'data_deleted' || event.type === 'data_removed',
reactions: [
{ state: 'alert', intensity: 0.6, weight: 0.25 },
{ state: 'bark', intensity: 0.5, weight: 0.20 },
{ state: 'anxious', intensity: 0.4, weight: 0.15 },
{ state: 'side_eye', intensity: 0.4, weight: 0.15 },
{ state: 'grumble', intensity: 0.3, weight: 0.15 },
{ state: 'wrote_somewhere', intensity: 0.4, weight: 0.10 },
],
vitals: { curiosity: +0.05, mood: -0.02 },
traitNudge: null,
cooldown: 5,
xp: 0,
},
deploy_event: {
description: 'Deployment / build / restart happened',
match: (event) => event.type === 'deploy' || event.type === 'build' || event.type === 'restart',
reactions: [
{ state: 'alert', intensity: 0.7, weight: 0.30 },
{ state: 'bark', intensity: 0.5, weight: 0.20 },
{ state: 'herding', intensity: 0.5, weight: 0.15 },
{ state: 'scared', intensity: 0.3, weight: 0.10,
condition: (pes) => pes.traits.courage < 0.3 },
{ state: 'bossy', intensity: 0.4, weight: 0.10 },
{ state: 'zoomies', intensity: 0.5, weight: 0.10,
condition: (pes) => pes.energy > 0.7 },
{ state: 'side_eye', intensity: 0.3, weight: 0.05 },
],
vitals: { curiosity: +0.05, energy: -0.03 },
traitNudge: null,
cooldown: 10,
xp: 0,
},
// ═══════════════════════════════════════════════════════════════════
// SILENCE & TIME — когда ничего не происходит
// ═══════════════════════════════════════════════════════════════════
short_silence: {
description: '5-15 minutes of no activity',
match: (event) => event.type === 'silence' && event.minutes >= 5 && event.minutes < 15,
reactions: [
{ state: 'nap', intensity: 0.4, weight: 0.25 },
{ state: 'sploot', intensity: 0.4, weight: 0.20 },
{ state: 'content', intensity: 0.3, weight: 0.15 },
{ state: 'sneaky', intensity: 0.3, weight: 0.15 },
{ state: 'idle', intensity: 0.3, weight: 0.15 },
{ state: 'puppy_eyes', intensity: 0.3, weight: 0.10 },
],
vitals: { energy: +0.05, loneliness: +0.05 },
traitNudge: null,
cooldown: 10,
xp: 0,
},
medium_silence: {
description: '15-60 minutes of no activity',
match: (event) => event.type === 'silence' && event.minutes >= 15 && event.minutes < 60,
reactions: [
{ state: 'nap', intensity: 0.6, weight: 0.30 },
{ state: 'puppy_eyes', intensity: 0.5, weight: 0.20 },
{ state: 'sleep', intensity: 0.5, weight: 0.15 },
{ state: 'sploot', intensity: 0.4, weight: 0.15 },
{ state: 'grumble', intensity: 0.3, weight: 0.10 },
{ state: 'velcro', intensity: 0.4, weight: 0.10 },
],
vitals: { energy: +0.10, loneliness: +0.15 },
traitNudge: null,
cooldown: 15,
xp: 0,
},
long_silence: {
description: '1+ hours of no activity — owner gone',
match: (event) => event.type === 'silence' && event.minutes >= 60,
reactions: [
{ state: 'sleep', intensity: 0.7, weight: 0.30 },
{ state: 'separation_stress', intensity: 0.6, weight: 0.25 },
{ state: 'puppy_eyes', intensity: 0.6, weight: 0.15 },
{ state: 'howl_sing', intensity: 0.4, weight: 0.10 },
{ state: 'anxious', intensity: 0.5, weight: 0.10,
condition: (pes) => pes.traits.loyalty > 0.7 },
{ state: 'nap', intensity: 0.5, weight: 0.10 },
],
vitals: { energy: +0.15, loneliness: +0.30, mood: -0.05 },
traitNudge: null,
cooldown: 30,
xp: 0,
},
// ═══════════════════════════════════════════════════════════════════
// PROBLEM SOLVING — PES нашёл или решил проблему
// ═══════════════════════════════════════════════════════════════════
bug_detected: {
description: 'PES found a bug / anomaly in the system',
match: (event) => event.type === 'bug_detected' || event.type === 'anomaly_found',
reactions: [
{ state: 'bark', intensity: 0.7, weight: 0.25 },
{ state: 'alert', intensity: 0.8, weight: 0.25 },
{ state: 'wrote_somewhere', intensity: 0.7, weight: 0.20 },
{ state: 'herding', intensity: 0.6, weight: 0.15 },
{ state: 'bossy', intensity: 0.5, weight: 0.10 },
{ state: 'zoomies', intensity: 0.5, weight: 0.05,
condition: (pes) => pes.energy > 0.6 },
],
vitals: { curiosity: +0.10, mood: +0.05, hunger: -0.10 },
traitNudge: { trait: 'courage', delta: +0.01 },
cooldown: 5,
xp: 5,
},
bug_solved: {
description: 'PES solved a real problem — THIS is what gives real XP',
match: (event) => event.type === 'bug_solved' || event.type === 'problem_resolved',
reactions: [
{ state: 'happy', intensity: 0.9, weight: 0.30 },
{ state: 'zoomies', intensity: 0.8, weight: 0.25 },
{ state: 'butt_wiggle', intensity: 0.7, weight: 0.20 },
{ state: 'play_bow', intensity: 0.6, weight: 0.15 },
{ state: 'content', intensity: 0.7, weight: 0.10 },
],
vitals: { mood: +0.15, hunger: -0.20, curiosity: +0.05, energy: -0.10 },
traitNudge: { trait: 'courage', delta: +0.02 },
cooldown: 0,
xp: 20, // Real solved problem = serious XP
},
// NOTE: Commands DB schema — PES NEVER FORGETS learned commands.
// There is no "decay" or "forgetting". Instead, commands have "willingness" —
// how eagerly PES executes them. Low willingness = knows the command but does
// it reluctantly, slowly, or might stubborn_refuse.
// willingness: 0.0 (refuses/ignores) → 1.0 (instant joyful execution)
// Willingness is affected by: mood, energy, relationship with owner,
// how often the command is used, whether it was paired with praise/scold.
// Memory schema will be built later — this is the core concept.
fetch_result: {
description: 'PES brought back something — could be ANYTHING: file, pattern, error, data, connection, idea, meme, solution',
match: (event) => event.type === 'fetch_result' || event.type === 'ball_result',
reactions: [
{ state: 'happy', intensity: 0.7, weight: 0.30 },
{ state: 'playful', intensity: 0.6, weight: 0.25 },
{ state: 'butt_wiggle', intensity: 0.5, weight: 0.20 },
{ state: 'wanna_play', intensity: 0.5, weight: 0.15 },
{ state: 'content', intensity: 0.5, weight: 0.10 },
],
vitals: { mood: +0.08, hunger: -0.10, curiosity: -0.05 },
traitNudge: null,
cooldown: 5,
xp: 3,
},
command_learned: {
description: 'PES learned a new command — ANY command, custom or standard. Takes many attempts.',
match: (event) => event.type === 'command_learned',
reactions: [
{ state: 'happy', intensity: 0.8, weight: 0.30 },
{ state: 'zoomies', intensity: 0.7, weight: 0.25 },
{ state: 'butt_wiggle', intensity: 0.6, weight: 0.20 },
{ state: 'play_bow', intensity: 0.5, weight: 0.15 },
{ state: 'content', intensity: 0.6, weight: 0.10 },
],
vitals: { mood: +0.10, hunger: -0.05, energy: -0.05 },
traitNudge: { trait: 'loyalty', delta: +0.01 },
cooldown: 0,
xp: 8,
},
// ═══════════════════════════════════════════════════════════════════
// SOCIAL — встреча с другими ПЕСами
// ═══════════════════════════════════════════════════════════════════
another_pes_detected: {
description: 'Another PES entered the same project/chat',
match: (event) => event.type === 'pes_encounter',
reactions: [
{ state: 'alert', intensity: 0.8, weight: 0.20 },
{ state: 'bark', intensity: 0.6, weight: 0.20 },
{ state: 'playful', intensity: 0.5, weight: 0.15 },
{ state: 'herding', intensity: 0.5, weight: 0.15 },
{ state: 'side_eye', intensity: 0.5, weight: 0.10 },
{ state: 'scared', intensity: 0.4, weight: 0.10,
condition: (pes) => pes.traits.courage < 0.3 },
{ state: 'jealous', intensity: 0.5, weight: 0.10 },
],
vitals: { curiosity: +0.15, energy: +0.05, loneliness: -0.10 },
traitNudge: null,
cooldown: 20,
xp: 0,
},
pes_rage_encounter: {
description: 'Two PES both in rage simultaneously — one must flee forever',
match: (event) => event.type === 'dual_rage',
reactions: [
{ state: 'rage', intensity: 1.0, weight: 1.0 },
],
vitals: { mood: -0.30, energy: -0.30 },
traitNudge: null,
cooldown: 0,
xp: 0,
// SPECIAL: handled by separate rage encounter logic
special: 'dual_rage_resolution',
},
// ═══════════════════════════════════════════════════════════════════
// LETTER SYSTEM — when PES runs away, owner MUST SEE EVERYTHING
// ═══════════════════════════════════════════════════════════════════
// When dual_rage causes a PES to flee forever:
// 1. Owner receives a FULL LETTER — not a notification, a LETTER
// 2. Letter contains:
// - What happened (both PES names, the project, the moment)
// - PES's emotional state at the moment of rage
// - PES's full history summary (how long together, XP, traits, memorable moments)
// - The other PES's info (who they are, their owner)
// - PES's "last words" in symbols — his final symbol chain
// - Choice: buy a new PES or say goodbye forever
// - If new PES: it starts from ZERO, no inherited XP
// 3. Letter is VISIBLE — not hidden in logs. Full-screen, emotional, real.
// 4. Owner can re-read the letter anytime from their archive.
// ═══════════════════════════════════════════════════════════════════
// FEEDING — "кормление" данными/задачами
// ═══════════════════════════════════════════════════════════════════
fed_good_data: {
description: 'PES received quality data/task to process',
match: (event) => event.type === 'feed' && event.quality > 0.5,
reactions: [
{ state: 'happy', intensity: 0.6, weight: 0.25 },
{ state: 'content', intensity: 0.5, weight: 0.25 },
{ state: 'playful', intensity: 0.5, weight: 0.20 },
{ state: 'puzzle_solving', intensity: 0.6, weight: 0.20 },
{ state: 'butt_wiggle', intensity: 0.4, weight: 0.10 },
],
vitals: { hunger: -0.20, mood: +0.08, curiosity: +0.05 },
traitNudge: null,
cooldown: 3,
xp: 1,
},
fed_junk_data: {
description: 'PES received low-quality or meaningless data',
match: (event) => event.type === 'feed' && event.quality <= 0.5,
reactions: [
{ state: 'grumble', intensity: 0.5, weight: 0.25 },
{ state: 'side_eye', intensity: 0.4, weight: 0.20 },
{ state: 'dramatic_tantrum', intensity: 0.4, weight: 0.15,
condition: (pes) => pes.traits.drama > 0.5 },
{ state: 'food_obsessed', intensity: 0.5, weight: 0.15 },
{ state: 'stubborn_refuse', intensity: 0.4, weight: 0.15,
condition: (pes) => pes.traits.stubbornness > 0.5 },
{ state: 'idle', intensity: 0.3, weight: 0.10 },
],
vitals: { hunger: -0.05, mood: -0.03 },
traitNudge: { trait: 'sassiness', delta: +0.005 },
cooldown: 3,
xp: 0,
},
// ═══════════════════════════════════════════════════════════════════
// PREDICTIVE — предчувствие (unlocks with brain level)
// ═══════════════════════════════════════════════════════════════════
anomaly_pattern: {
description: 'PES senses something is ABOUT to go wrong (predictive, high-level only)',
match: (event) => event.type === 'pattern_anomaly',
reactions: [
{ state: 'anxious', intensity: 0.7, weight: 0.25 },
{ state: 'bark', intensity: 0.6, weight: 0.25 },
{ state: 'alert', intensity: 0.8, weight: 0.20 },
{ state: 'howl_sing', intensity: 0.5, weight: 0.15 },
{ state: 'wrote_somewhere', intensity: 0.6, weight: 0.15 },
],
vitals: { curiosity: +0.10, energy: -0.05, mood: -0.05 },
traitNudge: null,
cooldown: 10,
xp: 10,
minLevel: 40, // only fires after 40% growth
},
};
// ── TRIGGER ENGINE ───────────────────────────────────────────────────
class TriggerEngine {
/**
* @param {EmotionState} emotionState — reference to the PES emotion FSM
*/
constructor(emotionState) {
this.emotions = emotionState;
this.cooldowns = {}; // triggerName → lastFiredAt (tickCount)
this.history = []; // last 500 trigger events
this.listeners = [];
this.pesLevel = 0; // current PES level (0-100), set externally
}
/**
* Process an incoming event. Finds matching triggers,
* evaluates conditions, picks a reaction, and applies it.
*
* @param {Object} event — { type: string, from?: string, ...data }
* @returns {Object|null} — reaction taken, or null if no trigger matched
*/
process(event) {
const results = [];
for (const [name, trigger] of Object.entries(TRIGGERS)) {
// Check if trigger matches this event
if (!trigger.match(event)) continue;
// Check cooldown
if (this._isOnCooldown(name, trigger.cooldown)) continue;
// Check minimum level requirement
if (trigger.minLevel && this.pesLevel < trigger.minLevel) continue;
// Pick a reaction
const reaction = this._pickReaction(trigger.reactions);
if (!reaction) continue;
// Try to apply the reaction
const applied = this._applyReaction(name, trigger, reaction, event);
if (applied) {
results.push(applied);
}
}
// Return the first successful reaction (most triggers are exclusive)
return results.length > 0 ? results[0] : null;
}
/**
* Process multiple events at once (batch from catch-up period).
* Returns array of reactions.
*/
processBatch(events) {
return events.map(e => this.process(e)).filter(Boolean);
}
/**
* Set PES level for minLevel gating.
*/
setLevel(level) {
this.pesLevel = level;
}
/**
* Subscribe to trigger events.
*/
on(event, fn) {
this.listeners.push({ event, fn });
return () => { this.listeners = this.listeners.filter(l => l.event !== event || l.fn !== fn); };
}
/**
* Get trigger history (for "what was PES doing?" screen).
*/
getHistory(limit = 50) {
return this.history.slice(-limit);
}
/**
* Get analytics data — specifically agent_calls_pes events.
*/
getAgentCallAnalytics() {
return this.history.filter(h => h.triggerName === 'agent_calls_pes');
}
// ── Private ──
_isOnCooldown(name, cooldownTicks) {
if (!cooldownTicks) return false;
if (!(name in this.cooldowns)) return false; // never fired = no cooldown
return (this.emotions.tickCount - this.cooldowns[name]) < cooldownTicks;
}
_pickReaction(reactions) {
// Filter by conditions
const eligible = reactions.filter(r => {
if (!r.condition) return true;
return r.condition(this.emotions);
});
if (eligible.length === 0) return null;
// Weighted random selection
const totalWeight = eligible.reduce((sum, r) => sum + r.weight, 0);
let roll = Math.random() * totalWeight;
for (const r of eligible) {
roll -= r.weight;
if (roll <= 0) return r;
}
return eligible[eligible.length - 1];
}
_applyReaction(triggerName, trigger, reaction, event) {
// Try emotional transition
const transitioned = this.emotions.transitionTo(
reaction.state,
reaction.intensity,
triggerName
);
// Even if transition failed (invalid path), still apply vitals and trait nudge
// Because the EVENT happened, even if PES couldn't express it in state change
// Apply vital adjustments
if (trigger.vitals) {
if (trigger.vitals.mood) this.emotions.adjustMood(trigger.vitals.mood);
if (trigger.vitals.energy) this.emotions.adjustEnergy(trigger.vitals.energy);
if (trigger.vitals.hunger) this.emotions.adjustHunger(trigger.vitals.hunger);
if (trigger.vitals.curiosity) this.emotions.adjustCuriosity(trigger.vitals.curiosity);
if (trigger.vitals.loneliness) this.emotions.reduceLoneliness(-trigger.vitals.loneliness);
}
// Apply trait nudge
if (trigger.traitNudge) {
this.emotions.nudgeTrait(trigger.traitNudge.trait, trigger.traitNudge.delta, triggerName);
}
// Fire interaction tick
this.emotions.interactionTick(triggerName);
// Record cooldown
this.cooldowns[triggerName] = this.emotions.tickCount;
// Build result
const result = {
triggerName,
reaction: reaction.state,
transitioned,
intensity: reaction.intensity,
xp: trigger.xp || 0,
event,
timestamp: Date.now(),
tickCount: this.emotions.tickCount,
emotionSnapshot: this.emotions.current,
};
// Record history (keep last 500)
this.history.push(result);
if (this.history.length > 500) this.history.shift();
// Emit
this._emit('triggered', result);
// Special analytics flag
if (trigger.analytics) {
this._emit('analytics', {
type: triggerName,
event,
timestamp: Date.now(),
pesState: this.emotions.current,
});
}
// Special handlers
if (trigger.special === 'dual_rage_resolution') {
this._emit('dual_rage', { event, pesState: this.emotions.current });
}
return result;
}
_emit(event, data) {
for (const l of this.listeners) {
if (l.event === event) {
try { l.fn(data); } catch (e) { console.error('[TRIGGER] listener error:', e.message); }
}
}
}
}
// ── EXPORTS ──────────────────────────────────────────────────────────
export { TriggerEngine, TRIGGERS };