# Messaging & Communications Overhaul Design

**Date:** 2026-02-26
**Status:** Approved
**Scope:** Backend (server/src/) + iOS (ios/lichunWebsocket/)

## Goal

Make conversations feel real. Affinity should swing dramatically based on what you say. NPCs should write long messages when the moment warrants it. Characters should text first sometimes. Time of day should matter.

## 1. Dynamic Affinity System

### Problem
Every message gets a flat +5/-5/0 affinity change regardless of emotional weight. "I hate you" and "meh" both produce -5.

### Solution
Replace flat sentiment scoring with AI-determined affinity deltas in the range **-50 to +30** (asymmetric: easier to destroy trust than build it).

The AI returns `affinityDelta` (integer) with every response. The prompt includes scoring guidelines:

| Player Message Type | Delta Range |
|---|---|
| Casual positive ("lol", "nice") | +1 to +3 |
| Engaged conversation (sharing stories) | +3 to +8 |
| Deep emotional sharing (fears, dreams) | +5 to +15 |
| Compliments / affection | +8 to +20 |
| Romantic (appropriate context) | +15 to +30 |
| Mild rudeness ("whatever") | -3 to -8 |
| Dismissive / hurtful | -15 to -30 |
| Hostile / cruel ("I hate you") | -30 to -50 |
| Inappropriate context (unwanted advances) | -20 to -40 |

### Critical: Context-Dependent Scoring
The delta MUST be scored from the NPC's perspective given the current relationship state. Examples:
- "I love you" from a committed partner (affinity 85) = +20
- "I love you" from an acquaintance (affinity 15) = -15 (creepy)
- An apology after a fight = high positive (proportional to damage done)
- Ignoring a serious question = more negative for close relationships than distant ones

### Implementation
- Change AI response schema: `{message, sentiment}` → `{message, sentiment, affinityDelta}`
- Add scoring guidelines and context instructions to system prompt in `ai_response.ts`
- Tool calling mode: extend `affinityChange` field to use full -50/+30 range with better instructions
- Clamp result: `Math.max(0, Math.min(100, currentAffinity + delta))`
- Send actual delta to iOS in message data for display

### Files
- `server/src/events/conversations/ai_response.ts` — prompt changes, response parsing
- `server/src/handlers/conversations.ts` — affinity application logic

## 2. Context-Dependent Message Lengths

### Problem
Hard word limits cap NPC responses: low=10-20 words, medium=20-40, high=40-65. A breakup conversation gets the same ceiling as casual chat.

### Solution
Replace word-count hints with situation-driven length scaling:

| Context | Words | maxTokens | Triggers |
|---|---|---|---|
| Quick reactions | 1-10 | 100 | One-word player input, emoji, "lol" |
| Casual chat | 10-30 | 300 | Small talk, brief updates |
| Normal conversation | 30-60 | 600 | Stories, opinions, plans |
| Emotional / deep | 60-150 | 1200 | "why", "how do you feel", relationship talks |
| Major moments | 150-300+ | 2000 | Breakups, confessions, life news, confrontations |

### Context Detection
Enhance `detectVerbosityLevel()` to detect major moments:
- Player mentions: breakup, death, marriage, pregnancy, cheating → major
- Relationship just changed status → major
- Player asks deep questions ("why", "what do we do") → emotional
- Player sends one-word responses → NPC matches casualness
- First conversation with a character → medium (getting to know)

### Prompt Changes
Replace rigid word counts with natural-language guidance:
- Quick: "React briefly, like a quick text"
- Casual: "Keep it short and natural"
- Normal: "Respond naturally with a full thought"
- Emotional: "Take your time. This deserves a thoughtful, complete response"
- Major: "This is a significant moment. Write as much as the situation demands. Don't cut yourself short."

### Files
- `server/src/events/conversations/ai_response.ts` — verbosity detection, prompt templates, maxTokens

## 3. NPC-Initiated Messages

### Problem
NPCs never text first. The player must always initiate every conversation.

### Solution
NPCs send messages based on realistic triggers, throttled to prevent spam.

### Triggers

| Trigger | Who | Cooldown | Condition |
|---|---|---|---|
| Birthday | Friends (>40 affinity), family, partner | 1/year | Always known |
| Long time gap (3+ game days) | High-affinity (>60) | 1 per gap | Affinity threshold |
| After a fight (affinity dropped >15) | The hurt character | 1 game day delay | Automatic |
| Morning text | Partner/spouse, affinity >70 | Max 1/week | Relationship type |
| Shared memory | Characters with extracted facts | Max 1/2 weeks | CharacterMemory has facts |

### What NPCs Don't Know
NPCs ONLY reference information they could realistically know:
- **Always know:** birthdays, seasons/holidays, shared history from conversations
- **Only know if told:** job changes, new relationships, health issues, financial status
- **Source:** CharacterMemory fact extraction system (already exists, runs every 10 messages)

### Throttling
- Max **2 NPC-initiated messages per game day** across all characters
- Each trigger has its own cooldown (see table)
- Don't fire during deep night (2am-6am) unless the character is a night owl

### Implementation
- New function: `checkNPCInitiatedMessages(player, session)` in a new file
- Called from `processHourTick()` in `PlayerSession.ts`
- Generates short AI message using character context + trigger reason
- Creates conversation entry and sends to client as `conversationEvent`
- Track last-initiated timestamps per character to enforce cooldowns

### Files
- `server/src/events/conversations/npc_initiative.ts` (new)
- `server/src/game/PlayerSession.ts` — hook into processHourTick

## 4. Full Time Awareness

### Problem
Time is in the AI prompt context but NPCs don't meaningfully react to it.

### Solution
Add time-of-day behavioral directives to the system prompt:

| Time | NPC Behavior |
|---|---|
| Morning (6am-11am) | Greetings natural. Alert, responsive. |
| Afternoon (11am-5pm) | Normal. Working NPCs may be brief ("at work rn"). |
| Evening (5pm-10pm) | Most engaged. Longer conversations natural. Activity invitations. |
| Late night (10pm-2am) | Close friends/partners fine. Acquaintances note "it's late". |
| Deep night (2am-6am) | Very close people only. Others: "why are you texting at 3am?" Low-affinity may not respond (delay to morning). |

### Delayed Response Mechanic
For deep-night messages to non-close characters:
- Don't generate AI response immediately
- Queue a response for the next morning (6-8am)
- Send with time gap indicator: "[The next morning]"
- Character references the late message: "saw your text from last night..."

### Implementation
- Add time-awareness section to system prompt in `ai_response.ts`
- New helper: `getTimeAwarenessDirective(hour, affinity, relationshipType)`
- For delayed responses: queue mechanism in conversation handler
- NPC-initiated messages respect appropriate hours per character

### Files
- `server/src/events/conversations/ai_response.ts` — prompt additions
- `server/src/handlers/conversations.ts` — delayed response logic

## 5. Live iOS Feedback

### Affinity Changes
- Affinity pill in ChatHeaderCard **pulses** on change with floating `+N`/`-N` animation
- Green glow for positive, red flash for negative
- Large swings (>15 points): brief toast "Your relationship with [name] has changed significantly"
- Animate affinity number transition (old → new)

### Energy Consumption
- Energy pill **flashes** when player sends message, shows `-10`
- Below 20 energy: pill turns amber as warning
- Keeps existing "Not enough energy" toast when at 0

### Per-Message Indicators
- Replace flat "+5"/"-5" badges with **actual affinity delta** from AI
- Show real numbers: "+12", "-8", "+1", "-35"
- Scale badge size/color by magnitude:
  - Small positive (+1 to +5): subtle green, small text
  - Medium positive (+6 to +15): green, normal text
  - Large positive (+16 to +30): bright green, bold, heart animation
  - Small negative (-1 to -8): subtle orange
  - Medium negative (-9 to -20): red
  - Large negative (-21 to -50): dark red, bold, cracked heart icon

### Time Gap Indicators in UI
- Display time gap labels between messages when significant gaps exist
- Already generated by backend (`getTimeGapIndicator`) — surface them in the iOS message list
- Gray centered text: "[A bit later]", "[The next morning]", "[3 days later]"

### Implementation
- Extend `ConversationMessage` model to include `affinityDelta` field
- Add animation modifiers to ChatHeaderCard affinity pill
- Enhance CozyMessageBubble sentiment indicator to use real deltas
- Add time gap separator view between messages in ChatView

### Files
- `ios/.../Core/Models/Conversation.swift` — add affinityDelta to model
- `ios/.../Features/Messaging/Components/ChatHeaderCard.swift` — animated affinity
- `ios/.../Features/Messaging/Components/CozyMessageBubble.swift` — dynamic badges
- `ios/.../Features/Messaging/Views/ChatView.swift` — time gap separators, energy animation
