# Home View Redesign - Claimable Life Events System

**Date:** November 12, 2025
**Project:** BaoLife iOS Application
**Feature:** Main home page UI overhaul with interactive event claiming
**Estimated Effort:** 12 hours

---

## Executive Summary

This document outlines a complete redesign of the main home page (HomeView) from a text-heavy, cramped layout to a cozy card-based dashboard with an interactive claimable events system. The new design prioritizes quick-glance utility for resources/stats while transforming the message feed into an engaging visual life timeline where players tap to claim rewards.

**Key Changes:**
- Dashboard card layout (Stardew Valley-inspired)
- Claimable event system (positive rewards require tap to claim)
- Visual life timeline with icons, colors, and animations
- Better information hierarchy and breathing room
- Enhanced player engagement through reward claiming loop

---

## Current State Analysis

### Problems with Current HomeView

1. **MainCharacterView is cramped** - Horizontal split with tiny avatar, too much text
2. **Messages are plain text** - Just strings in a list, no visual interest
3. **Poor use of space** - Text list dominates but feels empty
4. **No engagement loop** - Passive information display only
5. **Flat visual hierarchy** - Everything competes equally for attention

### Current Backend Integration

**Message System** (WebSocketService.swift:402-425):
- Backend sends `messageEvent` type with MessageEvent object
- Contains: id, message, type, date, hour, costs (energy/money/diamonds), rewards (affinity, etc.)
- Currently added to `messages: [String]` array as plain text
- If has image, shows modal instead of adding to list

---

## Design Overview

### New Layout Structure

```
┌─────────────────────────────────────────────────────┐
│ Status Header Card                                  │
│ [Time, Season, Character Name, Resources]           │
└─────────────────────────────────────────────────────┘

┌──────────────────────┐  ┌────────────────────────┐
│ Avatar Card          │  │ Quick Stats Card       │
│ [Large avatar]       │  │ [Health, Happiness,    │
│ [Mood display]       │  │  Intelligence, Prestige│
└──────────────────────┘  └────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ Life Timeline (Claimable Events)                    │
│ [Visual event cards with claim interaction]         │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ Game Controls Card                                  │
│ [Speed controls + Start/Stop/Restart]               │
└─────────────────────────────────────────────────────┘
```

---

## Core Feature: Claimable Events System

### Event Classification

**Claimable Events (Positive Rewards)** - Require player tap:
- Career wins: Promotions, bonuses, raises
- Social wins: Date success, new friendships, gifts received
- Achievements: Unlocked achievements, milestones
- Education: Graduation, academic awards
- Lucky events: Found money, surprise gifts

**Auto-Applied Events (Negative/Neutral)** - No interaction needed:
- Losses: Lost money, fired, bills, fines
- Relationship endings: Breakups, deaths, moved away
- Health problems: Illness, injury, stress
- Neutral updates: Moved cities, birthday, time passing
- Bad luck: Accidents, robberies

### Event State Machine

```
┌─────────────────────────────────────────────────┐
│ Event arrives from backend (messageEvent)       │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
         ┌───────────────┐
         │ Has rewards?  │
         └───┬───────┬───┘
             │       │
         Yes │       │ No
             ▼       ▼
    ┌────────────┐  ┌─────────────────┐
    │ Claimable  │  │ Auto-Applied    │
    │ (Unclaimed)│  │ (Show as info)  │
    └─────┬──────┘  └─────────────────┘
          │
    User taps
          │
          ▼
    ┌────────────┐
    │ Claiming   │ ← Animation + send claim to backend
    └─────┬──────┘
          │
          ▼
    ┌────────────┐
    │ Claimed    │ ← Muted, timestamped, collapses
    └────────────┘
```

### Backend Integration

#### Detecting Claimable Events

An event is claimable if it has **positive** costs/rewards:
- `energyCost > 0` → Energy reward
- `moneyCost > 0` → Money reward
- `diamondCost > 0` → Diamond reward
- `affinityChange > 0` → Affinity gain
- **Note:** Negative values are costs (auto-applied)

#### Claim Message Protocol

**When user taps to claim:**

```json
{
  "type": "claimEvent",
  "message": {
    "eventId": "abc123...",
    "timestamp": "2025-11-12T14:30:00Z"
  }
}
```

**Backend response:**
- Updates player resources (money, energy, diamonds, etc.)
- Sends `u` type update with new resource values
- Frontend marks event as claimed and plays animation

#### Message Model Update

**Current MessageEvent.swift:**
```swift
struct MessageEvent {
    var id: String
    var message: String
    var type: String
    var date: String?
    var hour: String?
    var energyCost: Int?
    var diamondCost: Int?
    var moneyCost: Int?
    var affinityChange: Int?
    var title: String?
    var image: String?
}
```

**Updated MessageEvent.swift (add these properties):**
```swift
struct MessageEvent: Identifiable, Codable {
    // ... existing properties ...

    // NEW: Claiming state
    var claimed: Bool = false
    var claimedAt: Date?

    // NEW: Event categorization
    var category: EventCategory = .neutral

    // NEW: Computed property
    var isClaimable: Bool {
        // Has positive rewards?
        (energyCost ?? 0) > 0 ||
        (moneyCost ?? 0) > 0 ||
        (diamondCost ?? 0) > 0 ||
        (affinityChange ?? 0) > 0
    }

    var isNegative: Bool {
        // Has negative costs?
        (energyCost ?? 0) < 0 ||
        (moneyCost ?? 0) < 0 ||
        (diamondCost ?? 0) < 0 ||
        (affinityChange ?? 0) < 0
    }
}

enum EventCategory: String, Codable {
    case career = "💼"
    case social = "❤️"
    case achievement = "🏆"
    case education = "🎓"
    case health = "❤️‍🩹"
    case finance = "💰"
    case random = "🎲"
    case neutral = "ℹ️"
    case negative = "📉"
}
```

#### WebSocketService Changes

**Current:**
```swift
@Published var messages: [String] = []
```

**New:**
```swift
@Published var lifeEvents: [MessageEvent] = []
@Published var unclaimedEventCount: Int = 0

func claimEvent(_ event: MessageEvent) {
    // Mark as claimed locally (optimistic update)
    if let index = lifeEvents.firstIndex(where: { $0.id == event.id }) {
        lifeEvents[index].claimed = true
        lifeEvents[index].claimedAt = Date()
        updateUnclaimedCount()
    }

    // Send claim to backend
    let message: [String: Any] = [
        "type": "claimEvent",
        "message": [
            "eventId": event.id,
            "timestamp": ISO8601DateFormatter().string(from: Date())
        ]
    ]
    sendMessage(message: message)
}

private func updateUnclaimedCount() {
    unclaimedEventCount = lifeEvents.filter { $0.isClaimable && !$0.claimed }.count
}
```

**Message parsing update (WebSocketService.swift:402):**
```swift
} else if (type == "messageEvent") {
    if let messageDict = parsedJson as? [String: Any] {
        var messageEvent = MessageEvent(
            id: messageDict["id"] as? String ?? UUID().uuidString,
            message: messageDict["message"] as? String ?? "",
            type: messageDict["type"] as? String ?? "messageEvent",
            date: messageDict["date"] as? String,
            hour: messageDict["hour"] as? String,
            energyCost: messageDict["energyCost"] as? Int,
            diamondCost: messageDict["diamondCost"] as? Int,
            moneyCost: messageDict["moneyCost"] as? Int,
            affinityChange: messageDict["affinityChange"] as? Int,
            title: messageDict["title"] as? String,
            image: messageDict["image"] as? String
        )

        // Determine category from message content or type
        messageEvent.category = determineCategory(from: messageDict)

        // Auto-apply if negative, otherwise add as claimable
        if messageEvent.isNegative || !messageEvent.isClaimable {
            messageEvent.claimed = true
            messageEvent.claimedAt = Date()
        }

        // Add to events (insert at beginning for newest-first)
        self?.lifeEvents.insert(messageEvent, at: 0)
        self?.updateUnclaimedCount()

        // Keep only last 50 events
        if self?.lifeEvents.count ?? 0 > 50 {
            self?.lifeEvents.removeLast()
        }
    }
}
```

---

## Component Specifications

### 1. StatusHeaderCard

**File:** `Features/Home/Components/StatusHeaderCard.swift`

**Purpose:** Top card showing time, season, character name, and all resources

**Layout:**
```
┌─────────────────────────────────────────────────────┐
│ 🍂 Fall, Year 2  │  Monday 9:00 AM  │  Speed: ▶▶▶ │
│─────────────────────────────────────────────────────│
│ Sarah Martinez                                      │
│ ⚡ 60  💰 $1,200  💎 25  ❤️ 85%                     │
└─────────────────────────────────────────────────────┘
```

**Styling:**
- Background: Seasonal gradient from AppColors.cozySeasonGradient()
- Height: 100pt
- Padding: AppSpacing.md
- Corner radius: AppSpacing.largeCornerRadius
- Shadow: Soft (radiusSoft)

**Code Structure:**
```swift
struct StatusHeaderCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        VStack(spacing: AppSpacing.sm) {
            // Row 1: Time, date, speed
            HStack {
                seasonBadge
                Spacer()
                dateBadge
                Spacer()
                speedIndicator
            }

            Divider()

            // Row 2: Name
            Text(characterName)
                .font(.appTitle2)
                .foregroundColor(AppColors.primaryText)

            // Row 3: Resources
            HStack(spacing: AppSpacing.md) {
                ResourcePill(icon: "bolt.fill", value: "\(energy)", color: AppColors.energy)
                ResourcePill(icon: "dollarsign.circle.fill", value: "$\(money)", color: AppColors.money)
                ResourcePill(icon: "gem.fill", value: "\(diamonds)", color: AppColors.diamond)
                ResourcePill(icon: "heart.fill", value: "\(health)%", color: AppColors.health)
            }
        }
        .padding(AppSpacing.md)
        .background(AppColors.cozySeasonGradient(season))
        .cornerRadius(AppSpacing.largeCornerRadius)
        .shadow(color: Color.black.opacity(0.06), radius: AppSpacing.Shadow.radiusSoft)
    }
}
```

---

### 2. AvatarStatsRow

**Files:**
- `Features/Home/Components/AvatarCard.swift`
- `Features/Home/Components/QuickStatsCard.swift`

**Purpose:** 2-column row showing avatar + key stats

**Layout:**
```
┌──────────────────────┐  ┌────────────────────────┐
│                      │  │ Quick Stats            │
│   [Large Avatar]     │  │                        │
│   (120px circle)     │  │ ❤️ Health      85% ▓▓▓ │
│                      │  │ 😊 Happiness   72% ▓▓▓ │
│  Feeling happy       │  │ 🧠 Intelligence 60 ▓▓▓ │
│                      │  │ 🎩 Prestige    50  ▓▓  │
└──────────────────────┘  └────────────────────────┘
```

**AvatarCard Code:**
```swift
struct AvatarCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        BaseCard {
            VStack(spacing: AppSpacing.md) {
                // Large avatar
                if let url = URL(string: webSocketService.person.image) {
                    SVGImageView(url: url, desiredSize: CGSize(width: 120, height: 120))
                        .frame(width: 120, height: 120)
                        .clipShape(Circle())
                        .overlay(
                            Circle()
                                .strokeBorder(
                                    LinearGradient(
                                        colors: [AppColors.primary, AppColors.accent],
                                        startPoint: .topLeading,
                                        endPoint: .bottomTrailing
                                    ),
                                    lineWidth: 4
                                )
                        )
                        .shadow(color: AppColors.primary.opacity(0.3), radius: 12, x: 0, y: 6)
                }

                // Mood
                Text(webSocketService.person.mood)
                    .font(.appCaption)
                    .foregroundColor(AppColors.secondaryText)
            }
        }
    }
}
```

**QuickStatsCard Code:**
```swift
struct QuickStatsCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        BaseCard {
            VStack(spacing: AppSpacing.md) {
                Text("Quick Stats")
                    .font(.appHeadline)
                    .foregroundColor(AppColors.primaryText)

                CozyStatBar(
                    label: "Health",
                    value: Int(webSocketService.person.health * 100),
                    color: AppColors.health,
                    height: 10
                )

                CozyStatBar(
                    label: "Happiness",
                    value: webSocketService.person.happiness,
                    color: AppColors.happiness,
                    height: 10
                )

                CozyStatBar(
                    label: "Intelligence",
                    value: webSocketService.person.intelligence,
                    color: AppColors.intelligence,
                    height: 10
                )

                CozyStatBar(
                    label: "Prestige",
                    value: webSocketService.person.prestige,
                    color: AppColors.prestige,
                    height: 10
                )
            }
        }
    }
}
```

---

### 3. LifeTimelineCard (Core Feature)

**File:** `Features/Home/Components/LifeTimelineCard.swift`

**Purpose:** Visual event feed with claimable interactions

**Layout:**
```
┌──────────────────────────────────────────────────┐
│ Life Timeline      [3 rewards]  [Filter: All ▾] │
│──────────────────────────────────────────────────│
│ ✨ [PULSING]                                     │
│ 💼 Promoted to Senior Manager                    │
│ 👆 Tap to claim: +$500 💰  +10 🎩               │
│                                                  │
│──────────────────────────────────────────────────│
│ 📉 Car broke down                                │
│ Lost: -$800 💰                                   │
│ 30 minutes ago                                   │
│──────────────────────────────────────────────────│
│ ✅ Claimed                                       │
│ 🎓 Graduated from University                     │
│ Earned: +$200 💰  +20 🧠  +15 🎩                │
│ 6 hours ago                                      │
└──────────────────────────────────────────────────┘
```

**Code:**
```swift
struct LifeTimelineCard: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @State private var selectedFilter: EventFilter = .all

    enum EventFilter: String, CaseIterable {
        case all = "All"
        case career = "Career"
        case social = "Social"
        case achievements = "Achievements"
    }

    var filteredEvents: [MessageEvent] {
        let events = webSocketService.lifeEvents

        switch selectedFilter {
        case .all:
            return events
        case .career:
            return events.filter { $0.category == .career }
        case .social:
            return events.filter { $0.category == .social }
        case .achievements:
            return events.filter { $0.category == .achievement }
        }
    }

    var sortedEvents: [MessageEvent] {
        // Unclaimed first, then by date
        filteredEvents.sorted { event1, event2 in
            if !event1.claimed && event2.claimed {
                return true
            } else if event1.claimed && !event2.claimed {
                return false
            } else {
                // Both same claim status, sort by date (newest first)
                return (event1.claimedAt ?? Date.distantPast) > (event2.claimedAt ?? Date.distantPast)
            }
        }
    }

    var body: some View {
        BaseCard {
            VStack(spacing: AppSpacing.md) {
                // Header
                HStack {
                    Text("Life Timeline")
                        .font(.appHeadline)
                        .foregroundColor(AppColors.primaryText)

                    Spacer()

                    // Unclaimed count badge
                    if webSocketService.unclaimedEventCount > 0 {
                        HStack(spacing: 4) {
                            Text("\(webSocketService.unclaimedEventCount)")
                                .font(.appCaptionBold)
                            Image(systemName: "gift.fill")
                                .font(.system(size: 12))
                        }
                        .foregroundColor(.white)
                        .padding(.horizontal, 10)
                        .padding(.vertical, 4)
                        .background(AppColors.primary)
                        .cornerRadius(12)
                    }

                    // Filter picker
                    Menu {
                        ForEach(EventFilter.allCases, id: \.self) { filter in
                            Button(filter.rawValue) {
                                selectedFilter = filter
                            }
                        }
                    } label: {
                        HStack(spacing: 4) {
                            Text(selectedFilter.rawValue)
                                .font(.appCaption)
                            Image(systemName: "chevron.down")
                                .font(.system(size: 10))
                        }
                        .foregroundColor(AppColors.secondaryText)
                    }
                }

                Divider()

                // Events list
                ScrollView {
                    LazyVStack(spacing: AppSpacing.md) {
                        ForEach(sortedEvents.prefix(12)) { event in
                            LifeEventCard(event: event)
                                .environmentObject(webSocketService)
                        }
                    }
                }
                .frame(maxHeight: 400)
            }
        }
    }
}
```

---

### 4. LifeEventCard (Individual Event)

**File:** `Features/Home/Components/LifeEventCard.swift`

**Purpose:** Individual event card with claim interaction

**States:**

**Unclaimed (Claimable):**
```swift
struct LifeEventCard: View {
    @EnvironmentObject var webSocketService: WebSocketService
    let event: MessageEvent

    @State private var isPulsing = false
    @State private var isClaiming = false
    @State private var showConfetti = false

    var body: some View {
        Button(action: {
            if event.isClaimable && !event.claimed {
                claimEvent()
            }
        }) {
            HStack(alignment: .top, spacing: AppSpacing.sm) {
                // Category icon
                Text(event.category.rawValue)
                    .font(.system(size: 32))

                VStack(alignment: .leading, spacing: AppSpacing.xs) {
                    // Event message
                    Text(event.message)
                        .font(.appBodyBold)
                        .foregroundColor(AppColors.primaryText)
                        .multilineTextAlignment(.leading)

                    // State-specific content
                    if event.isClaimable && !event.claimed {
                        // Unclaimed state
                        HStack(spacing: 4) {
                            Image(systemName: "hand.tap.fill")
                                .font(.system(size: 12))
                            Text("Tap to claim:")
                                .font(.appCaption)
                        }
                        .foregroundColor(AppColors.primary)

                        rewardDisplay
                            .foregroundColor(AppColors.secondaryText)

                    } else if event.claimed {
                        // Claimed state
                        HStack(spacing: 4) {
                            Image(systemName: "checkmark.circle.fill")
                                .font(.system(size: 12))
                                .foregroundColor(AppColors.success)
                            Text("Earned:")
                                .font(.appCaption)
                        }

                        rewardDisplay
                            .foregroundColor(AppColors.secondaryText.opacity(0.7))

                        if let claimedAt = event.claimedAt {
                            Text(timeAgo(from: claimedAt))
                                .font(.appSmall)
                                .foregroundColor(AppColors.disabledText)
                        }

                    } else {
                        // Auto-applied (negative/neutral)
                        lossDisplay
                            .foregroundColor(AppColors.error)

                        Text(timeAgo(from: event.claimedAt ?? Date()))
                            .font(.appSmall)
                            .foregroundColor(AppColors.disabledText)
                    }
                }

                Spacer()
            }
            .padding(AppSpacing.md)
            .background(backgroundColor)
            .cornerRadius(AppSpacing.cornerRadius)
            .overlay(
                RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                    .strokeBorder(borderGradient, lineWidth: borderWidth)
            )
            .shadow(color: shadowColor, radius: shadowRadius, x: 0, y: 4)
            .scaleEffect(isClaiming ? 0.95 : 1.0)
            .opacity(event.claimed ? 0.7 : 1.0)
            .overlay(
                // Confetti overlay
                Group {
                    if showConfetti {
                        ConfettiView()
                    }
                }
            )
        }
        .buttonStyle(PlainButtonStyle())
        .disabled(event.claimed || !event.isClaimable)
        .onAppear {
            if event.isClaimable && !event.claimed {
                startPulseAnimation()
            }
        }
    }

    // MARK: - Computed Properties

    var backgroundColor: Color {
        if event.isClaimable && !event.claimed {
            return AppColors.surfaceElevated
        } else if event.isNegative {
            return AppColors.error.opacity(0.1)
        } else {
            return AppColors.surfaceSubtle
        }
    }

    var borderGradient: LinearGradient {
        if event.isClaimable && !event.claimed {
            return LinearGradient(
                colors: [AppColors.primary, AppColors.accent],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        } else {
            return LinearGradient(
                colors: [Color.clear],
                startPoint: .top,
                endPoint: .bottom
            )
        }
    }

    var borderWidth: CGFloat {
        event.isClaimable && !event.claimed ? 2 : 0
    }

    var shadowColor: Color {
        if event.isClaimable && !event.claimed {
            return AppColors.primary.opacity(isPulsing ? 0.4 : 0.2)
        } else {
            return Color.black.opacity(0.05)
        }
    }

    var shadowRadius: CGFloat {
        event.isClaimable && !event.claimed ? 12 : 6
    }

    var rewardDisplay: some View {
        HStack(spacing: AppSpacing.xs) {
            if let money = event.moneyCost, money > 0 {
                Text("+$\(money) 💰")
                    .font(.appCaption)
            }
            if let energy = event.energyCost, energy > 0 {
                Text("+\(energy) ⚡")
                    .font(.appCaption)
            }
            if let diamonds = event.diamondCost, diamonds > 0 {
                Text("+\(diamonds) 💎")
                    .font(.appCaption)
            }
            if let affinity = event.affinityChange, affinity > 0 {
                Text("+\(affinity) ❤️")
                    .font(.appCaption)
            }
        }
    }

    var lossDisplay: some View {
        HStack(spacing: AppSpacing.xs) {
            Text("Lost:")
                .font(.appCaption)

            if let money = event.moneyCost, money < 0 {
                Text("-$\(abs(money)) 💰")
                    .font(.appCaption)
            }
            if let energy = event.energyCost, energy < 0 {
                Text("\(energy) ⚡")
                    .font(.appCaption)
            }
        }
    }

    // MARK: - Actions

    func claimEvent() {
        // Haptic feedback
        let generator = UIImpactFeedbackGenerator(style: .medium)
        generator.impactOccurred()

        // Animation
        withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
            isClaiming = true
        }

        // Show confetti
        showConfetti = true

        // Send claim to backend
        webSocketService.claimEvent(event)

        // Success haptic after delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            let notification = UINotificationFeedbackGenerator()
            notification.notificationOccurred(.success)
        }

        // Reset animation
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
            withAnimation {
                isClaiming = false
                showConfetti = false
            }
        }
    }

    func startPulseAnimation() {
        withAnimation(.easeInOut(duration: 2.0).repeatForever(autoreverses: true)) {
            isPulsing = true
        }
    }

    func timeAgo(from date: Date) -> String {
        let interval = Date().timeIntervalSince(date)

        if interval < 60 {
            return "Just now"
        } else if interval < 3600 {
            let minutes = Int(interval / 60)
            return "\(minutes) minute\(minutes == 1 ? "" : "s") ago"
        } else if interval < 86400 {
            let hours = Int(interval / 3600)
            return "\(hours) hour\(hours == 1 ? "" : "s") ago"
        } else {
            let days = Int(interval / 86400)
            return "\(days) day\(days == 1 ? "" : "s") ago"
        }
    }
}
```

---

### 5. Updated HomeView

**File:** `Features/Home/Views/HomeView.swift`

**Complete new structure:**
```swift
import SwiftUI

struct HomeView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @EnvironmentObject var tooltipManager: TooltipManager

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: AppSpacing.md) {
                    // Status header
                    StatusHeaderCard()
                        .environmentObject(webSocketService)

                    // Avatar + Stats row
                    HStack(spacing: AppSpacing.md) {
                        AvatarCard()
                            .environmentObject(webSocketService)

                        QuickStatsCard()
                            .environmentObject(webSocketService)
                    }

                    // Life timeline
                    LifeTimelineCard()
                        .environmentObject(webSocketService)

                    // Game controls
                    GameControlsCard()
                        .environmentObject(webSocketService)
                }
                .padding(AppSpacing.md)
            }
            .background(AppColors.background)
        }
        .onAppear {
            AnalyticsManager.shared.trackScreenView("home", screenClass: "HomeView")

            tooltipManager.showTooltipIfNeeded(
                "home_claim_rewards",
                title: "Claim Rewards!",
                message: "Tap glowing life events to claim your rewards!",
                position: .bottom
            )
        }
    }
}
```

---

### 6. GameControlsCard

**File:** `Features/Home/Components/GameControlsCard.swift`

**Purpose:** Speed controls + game action buttons

**Layout:**
```
┌─────────────────────────────────────────────────┐
│  Speed: [−] ▶▶▶ [+]                            │
│  [Start] [Stop] [Restart]                       │
└─────────────────────────────────────────────────┘
```

**Code:**
```swift
struct GameControlsCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    let buttonSpeedValues = [5000, 1000, 500, 50, 20, 1]

    var speedLevel: Int {
        if let index = buttonSpeedValues.firstIndex(of: webSocketService.player.gameSpeed) {
            return index + 1
        }
        return 0
    }

    var body: some View {
        BaseCard {
            VStack(spacing: AppSpacing.md) {
                // Speed controls
                HStack(spacing: AppSpacing.md) {
                    Text("Speed:")
                        .font(.appBody)
                        .foregroundColor(AppColors.primaryText)

                    CozyIconButton(icon: "minus", color: AppColors.secondary, size: 36) {
                        let message = ["type": "speed", "message": "-"]
                        webSocketService.sendMessage(message: message)
                    }

                    // Visual speed indicator
                    HStack(spacing: 2) {
                        ForEach(0..<6) { index in
                            Image(systemName: index < speedLevel ? "play.fill" : "play")
                                .font(.system(size: 10))
                                .foregroundColor(index < speedLevel ? AppColors.primary : AppColors.disabledText)
                        }
                    }

                    CozyIconButton(icon: "plus", color: AppColors.secondary, size: 36) {
                        let message = ["type": "speed", "message": "+"]
                        webSocketService.sendMessage(message: message)
                    }
                }

                Divider()

                // Action buttons
                HStack(spacing: AppSpacing.sm) {
                    PrimaryButton(
                        title: "Start",
                        backgroundColor: AppColors.success
                    ) {
                        let message = ["type": "command", "message": "start"]
                        webSocketService.sendMessage(message: message)
                    }

                    PrimaryButton(
                        title: "Stop",
                        backgroundColor: AppColors.error
                    ) {
                        let message = ["type": "command", "message": "stop"]
                        webSocketService.sendMessage(message: message)
                    }

                    SecondaryButton(
                        title: "Restart",
                        color: AppColors.warning
                    ) {
                        let message = ["type": "command", "message": "restart"]
                        webSocketService.sendMessage(message: message)
                    }
                }
            }
        }
    }
}
```

---

## Animation Specifications

### Pulse Animation (Unclaimed Events)

```swift
// Border glow pulse
.shadow(color: AppColors.primary.opacity(isPulsing ? 0.4 : 0.2), radius: 12)
.animation(.easeInOut(duration: 2.0).repeatForever(autoreverses: true), value: isPulsing)
```

### Claim Animation Sequence (0.8 seconds total)

1. **Tap (0.0s):** Medium haptic feedback
2. **Squish (0.0-0.1s):** Scale to 0.95
3. **Confetti (0.1s):** Burst from center
4. **Reward numbers (0.1-0.5s):** Float up and fade
5. **Success haptic (0.2s):** Success notification feedback
6. **Expand back (0.5-0.8s):** Scale to 1.0
7. **State change (0.8s):** Update to claimed state

### Confetti Effect

Reuse existing `ConfettiView` with these colors:
- AppColors.primary
- AppColors.accent
- AppColors.success
- AppColors.happiness

---

## Backend Requirements

### New Message Type: `claimEvent`

**Client sends:**
```json
{
  "type": "claimEvent",
  "message": {
    "eventId": "abc123...",
    "timestamp": "2025-11-12T14:30:00Z"
  }
}
```

**Backend handler (ws/app.py consumer function):**
```python
elif message_type == "claimEvent":
    event_id = message.get("eventId")
    # Validate event exists and not already claimed
    # Apply rewards to player
    # Send updated player state via "u" message
    # Optional: Track claim analytics
```

**Backend sends updated resources:**
```json
{
  "type": "u",
  "energy": 65,
  "money": 1700,
  "diamonds": 25,
  ...
}
```

### Message Event Categorization

Backend should add `category` field to messageEvent:
```python
messageEvent = {
    "type": "messageEvent",
    "id": str(uuid.uuid4()),
    "message": "Promoted to Senior Manager",
    "category": "career",  # NEW
    "moneyCost": 500,      # Positive = reward
    "prestigeCost": 10,    # Positive = reward
    ...
}
```

Categories:
- `"career"` - Job/work related
- `"social"` - Relationships/dating
- `"achievement"` - Milestones/achievements
- `"education"` - School/learning
- `"health"` - Health/wellness
- `"finance"` - Money (not job related)
- `"random"` - Random events
- `"neutral"` - Informational
- `"negative"` - Bad events

---

## Migration Plan

### Phase 1: Data Model Updates (1 hour)
1. Update `MessageEvent.swift` with new properties
2. Update `WebSocketService` messages → lifeEvents
3. Add claim tracking methods
4. Update message parsing

### Phase 2: Core Components (3 hours)
1. Create `StatusHeaderCard`
2. Create `AvatarCard` and `QuickStatsCard`
3. Create `ResourcePill` helper component

### Phase 3: Event System (4 hours)
1. Create `LifeTimelineCard` with filtering
2. Create `LifeEventCard` with all states
3. Implement claim animation sequence
4. Add confetti effect integration
5. Add haptic feedback

### Phase 4: Integration (2 hours)
1. Update `HomeView` with new layout
2. Create `GameControlsCard`
3. Wire up all WebSocket connections
4. Test claim → backend → update flow

### Phase 5: Polish (2 hours)
1. Add loading states
2. Add empty state (no events yet)
3. Refine animations and timing
4. Add accessibility labels
5. Test on different screen sizes

---

## Success Criteria

### Functional
- ✅ Events display in timeline sorted correctly
- ✅ Claimable events pulse and show rewards
- ✅ Tapping claimable event triggers claim
- ✅ Claim sends message to backend
- ✅ Backend updates resources
- ✅ Event transitions to claimed state
- ✅ Negative events auto-apply and display correctly
- ✅ Filter works (All, Career, Social, Achievements)
- ✅ Unclaimed count badge updates

### Visual
- ✅ Cards use cozy palette from design system
- ✅ Animations feel smooth and delightful
- ✅ Confetti celebrates claim
- ✅ Claimed events fade and show timestamp
- ✅ Status header shows seasonal gradient
- ✅ Avatar card looks polished
- ✅ Timeline has good scrolling performance

### Engagement
- ✅ Players excited to tap and claim rewards
- ✅ Visual feedback makes claiming satisfying
- ✅ Timeline tells a story of character's life
- ✅ Badge count creates "must check" feeling
- ✅ Negative events feel impactful but not punishing

---

## Testing Plan

### Unit Tests
- MessageEvent.isClaimable logic
- MessageEvent.isNegative logic
- Event sorting (unclaimed first)
- Time ago formatting

### Integration Tests
- Claim flow end-to-end
- WebSocket message parsing
- Event list updates on new message
- Filter logic

### Manual Tests
- Claim animation smoothness
- Confetti timing
- Haptic feedback intensity
- Different screen sizes (SE to Pro Max)
- Accessibility with VoiceOver
- Performance with 50+ events

---

## Future Enhancements

### Phase 2 (Post-Launch)
- Event details modal (tap claimed event to see full story)
- Lifetime stats (total money earned, events claimed)
- Event search/filter by date range
- Share event to social media
- Custom event categories/tags
- Event clustering ("3 events at school today")

### Phase 3 (Advanced)
- Event replay/timeline scrubbing
- Photo memories attached to events
- Achievement tracking for claim streaks
- Daily claim bonus (claim all events = bonus)
- Event reactions (emoji reactions to events)

---

## Appendix: Component Hierarchy

```
HomeView
├── StatusHeaderCard
│   ├── SeasonBadge
│   ├── DateBadge
│   ├── SpeedIndicator
│   └── ResourcePill (x4)
├── HStack (Avatar + Stats Row)
│   ├── AvatarCard
│   │   └── SVGImageView
│   └── QuickStatsCard
│       └── CozyStatBar (x4)
├── LifeTimelineCard
│   ├── Header (with unclaimed badge & filter)
│   └── ScrollView
│       └── LazyVStack
│           └── LifeEventCard (x12)
│               └── ConfettiView (conditional)
└── GameControlsCard
    ├── Speed controls
    │   ├── CozyIconButton (minus)
    │   ├── Speed indicator
    │   └── CozyIconButton (plus)
    └── Action buttons
        ├── PrimaryButton (Start)
        ├── PrimaryButton (Stop)
        └── SecondaryButton (Restart)
```

---

## Notes

- All components use existing cozy design system from `COZY_UI_REDESIGN_PLAN.md`
- Reuses `BaseCard`, `CozyStatBar`, `PrimaryButton`, `SecondaryButton`, `CozyIconButton`
- Maintains feature-based architecture from refactoring
- No changes to other tabs (Activities, Dating, Messages, More)
- Backend changes are minimal (add `claimEvent` handler)

---

**Document Status:** Ready for Implementation
**Next Steps:** Set up worktree and create implementation plan
