Skip to content

Emotion

How NPCs feel — derived state, not stored. Always fresh, always contextual.

Core Idea

Emotion in AI World Sim is derived, not stored.

This means an NPC's emotion is always computed fresh from their current needs + recent events. There's no "anger level" that decays over time. There's only "given what just happened and what state they're in, how do they feel right now?"

This produces more natural behavior than storing emotions as state variables.


Emotion Model

Discrete Labels

NPCs feel one of these emotions at any moment:

ts
type Emotion =
  | 'happy'
  | 'sad'
  | 'angry'
  | 'anxious'
  | 'lonely'
  | 'content'
  | 'frustrated'
  | 'excited'
  | 'bored'
  | 'neutral';

Plus an Intensity

ts
type EmotionalState = {
  label: Emotion;
  intensity: number;  // 0-1
};

How Emotion is Derived

Step 1 — Base Score from Needs

ts
function emotionFromNeeds(needs: Needs): EmotionScore {
  let score = { happy: 0, sad: 0, anxious: 0, ... };

  if (needs.hunger > 80) score.frustrated += 0.6;
  if (needs.energy < 20) score.bored += 0.4;
  if (needs.social > 70) score.lonely += 0.7;
  if (needs.health < 30) score.sad += 0.5;
  if (needs.fun > 70) score.bored += 0.5;

  return score;
}

Step 2 — Event Modifier

Recent events shift the score:

ts
const eventModifiers = {
  'argument': { angry: +0.8, anxious: +0.3 },
  'good_news': { happy: +0.7, excited: +0.3 },
  'lost_money': { sad: +0.4, anxious: +0.6 },
  'met_friend': { happy: +0.6, content: +0.3 },
  'got_praised': { happy: +0.5 },
  'got_insulted': { angry: +0.6, sad: +0.3 },
};

Step 3 — Personality Modifier

ts
function personalityModifier(emotion: Emotion, personality: Personality): number {
  if (emotion === 'anxious' && personality.neuroticism > 0.7) return 1.5;
  if (emotion === 'happy' && personality.neuroticism > 0.7) return 0.5;
  if (emotion === 'lonely' && personality.extraversion < 0.3) return 1.4;
  return 1.0;
}

Step 4 — Memory Recall

Sometimes the system recalls a relevant memory that colors the emotion:

Margaret just heard Tom's name. She recalls the argument they had last week. Her current emotion is amplified toward frustrated + sad.

Step 5 — LLM Refinement (optional)

For ambiguous cases, the LLM is consulted to choose between close-scoring emotions.


What Emotion Affects

Emotion influences:

SystemHow
DecisionBias toward emotion-congruent actions
ConversationTone of voice, word choice
DiaryOverall mood of the entry
RelationshipsEasier to offend when angry, etc.

Emotion Decay

Emotion does not persist across ticks by default. It's re-derived each tick.

However, strong emotions (intensity > 0.8) leave a memory trace:

ts
if (state.intensity > 0.8) {
  await memory.write({
    type: 'emotion',
    content: `Felt very ${state.label} because ${event.summary}`,
    importance: 0.9,
  });
}

This memory can then re-trigger the emotion if similar contexts arise.


Mood vs Emotion

We make a careful distinction:

  • Emotion — current, derived, lasts one tick
  • Mood — longer-term baseline, persists across ticks, computed from rolling average of recent emotions
ts
function computeMood(npc: NPC): Emotion {
  // Average last 24 hours of emotions
  // Weight recent ones higher
  // Return dominant mood
}

Mood is what makes an NPC generally grumpy or generally cheerful over weeks.


Emotion in Diary

Diary generation takes the NPC's average emotion over the day as the dominant tone:

"It was a content day. Nothing special happened, but nothing bad either."

If the day's emotion was unstable, the diary may reflect that:

"Started anxious, ended happy. Eleanor came by in the afternoon and we talked for an hour."


Failure Modes

The emotion system has been tuned to avoid:

  • Constant drama — NPCs feeling extreme emotions every tick
  • Emotional flatness — NPCs feeling nothing ever
  • Incoherent swings — wild mood changes with no cause

The baseline assumption is that most NPCs feel mostly content or neutral on most days. Strong emotions are reserved for actual events.

Last updated:

Released under the MIT License.