# CLAUDE.md

This file provides guidance to Claude Code when working with code in this repository.

## Project Overview

BaoLife Android is the Android version of an AI-based human life simulator app built with Kotlin and Jetpack Compose. Players create a character and experience life events, build relationships, manage activities, and make choices that affect their character's development over time.

**iOS Reference:** `/Users/craigvandergalien/Documents/GitHub/lichun/ios`
**Backend:** Node.js/TypeScript WebSocket server (`server/`) at `wss://lichun.app/wss/` (the Python `ws/` server is legacy/retired)
**Task List:** See `ANDROID_PARITY_TASKS.md` in the iOS repo for complete feature parity checklist.

## Architecture

### Tech Stack

| Layer | Technology |
|-------|------------|
| UI | Jetpack Compose (Material 3) |
| Navigation | Jetpack Navigation 3 (`NavDisplay`, type-safe `@Serializable` `NavKey`s) |
| State Management | Kotlin Flow + StateFlow, collected via `collectAsStateWithLifecycle()` |
| DI | Hilt |
| Networking | OkHttp WebSocket; inbound JSON decoded via `@Serializable` DTOs |
| Image Loading | Coil 3 (`coil3`) + AndroidSVG |
| In-App Purchases | Google Play Billing 8.x |
| Analytics | Firebase Analytics + Crashlytics (currently stubbed: `FIREBASE_ENABLED=false`, no `google-services.json`) |
| Push Notifications | Firebase Cloud Messaging (inert until Firebase is enabled) |

### Package Structure

```
com.craigvg.lichun_android/
├── BaoLifeApp.kt                    # Application class
├── MainActivity.kt                   # Single activity, Compose host
├── network/
│   ├── WebSocketManager.kt          # Central WebSocket connection + message dispatch
│   └── dto/
│       └── InboundMessages.kt       # @Serializable DTOs for inbound message payloads
├── domain/
│   ├── models/                       # Data classes (27 models)
│   └── repositories/                 # Data access layer
├── ui/
│   ├── theme/                        # Design system
│   │   ├── AppColors.kt
│   │   ├── AppTypography.kt
│   │   ├── AppSpacing.kt
│   │   └── BaoLifeTheme.kt
│   ├── components/                   # Reusable UI components
│   ├── screens/                      # Feature screens
│   │   ├── home/
│   │   ├── dating/
│   │   ├── activities/
│   │   ├── character/
│   │   ├── messaging/
│   │   ├── monetization/
│   │   ├── onboarding/
│   │   ├── retention/
│   │   ├── settings/
│   │   ├── store/
│   │   └── events/
│   └── navigation/
│       ├── MainNavigation.kt         # NavDisplay host + transitions/predictive back
│       ├── Destinations.kt           # @Serializable NavKey destination types
│       ├── TabbedBackStack.kt        # Per-tab back-stack state (source of truth)
│       └── SharedTransition.kt       # Shared-element scope + key helpers
├── viewmodel/
│   ├── GameStateViewModel.kt         # Cross-feature game state
│   └── PlayerViewModel.kt            # Character-specific state
│                                     # (no AppViewModel — nav state lives in TabbedBackStack)
├── managers/
│   ├── ToastManager.kt
│   ├── SoundManager.kt
│   ├── AnalyticsManager.kt
│   ├── TooltipManager.kt
│   └── BillingManager.kt
├── utils/
│   ├── HapticFeedback.kt
│   └── Extensions.kt
└── di/
    └── AppModule.kt                  # Dependency injection
```

### State Management Pattern

The app uses ViewModels with StateFlow, mirroring the iOS @EnvironmentObject pattern:

**Main ViewModels:**

1. **WebSocketManager** - Central hub for all real-time server communication
   - Connects to `wss://lichun.app/wss/`
   - Handles 26+ message types
   - Auto-reconnection with exponential backoff (0.1s → 5s, max 1000 retries)

2. **GameStateViewModel** - Cross-feature game state
   - StateFlow for: energy, money, diamonds, time, season, gameSpeed
   - Computed: `canAfford()`, `formattedTime`, `seasonEmoji`

3. **PlayerViewModel** - Character-specific state
   - Person info, relationships, activities, inventory
   - Activity management methods
   - Relationship helpers

> Navigation state is NOT a ViewModel. It lives in `TabbedBackStack` (per-tab Navigation 3 back stacks + a full-screen override stack), which `MainNavigation` renders via `NavDisplay`.

**Manager Singletons:**

5. **ToastManager** - Toast notification queue
6. **SoundManager** - Audio playback (14+ sounds)
7. **AnalyticsManager** - Firebase Analytics & Crashlytics
8. **TooltipManager** - Onboarding tooltips
9. **BillingManager** - Google Play Billing

### WebSocket Message Flow

All game actions follow this pattern:

```kotlin
// Sending
webSocketManager.send(
    mapOf("type" to "messageType", "message" to payload)
)

// Receiving - handled in MessageHandler
when (type) {
    "u" -> handleLightweightUpdate(data)
    "playerObject" -> handleFullPlayerUpdate(data)
    "questionEvent" -> handleQuestion(data)
    // ...
}
```

**Key Message Types:**

| Direction | Type | Purpose |
|-----------|------|---------|
| Out | `init` | Connection initialization with device UUID |
| Out | `claimEvent` | Claim life event reward |
| Out | `speed` | Set game speed (6 levels) |
| In | `u` | Lightweight updates (energy, money, time) |
| In | `playerObject` | Full player state |
| In | `questionEvent` | Game events requiring choices |
| In | `messageEvent` | Life events (claimable) |
| In | `achievementUnlocked` | Achievement notification |

## Core Data Models

### Player
```kotlin
data class Player(
    val date: String,
    val status: String,
    val season: String,
    val hourOfDay: Int,
    val minuteOfHour: Int,
    val gameSpeed: Int,
    val activeConversations: List<ConversationObj>,
    val focuses: List<FocusOption>,
    val storeItems: List<StoreItem>,
    val r: List<Person>,  // Relationships
    val relData: List<Relationship>
)
```

### Person
```kotlin
data class Person(
    val id: String,
    val image: String,
    val status: String,
    val sex: String,
    val firstname: String,
    val lastname: String,
    val age: Int,
    val birthday: String,
    // Stats
    val mood: Int,
    val affinity: Int,
    val money: Double,
    val diamonds: Int,
    val prestige: Int,
    val happiness: Int,
    val health: Int,
    val intelligence: Int,
    // Activities
    val activities: List<Activity>,
    val activityRecords: List<ActivityRecord>,
    val habits: List<Habit>,
    // Social
    val relationships: List<String>,
    val bio: String,
    val interests: List<String>,
    val personalityTraits: List<String>,
    val compatibilityScore: Int?
)
```

### MessageEvent (Claimable Events)
```kotlin
data class MessageEvent(
    val id: String,
    val message: String,
    val type: String,
    val date: String,
    val hour: Int,
    val energyCost: Int?,
    val diamondCost: Int?,
    val moneyCost: Double?,
    val affinityChange: Int?,
    val characters: List<SimplePerson>?,
    val claimed: Boolean,
    val claimedAt: Long?,
    val category: EventCategory
) {
    val isClaimable: Boolean
        get() = !claimed && (energyCost ?: 0) > 0 || (moneyCost ?: 0.0) > 0 || (diamondCost ?: 0) > 0 || (affinityChange ?: 0) > 0

    val isNegative: Boolean
        get() = (energyCost ?: 0) < 0 || (moneyCost ?: 0.0) < 0 || (diamondCost ?: 0) < 0 || (affinityChange ?: 0) < 0
}

enum class EventCategory {
    CAREER, SOCIAL, ACHIEVEMENT, EDUCATION, HEALTH, FINANCE, RANDOM, NEUTRAL, NEGATIVE
}
```

## Design System

### Colors (Cozy, Warm Palette)

```kotlin
object AppColors {
    // Primary
    val primary = Color(0xFFF4A5B5)        // Soft rose pink
    val secondary = Color(0xFFB5C9F4)      // Soft periwinkle
    val accent = Color(0xFFFFD89B)         // Warm peach

    // Backgrounds (Warm, not stark white)
    val background = Color(0xFFFFF8F3)     // Warm cream
    val surfaceElevated = Color(0xFFFFF0E6)
    val surfaceSubtle = Color(0xFFFDF5EE)

    // Text (Warm Browns, not black)
    val primaryText = Color(0xFF5A4A3A)
    val secondaryText = Color(0xFF8B7A6A)
    val disabledText = Color(0xFFC4B5A7)

    // Stats (Soft Pastels)
    val energy = Color(0xFF9DDFAA)          // Sage green
    val money = Color(0xFFFFE07A)           // Butter yellow
    val diamond = Color(0xFFA8D5EA)         // Sky blue
    val health = Color(0xFFFFB3BA)          // Coral pink
    val happiness = Color(0xFFFFFEA7)       // Sunflower
    val intelligence = Color(0xFFC9B8F4)    // Lavender
    val prestige = Color(0xFFF9D5A7)        // Apricot
}
```

### Spacing

```kotlin
object AppSpacing {
    val xxs = 2.dp
    val xs = 6.dp
    val sm = 10.dp
    val md = 20.dp
    val lg = 30.dp
    val xl = 40.dp
    val xxl = 60.dp

    val buttonHeight = 54.dp
    val cornerRadius = 16.dp
    val avatarSmall = 44.dp
    val avatarMedium = 70.dp
    val avatarLarge = 120.dp
    val minTouchTarget = 44.dp
}
```

### Typography

Use rounded fonts with soft shadows for the cozy aesthetic. Font sizes:
- Display: 34sp
- Title: 28sp
- Headline: 18sp
- Body: 16sp
- Caption: 13sp
- Micro: 10sp

## Key Features

### 1. Claimable Events System
Life events appear in timeline with claimable rewards:
- **Unclaimed**: Pulsing border, "Tap to claim"
- **Claiming**: Squish animation, confetti, haptic
- **Claimed**: Muted (0.7 opacity), checkmark

### 2. Monetization
- **Energy Refills**: Small (50), Medium (100), Full (200), Unlimited 24h
- **Time Skips**: 1 hour, 1 day, 1 week, next event
- **IAP**: diamond1, diamond2 products

### 3. Retention
- **Achievements**: 5 categories, unlock celebrations with confetti
- **Daily Rewards**: 7-day streak calendar
- **Daily Quests**: 5 tasks per day with progress tracking

### 4. Dating System
- Swipe interface with card stack
- Compatibility scoring
- Relationship events (arguments, anniversaries, proposals)
- Date mini-games

### 5. Onboarding
- 5-step tutorial flow
- Contextual tooltips
- Guided first actions

## Development Commands

### Building

```bash
# Debug build
./gradlew assembleDebug

# Release build
./gradlew assembleRelease

# Build app bundle for Play Store
./gradlew bundleRelease
```

### Testing

```bash
# Unit tests
./gradlew test

# Instrumented tests
./gradlew connectedAndroidTest

# Specific test class
./gradlew testDebugUnitTest --tests "com.craigvg.lichun_android.network.WebSocketManagerDispatchTest"

# Compose UI tests run on the JVM via Robolectric (no device needed) under testDebugUnitTest
```

### Running

```bash
# Install debug APK
./gradlew installDebug

# List connected devices
adb devices

# Run on specific device
adb -s <device_id> install app/build/outputs/apk/debug/app-debug.apk
```

### Linting

```bash
# Run lint
./gradlew lint

# Generate lint report
./gradlew lintDebug
```

## Dependencies

Versions are managed in `gradle/libs.versions.toml` (the source of truth) and applied in `app/build.gradle.kts`. Current key versions:

| Dependency | Version | Notes |
|------------|---------|-------|
| AGP / Gradle | 8.13.2 / 8.13 | |
| Kotlin (K2) | 2.0.21 | KSP 2.0.21-1.0.28; **caps Coil at 3.2 and Play Billing at 8.0** (newer releases ship Kotlin 2.2+ metadata) |
| Compose BOM | 2026.05.01 | Material 3 |
| Navigation 3 | navigation3-runtime/ui 1.1.1 | + `lifecycle-viewmodel-navigation3` 2.10.0 |
| Hilt | 2.51.1 | uses **KSP**, not kapt; `hilt-navigation-compose` 1.2.0 |
| Coroutines | 1.9.0 | |
| OkHttp | 4.12.0 | WebSocket |
| kotlinx.serialization | 1.7.3 | inbound DTO decode + NavKey serialization |
| Coil | 3.2.0 (`coil3`) | + `coil-network-okhttp`, `coil-svg`; AndroidSVG 1.4 |
| Play Billing | 8.0.0 | |
| Firebase BOM | 33.7.0 | analytics/crashlytics/messaging — gated behind `FIREBASE_ENABLED` (stubbed) |
| compileSdk / targetSdk / minSdk | **36 / 35 / 26** | compileSdk 36 is a Nav3 1.1.1 compile-time requirement; runtime target stays 35 |
| Test | JUnit4, mockk 1.13.13, turbine 1.2.0, Robolectric 4.14.1 | Robolectric runs Compose UI tests on the JVM |

## Important Patterns

### SVG Image Loading
Use Coil 3 with the `coil3` SVG decoder for character avatars (note the `coil3.*` packages and `coil3.request.crossfade` extension):

```kotlin
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.svg.SvgDecoder

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(imageUrl)
        .crossfade(true)
        .decoderFactory(SvgDecoder.Factory())
        .build(),
    contentDescription = "Avatar"
)
```

### Resource Cost Checks
Always check resource availability before enabling actions:

```kotlin
val canPerform = gameState.energy >= energyCost &&
                 gameState.money >= moneyCost &&
                 gameState.diamonds >= diamondCost
```

### Haptic Feedback
Use consistent haptic patterns:

```kotlin
object HapticFeedback {
    fun light(view: View) = view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
    fun medium(view: View) = view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
    fun success(view: View) = view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
}
```

### WebSocket Reconnection
Implement exponential backoff:

```kotlin
private var retryDelay = 100L // Start at 0.1s
private val maxDelay = 5000L   // Max 5s
private var retryCount = 0
private val maxRetries = 1000

fun reconnect() {
    if (retryCount >= maxRetries) return

    viewModelScope.launch {
        delay(retryDelay)
        retryDelay = (retryDelay * 2).coerceAtMost(maxDelay)
        retryCount++
        connect()
    }
}
```

## iOS Reference Files

When implementing features, reference these iOS files:

| Android Feature | iOS Reference |
|-----------------|---------------|
| WebSocketManager | `WebSocketService.swift` (1130 lines) |
| GameStateViewModel | `Core/ViewModels/GameStateViewModel.swift` (130 lines) |
| PlayerViewModel | `Core/ViewModels/PlayerViewModel.swift` (231 lines) |
| Models | `Core/Models/*.swift` (11 files) |
| Design System | `Shared/DesignSystem/*.swift` (3 files) |
| Components | `Shared/Components/**/*.swift` (35+ files) |
| Home | `Features/Home/*.swift` (14 files) |
| Dating | `Features/Dating/*.swift` (18 files, most complex) |
| Activities | `Features/Activities/*.swift` (6 files) |
| Messaging | `Features/Messaging/*.swift` (11 files) |

## Known Considerations

### Platform Differences

1. **Navigation**: iOS uses NavigationStack/sheets, Android uses Jetpack Navigation 3 (`NavDisplay` + per-tab `TabbedBackStack`)
2. **Modals**: iOS uses `.sheet()`, Android uses `ModalBottomSheet`
3. **Haptics**: Different APIs but similar patterns
4. **IAP**: StoreKit vs Play Billing have different flows
5. **Push**: APNS vs FCM, but backend handles abstraction

### Performance Tips

1. Use `remember` and `derivedStateOf` to minimize recomposition
2. Use `LazyColumn`/`LazyRow` for lists
3. Preload sounds in SoundManager
4. Cache WebSocket messages appropriately
5. Use Baseline Profiles for startup optimization

## Environment

- **Min SDK**: 26 (Android 8.0)
- **Target SDK**: 35 (Android 15)
- **Compile SDK**: 36 (Nav3 1.1.1 compile-time requirement; runtime target stays 35)
- **Kotlin**: 2.0.21 (K2)
- **Compose BOM**: 2026.05.01
- **Gradle / AGP**: 8.13 / 8.13.2
- **JVM target**: 17
