# Event Review - Sprint 3

**Reviewer**: event-reviewer
**Date**: 2026-02-07
**Scope**: All Sprint 2 + Sprint 3 event files
**Sprint 3 files reviewed**:
- `server/src/events/career/workplaceEvents.ts` (7 realtime events)
- `server/src/events/career/careerMilestones.ts` (7 class-based events)
- `server/src/events/health/healthEvents.ts` (9 function-based events)
- `server/src/events/health/medicalArcs.ts` (4 class-based events)
- `server/src/events/family/familyDynamics.ts` (6 realtime events)
- `server/src/events/family/familyMilestones.ts` (6 class-based events)
- `server/src/utils/textVariations.ts` (utility)
- `server/src/events/index.ts` (wiring)
- `server/src/events/family/index.ts` (wiring)
- `server/src/events/health/index.ts` (wiring)
- `server/src/events/career/index.ts` (wiring)

**Sprint 2 files re-reviewed**:
- `server/src/events/family/activities.ts` (5 events + answer handlers)
- `server/src/events/education/schoolMilestones.ts`, `schoolSocial.ts`
- `server/src/events/holidays/birthdays.ts`, `enhanced.ts`
- `server/src/events/social/friendships.ts`
- `server/src/events/random/dailyDisruptions.ts`
- `server/src/events/health/events.ts`
- `server/src/events/base.ts`, `server/src/handlers/events.ts`

---

## Summary

Sprint 3 added 39 new events (22 function-based + 17 class-based) across career, health, and family domains. Event wiring, mode overrides, and class-based event registration are all correct. The main issues found are balance values in `activities.ts` that are far outside the established scales, and a systemic limitation where function-based question events cannot deliver positive stat rewards (only costs are deducted).

**Event counts**:
| File | Function-based | Class-based | Total |
|------|---------------|-------------|-------|
| workplaceEvents.ts | 7 | 0 | 7 |
| careerMilestones.ts | 0 | 7 | 7 |
| healthEvents.ts | 9 | 0 | 9 |
| medicalArcs.ts | 0 | 4 | 4 |
| familyDynamics.ts | 6 | 0 | 6 |
| familyMilestones.ts | 0 | 6 | 6 |
| **Total Sprint 3** | **22** | **17** | **39** |

---

## CRITICAL Issues

### C1. SYSTEMIC: Function-based question events cannot deliver positive rewards

**Status**: REPORTED (systemic pre-existing issue, not Sprint 3 specific)

When a player answers a function-based question event, `handlers/events.ts` (line 215) calls `eventFn(player, 'answer', response)`. However, ALL function-based events only handle the 'check' type (they re-run probability checks on 'answer' and return null). This means:

- **Only energy/money/diamond costs from `createAnswerOption` are automatically deducted** (handler lines 188-198)
- **Positive stat changes (happiness, affinity, health, social boosts) described in event comments NEVER HAPPEN**
- Only `activities.ts` events have dedicated answer handler functions that apply rich stat changes

**Affected Sprint 3 events (22 total)**:
- All 7 workplace events: social/happiness/performance boosts never applied
- 6 health realtime events: energy restoration (nap), stress reduction, health boosts never applied
- 3 health fast events: pre-apply damage correctly, but treatment benefits missing
- 6 family dynamics events: affinity/happiness boosts never applied

**Impact**: Players feel choices are meaningless. Choosing "Take a nap" in `feelingTiredWarning` does NOT restore energy. Answering mom's call in `parentCalls` does NOT boost affinity. The events are cosmetic beyond cost deductions.

**Note**: This same limitation affects Sprint 2 function-based events (schoolSocial, dailyDisruptions, etc.). It is a systemic architectural limitation, not a Sprint 3 regression.

**Recommended fix**: Add answer handling branches to function-based events that accept a 3rd `response` parameter, check `type === 'answer'`, and apply stat changes based on the selected option. This should be a dedicated task.

---

### C2. activities.ts: Affinity clamping uses 0-100 instead of -100 to 100 (FIXED)

**Status**: FIXED

All affinity operations in `activities.ts` use `Math.max(0, ...)` which prevents affinity from going negative. The correct affinity range is -100 to 100 (as used by `modifyStat(value, delta, -100, 100)` everywhere else). This means:
- A player with low affinity (e.g., 3) who refuses family events gets clamped to 0 instead of going to -17
- Negative affinity relationships are impossible from family events

Fixed by replacing all affinity Math.min/Math.max with `modifyStat(value, delta, -100, 100)`.

---

## BALANCE Issues

### B1. activities.ts: familyGameNight affinity +20 per member (FIXED)

**Status**: FIXED (weekly: +20 -> +5, monthly: +10 -> +3)

With 4 family members, weekly game night gave +80 affinity total per event. Balance scale: +5 = good hangout, +10 = significant bond. A game night is a good hangout, not a life-changing bond. Would max out all family relationships in 2-3 weeks.

### B2. activities.ts: familyGameNight happiness +25 (FIXED)

**Status**: FIXED (+25 -> +10)

Balance: +15 = great day, +25 reserved for major life events (graduation, prom). A board game night is nice but not in that tier.

### B3. activities.ts: familyPhoto refusal affinity -20 per member (FIXED)

**Status**: FIXED (-20 -> -5)

With 4 family members, refusing a photo gave -80 affinity total. Balance: -10 = real conflict. Refusing a photo is a social faux pas, not a family conflict.

### B4. activities.ts: familyVacation big trip affinity +20 per member (FIXED)

**Status**: FIXED (+20 -> +10)

A big trip IS significant, but +20 per member was too high. With 4 members = +80 total.

### B5. activities.ts: teachSiblingSkill affinity +25 (FIXED)

**Status**: FIXED (+25 -> +8)

Teaching a sibling is nice but +25 is extreme. Balance: +10 = significant bond.

### B6. activities.ts: helpParentProject refusal affinity -20 (FIXED)

**Status**: FIXED (-20 -> -5)

Being too busy to help a parent is disappointing but not a real conflict.

### B7. activities.ts: Overall stat inflation across all family events (FIXED)

**Status**: FIXED

All five activity events had stat values 2-5x above the balance scales. Comprehensive rebalance applied using `modifyStat()` consistently. See commit for details.

### B8. familyMilestones.ts: InheritanceDispute fight gives flat $5000

**Status**: REPORTED

Uses `player.money += 5000` directly. This is 2.5-10 months of salary ($500-2000/month) for choosing the selfish option. Risk (sibling affinity -10 each) may not balance the $5000 reward. Consider reducing to $2000-3000 or scaling by salary.

---

## MODERATE Issues

### M1. activities.ts: Uses player.c.money for direct deductions

The convention across other events is to use `createAnswerOption` moneyCost for automated deductions. `activities.ts` directly mutates `player.c.money` in answer handlers. Functionally correct (player.c is shorthand for player.character) but inconsistent with the pattern.

### M2. activities.ts: Used raw Math.min/Math.max instead of modifyStat() (FIXED)

All stat operations in activities.ts used `Math.min(100, Math.max(0, value + delta))` instead of the standard `modifyStat()` utility. Fixed as part of the balance rebalance.

### M3. healthEvents.ts: annualCheckupReminder name similarity

Both `annualCheckup` (health/events.ts) and `annualCheckupReminder` (healthEvents.ts) exist. Different fnames so no mechanical conflict, but confusing for maintainability.

### M4. familyMilestones.ts: InheritanceDispute uses player.money += directly

`player.money += 5000` (line 700) bypasses any money capping or validation. Should use proper money management pattern.

### M5. textVariations.ts: buildEventContext uses (player as any)

Two `as any` casts for `player.weather` and `player.weekDayText` (lines 85, 87). These properties should be typed on the Player model or handled with proper type guards.

---

## MINOR Issues

### N1. workplaceEvents.ts: All events are one-time only

All 7 workplace events use `player.events.has(fname)` for permanent dedup. Daily events like coffee chats and lunch invitations should arguably be repeatable (use cooldown timer instead). However, this matches Sprint 2 patterns.

### N2. healthEvents.ts: greatWorkout pre-applies stat changes

`greatWorkout` (lines 130-132) mutates happiness, health, and energy directly before creating the messageEvent. The energyCost in the message (line 139) is display-only. This works but is a different pattern from other events and could confuse future developers.

### N3. Duplicate event registrations in allEvents

`events/index.ts` spreads `...healthEvents` (which now includes `healthNarrativeEvents` as a namespace key) AND separately spreads `...healthNarrativeEvents` (individual functions). Similarly for familyDynamicsEvents. The duplicate object key is harmless (same reference) but could be cleaned up.

### N4. familyDynamics.ts and familyMilestones.ts: Duplicate helper functions

Both files define `findParent()`, `findSibling()`, `findChild()`, and `getParentTitle()`. These could be extracted to a shared family helpers module.

---

## Wiring Verification

All Sprint 3 events are correctly wired:

| Check | Status |
|-------|--------|
| health/index.ts exports healthNarrativeEvents | PASS |
| health/index.ts exports medicalArcClassEvents | PASS |
| family/index.ts exports familyDynamicsEvents | PASS |
| family/index.ts exports familyMilestoneClassEvents | PASS |
| events/index.ts includes healthNarrativeEvents in allEvents | PASS |
| events/index.ts includes familyDynamicsEvents in allEvents | PASS |
| events/index.ts includes medicalArcClassEvents in classBasedEvents | PASS |
| events/index.ts includes familyMilestoneClassEvents in classBasedEvents | PASS |
| eventModeOverrides includes all Sprint 3 function-based events | PASS |
| career/index.ts exports workplaceEvents and careerMilestoneClassEvents | PASS |

---

## Quality Assessment

### Sprint 3 Class-Based Events: GOOD

`careerMilestones.ts`, `medicalArcs.ts`, and `familyMilestones.ts` all follow the `BaseEvent` pattern correctly with proper `processAnswer()` methods that apply rich stat changes. Multi-stage events use `scheduleFollowUp()` properly. Stat values are generally within balance scales.

### Sprint 3 Function-Based Events: NEEDS IMPROVEMENT

`workplaceEvents.ts`, `healthEvents.ts`, and `familyDynamics.ts` are well-written narratively but limited by the systemic answer processing issue (C1). They function as "choose your vibe" events where only costs differ, not outcomes.

### textVariations.ts: GOOD

Clean utility with `pickVariant()`, `templateReplace()`, `buildEventContext()`. Well-structured and useful for future event text variety.

### activities.ts (Sprint 2): FIXED

Had severe balance inflation and incorrect affinity clamping. All 5 event answer handlers rebalanced to match established scales.
