diff --git a/docs/plans/2026-03-07-mutual-bonds-design.md b/docs/plans/2026-03-07-mutual-bonds-design.md new file mode 100644 index 0000000..d12a7de --- /dev/null +++ b/docs/plans/2026-03-07-mutual-bonds-design.md @@ -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` + +```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