docs: add mutual bond system design

Separates feelings (asymmetric sentiment values) from formal
relationship statuses (mutual bonds in a central registry).
Introduces proposal interaction protocol for partnership formation
with acceptance/rejection mechanics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-07 18:57:58 +00:00
parent b42362e041
commit d9b2a64430
@@ -0,0 +1,92 @@
# Mutual Bond System Design
## Problem
Partnership is currently a one-sided label derived from sentiment value. NPC A can call B their partner while B considers C their partner. The `classify()` function maps value >= 80 to "Partner" independently per entity, with no mutuality enforcement.
## Solution
Separate feelings from formal relationship status via three changes:
### 1. Bond Registry
World-level singleton component: `BondRegistry = Map<pairKey, Bond[]>`
```typescript
interface Bond {
type: 'partner'; // extensible: 'sibling', 'parent', etc.
formedAtTick: number;
status: 'active' | 'former';
}
```
- Keyed by canonical pair key (`"lowerEntityId:higherEntityId"`)
- Helper functions: `makePairKey()`, `getBonds()`, `hasBond()`, `addBond()`, `dissolveBond()`
- When a partner despawns, bond transitions to `status: 'former'` (consistent with existing memory system)
- Single source of truth for all structural relationships
### 2. Proposal Interaction
General-purpose mutual agreement protocol, with partnership as the first use case.
**New fields on `SocialState`:**
- `proposalCooldown: number` -- ticks until this entity can propose again
- `pendingProposal: { targetId: EntityId, type: 'partner' } | null` -- queued by relationship system
**Flow:**
1. **RelationshipSystem** (after updating sentiment): if entity's value toward other crosses >= `proposalThreshold` (default 80), no existing bond, no proposal cooldown, sets `pendingProposal` on the entity
2. **SocialSystem** (new interaction detection): when scanning for partners, if entity has `pendingProposal` targeting a nearby NPC, prioritize initiating interaction with that target. Interaction is flagged as proposal type.
3. **Proposal phases**: reuses existing `facing -> pausing -> emoting` but emoting phase uses ring emoji
4. **Resolution** (emoting->done transition):
- Check rejecter's sentiment toward proposer (must be >= `proposalAcceptanceThreshold`)
- Stat-influenced acceptance roll: base ~85%, empathy (+), sociability (+), temperament (-)
- **Accept**: bond added to registry, sentiment boost, positive emote, positive transient mods
- **Reject**: proposer takes amplified negative delta (2x), rejecter takes smaller hit (0.5x), both scaled by temperament. Proposal cooldown set on initiator, reduced by courage.
### 3. Classification Rework
- `classify()`: rename >= 80 tier from `'Partner'` to `'Devoted'`
- `getEffectiveClassification()`: add bond registry parameter. If `hasBond(partner, active)`, return `'Partner'` overriding the tier label
- `stateSerializer.ts`: switch from `classify()` to `getEffectiveClassification()`, pass bond registry
- Wire protocol: add `bond: 'partner' | 'former_partner' | null` field to serialized relationships
### 4. Client Display
- Ring emoji during proposal interaction (above both NPCs)
- NPC info panel: ring icon next to partner names
- Acceptance: ring emoji, then positive emote
- Rejection: ring emoji transitions to negative emote
### 5. Config Additions
All tuning values in `relationshipConfig.ts`:
```typescript
// Proposal system
proposalThreshold: 80,
proposalAcceptanceThreshold: 80,
proposalBaseAcceptanceChance: 0.85,
proposalEmpathyWeight: 0.02,
proposalSociabilityWeight: 0.02,
proposalTemperamentWeight: -0.02,
proposalAcceptanceBonus: 5,
proposalRejectionMultiplier: 2.0,
proposalRejectionRejecterMultiplier: 0.5,
baseProposalCooldown: 500,
courageCooldownWeight: 0.04,
proposalAcceptanceSociabilityMod: 2,
proposalAcceptanceEmpathyMod: 1,
proposalRejectionSociabilityMod: -1,
proposalRejectionEmpathyMod: -1,
proposalModDecayTicks: 150,
```
## Key Properties
- Feelings remain asymmetric and per-entity (existing system unchanged)
- Formal relationships are symmetric and centrally stored
- System is general enough for future bond types (sibling, parent/child)
- All tuning values configurable
- Rejection has real consequences, creating emergent drama
- System execution order unchanged: social -> relationship