# Gameplay Fixes — Sprint 1

> Completed: 2026-02-07

## Overview

Audited and fixed 33 files across the BaoLife monorepo, addressing critical gameplay bugs in the TypeScript backend, iOS app, and game engine. Three parallel audits identified 95 issues (20 P0, 28 P1, 20 P2, 12 P3). Five fix tasks resolved all P0 and most P1 issues.

**Impact**: +1,285 / -319 lines | 946 tests passing | TypeScript compiles clean

---

## Fix 1: Game Loop Consolidation

**Problem**: `PlayerSession.ts` (the active game loop) was missing critical features that existed in the unused `LoopManager.ts`/`GameEngine.ts`. Characters didn't age, stats didn't deplete, death checks never ran, and daily plans were never generated.

**Changes**:
- Added `updateAge()` call in `processHourTick()` — increments ageHours, handles birthday events, relationship aging/decay
- Added hourly stat depletion: hunger +3/hr, thirst +4/hr, energy -2/hr
- Added health decay when hunger/thirst > 80, slow recovery otherwise
- Added `getPeakEnergy()` call to recalculate calcEnergy from habits/activities
- Added `getIntradayActivity()` for location tracking based on daily plan
- Added `getDailyPlan()` in `processDayTick()` to regenerate schedules
- Added death checks (`updateDeathChance` + `checkDeath`) in daily tick
- Added daily energy recovery (+1/day) and auto-save

**Files**: `server/src/game/PlayerSession.ts`

---

## Fix 2: Event Wiring

**Problem**: 14 event categories were defined but never wired into the event system. Answer handling passed arguments in the wrong order. Family event responses were silently dropped.

**Changes**:
- Added schoolYearEvents, transitionEvents, dilemmaEvents to `allEvents` registry
- Created `classBasedEvents` array for ~70 activity events and ~4 social events
- Fixed answer argument order: `eventFn(player, 'answer', response)` (was passing response as 4th arg)
- Added family answer handler dispatch (familyGameNight, familyPhoto, etc.)
- Added class-based event answer handling via `classBasedEvents.find()`
- Cleaned `randomRelationshipEvents` export to only `handleRandomRelationshipEvents`
- Added classBasedEvents checking loop in `stats_manager.checkEvents()`

**Files**: `server/src/events/index.ts`, `server/src/handlers/events.ts`, `server/src/events/relationships/randomEvents.ts`, `server/src/stats/stats_manager.ts`

---

## Fix 3: State Normalization

**Problem**: Server sent occupation values iOS couldn't match (e.g., `"Software Engineer"` instead of `"work"`). Education grade mapping was inconsistent. Affinity had no upper bound. iOS didn't parse most stat updates. Health was typed as Double instead of Int.

**Changes**:
- `job_manager.ts`: `person.occupation = 'work'` (was `occupation.title`)
- `education_manager.ts`: `person.occupation = 'student'` (was `'school'`), grade 17 = `'12th'` (was `'high_school'`)
- `GameEngine.ts`: Affinity bounds standardized to 0-100, calcEnergy clamped to min 0, added hunger/thirst/health/intelligence/ageYears to hourly "u" update
- `WebSocketService.swift`: Added parsing for hunger, thirst, health, happiness, stress, intelligence, ageYears in "u" message handler
- `Person.swift`: `health: Int = 100` (was `Double = 0`), simplified healthPercentage
- `GameStateViewModel.swift`: Added 7 new `@Published` properties with person bindings

**Files**: `server/src/services/jobs/job_manager.ts`, `server/src/services/education/education_manager.ts`, `server/src/game/engine/GameEngine.ts`, `ios/.../WebSocketService.swift`, `ios/.../Person.swift`, `ios/.../GameStateViewModel.swift`

---

## Fix 4: Security Exploits

**Problem**: Diamond economy used an in-memory balance map that drifted from the player model. Daily rewards gave Day 7 reward (50 diamonds) on first login instead of Day 1 (5 diamonds). claimEvent had no server-side validation. Achievement cache wasn't loaded at session start, allowing re-trigger exploits.

**Changes**:
- `diamondEconomy.ts`: Reworked to use `player.c.diamonds` directly, removed in-memory balance map, added connectionRegistry fallback for string-only callers
- `dailyRewards.ts`: Fixed `next_reward_day` initialization (1→2 for first login), fixed display logic for same-day reconnect and streak breaks
- `retention.ts`: Implemented real claimEvent validation — verifies event exists in player.events/lifeEvents, blocks duplicate claims via `claimed:` markers, applies server-side rewards
- `WebSocketServer.ts`: Added `loadPlayerAchievements(userId)` at session init to pre-populate cache
- Updated all retention callers (achievements, dailyQuests, integration, purchases) to pass player context

**Files**: `server/src/monetization/diamondEconomy.ts`, `server/src/services/retention/dailyRewards.ts`, `server/src/handlers/retention.ts`, `server/src/server/WebSocketServer.ts`, `server/src/services/retention/achievements.ts`, `server/src/services/retention/dailyQuests.ts`, `server/src/services/retention/integration.ts`, `server/src/handlers/purchases.ts`

---

## Fix 5: Romance & Dating System

**Problem**: iOS sent raw string payloads (`"partner-id"`) but handlers expected objects (`{partnerId: "partner-id"}`). Relationship state machine had no propose/marry transitions. Break-up/divorce left orphaned references. Several handler commands were never registered.

**Changes**:
- Added `extractPartnerId()` and `extractDateActivity()` normalizer functions accepting both payload formats
- Implemented relationship state machine: Prospect → Dating → Engaged → Married
- Added `handlePropose` (Dating→Engaged) and `handleMarry` (Engaged→Married) handlers
- Break-up/divorce cleanup via shared `clearRelationshipReferences()` teardown
- Added `isRomanticRelationship()` type guard and centralized `ROMANTIC_TAGS`
- Registered 6 new commands: `propose`, `marry`, `startDate`, `getDateIdeas`, `relationshipEventResponse`, `dateMiniGameResponse`
- `messagingStyle.ts` updated to read from `player.messagingModifiers` instead of `player.relData`
- Player model constructor separates relData into relationship entries and messaging modifiers

**Files**: `server/src/handlers/romance.ts`, `server/src/handlers/index.ts`, `server/src/services/relationships/relationship_manager.ts`, `server/src/events/conversations/messagingStyle.ts`, `server/src/models/Player.ts`

---

## Other Changes

- Removed `energyLevel` property from Player model (replaced by `player.c.energy`)
- Updated `gameControl.ts` and `retention.ts` to use `player.c.energy` instead of `player.energyLevel`
- Fixed 8 test regressions caused by cross-module changes

## Remaining Known Issues (Pre-existing)

| Test | Status |
|------|--------|
| `config.test.ts` — tick interval expects 10ms, actual 1ms | Pre-existing |
| `conversation.test.ts` — single-char message filtering | Pre-existing |
| `deduplication.test.ts` — 3 window-based dedup failures | Pre-existing |
| `dailyQuests.test.ts` — flaky quest ID ordering | Pre-existing |
| `conversation-e2e.test.ts` — integration infra | Pre-existing |
| `lifecycle/*.test.ts` — lifecycle test infra | Pre-existing |
