# BaoLife App Store Launch - Complete Execution Plan

**Created:** November 11, 2025
**Timeline:** 10-11 weeks to launch
**Approach:** Linear phases with deep focus per domain
**Team Structure:** Solo developer + Claude Code working across backend and frontend

---

## Executive Summary

This document outlines the complete execution plan to transform BaoLife from a functional prototype into a commercially viable iOS app ready for App Store launch. The plan covers 72 distinct components organized into 7 phases, with clear separation between backend (Python - `../lichun`) and frontend (Swift - `lichunWebsocket`) work.

**Key Goals:**
1. **Monetization:** Energy refills and time skips as core revenue drivers
2. **Retention:** Achievements, daily rewards, and quests to drive engagement
3. **Conversion:** Onboarding flow that gets new players engaged
4. **Quality:** Production-ready stability, polish, and error handling
5. **Launch:** App Store approval and successful market entry

**Success Metrics:**
- Monetization: 5%+ conversion rate, $5+ average revenue per paying user
- Retention: Day 1 > 40%, Day 7 > 20%, Day 30 > 10%
- Quality: Crash rate < 0.1%, tutorial completion > 70%
- Launch: Successful App Store approval, positive beta feedback (NPS > 7/10)

---

## Repository Structure

**Backend Repository:** `../lichun` (Python WebSocket server)
- Game simulation engine
- Database operations (MySQL)
- WebSocket message handling
- AI integration (OpenAI for conversations)
- Business logic and validation

**Frontend Repository:** `lichunWebsocket` (iOS app with SwiftUI)
- iOS user interface
- WebSocket client communication
- StoreKit integration (IAP)
- Local state management
- UI/UX and animations

---

## Phase 0: Technical Debt Foundation (Week 1)

**Goal:** Fix architectural issues and establish patterns to make future development faster and safer.

### Backend Components (`../lichun`)

#### Component 1: Environment Configuration System
**Files:** `functions.py`, create `.env` file, new `config.py`

**Implementation:**
```python
# Install python-dotenv
# pip install python-dotenv

# Create .env file (add to .gitignore)
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=baolife_user
DATABASE_PASSWORD=your_secure_password
DATABASE_NAME=baolife_db
OPENAI_API_KEY=sk-...
SECRET_KEY=your_secret_key

# Create config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    DATABASE_HOST = os.getenv('DATABASE_HOST', 'localhost')
    DATABASE_PORT = int(os.getenv('DATABASE_PORT', 3306))
    DATABASE_USER = os.getenv('DATABASE_USER')
    DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD')
    DATABASE_NAME = os.getenv('DATABASE_NAME')
    OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
    SECRET_KEY = os.getenv('SECRET_KEY')

    @classmethod
    def validate(cls):
        """Validate all required config is present"""
        required = ['DATABASE_USER', 'DATABASE_PASSWORD', 'OPENAI_API_KEY']
        missing = [key for key in required if not getattr(cls, key)]
        if missing:
            raise ValueError(f"Missing required config: {', '.join(missing)}")

# In functions.py:26, replace hardcoded credentials
from config import Config
Config.validate()

db_connection = mysql.connector.connect(
    host=Config.DATABASE_HOST,
    port=Config.DATABASE_PORT,
    user=Config.DATABASE_USER,
    password=Config.DATABASE_PASSWORD,
    database=Config.DATABASE_NAME
)
```

**Estimated Time:** 2-3 hours

---

#### Component 2: Game Loop Performance Fix
**Files:** Main game loop file (likely `server.py`, `game_loop.py`, or similar)

**Implementation:**
```python
import time

class GameLoop:
    def __init__(self):
        self.target_fps = 60  # Changed from 5000
        self.frame_time = 1.0 / self.target_fps
        self.last_frame_time = time.time()

    def run(self):
        while self.running:
            frame_start = time.time()

            # Process game tick
            self.update_all_players()
            self.process_events()

            # Calculate sleep time to maintain target FPS
            frame_duration = time.time() - frame_start
            sleep_time = max(0, self.frame_time - frame_duration)

            if sleep_time > 0:
                time.sleep(sleep_time)

            # Log performance metrics
            actual_fps = 1.0 / (time.time() - self.last_frame_time)
            if actual_fps < self.target_fps * 0.9:  # 10% tolerance
                logging.warning(f"Game loop running slow: {actual_fps:.1f} FPS")

            self.last_frame_time = time.time()
```

**Benefits:**
- Reduces CPU usage by 98% (5000 → 60 FPS)
- Prevents battery drain on server
- Maintains smooth gameplay (60 FPS is more than sufficient for turn-based life sim)

**Estimated Time:** 2-3 hours

---

#### Component 3: Connection Management Improvements
**Files:** WebSocket connection handler (likely in main server file)

**Implementation:**
```python
import logging
from enum import Enum

class ConnectionState(Enum):
    CONNECTING = "connecting"
    CONNECTED = "connected"
    DISCONNECTED = "disconnected"
    RECONNECTING = "reconnecting"

class ConnectionManager:
    def __init__(self):
        self.max_retries = 10  # Changed from 1000
        self.base_backoff = 1  # seconds
        self.max_backoff = 60  # seconds

    def calculate_backoff(self, retry_count: int) -> float:
        """Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s (max)"""
        backoff = min(self.base_backoff * (2 ** retry_count), self.max_backoff)
        # Add jitter to prevent thundering herd
        import random
        return backoff * (0.5 + random.random())

    async def handle_disconnect(self, player_id: int):
        """Handle client disconnect with graceful degradation"""
        retry_count = 0

        while retry_count < self.max_retries:
            backoff = self.calculate_backoff(retry_count)
            logging.info(f"Player {player_id} disconnect. Retry {retry_count}/{self.max_retries} in {backoff:.1f}s")

            await asyncio.sleep(backoff)

            # Attempt reconnection
            if await self.attempt_reconnect(player_id):
                logging.info(f"Player {player_id} reconnected successfully")
                return True

            retry_count += 1

        # Max retries exceeded - switch to offline mode
        logging.warning(f"Player {player_id} failed to reconnect. Switching to offline mode.")
        await self.enable_offline_mode(player_id)
        return False

    async def enable_offline_mode(self, player_id: int):
        """Continue game simulation but queue updates for later"""
        # Mark player as offline
        # Continue running simulation
        # Queue all updates in memory/database
        # Send catch-up update when player reconnects
        pass

# Add structured logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    handlers=[
        logging.FileHandler('baolife.log'),
        logging.StreamHandler()
    ]
)
```

**Estimated Time:** 4-5 hours

---

#### Component 4: Database Connection Pooling
**Files:** `functions.py` or new `database.py`

**Implementation:**
```python
import mysql.connector
from mysql.connector import pooling
from config import Config

class DatabaseManager:
    _instance = None
    _pool = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialize_pool()
        return cls._instance

    def _initialize_pool(self):
        """Create connection pool (singleton)"""
        self._pool = pooling.MySQLConnectionPool(
            pool_name="baolife_pool",
            pool_size=10,  # Adjust based on concurrent users
            pool_reset_session=True,
            host=Config.DATABASE_HOST,
            port=Config.DATABASE_PORT,
            user=Config.DATABASE_USER,
            password=Config.DATABASE_PASSWORD,
            database=Config.DATABASE_NAME
        )
        logging.info("Database connection pool initialized")

    def get_connection(self):
        """Get connection from pool"""
        try:
            connection = self._pool.get_connection()
            if not connection.is_connected():
                connection.reconnect()
            return connection
        except mysql.connector.Error as e:
            logging.error(f"Database connection error: {e}")
            raise

    def execute_query(self, query: str, params: tuple = None, fetch: bool = True):
        """Execute query with automatic connection management"""
        connection = None
        cursor = None
        try:
            connection = self.get_connection()
            cursor = connection.cursor(dictionary=True)
            cursor.execute(query, params or ())

            if fetch:
                result = cursor.fetchall()
                return result
            else:
                connection.commit()
                return cursor.lastrowid
        except Exception as e:
            if connection:
                connection.rollback()
            logging.error(f"Query error: {e}")
            raise
        finally:
            if cursor:
                cursor.close()
            if connection:
                connection.close()  # Returns to pool

# Usage throughout codebase
db = DatabaseManager()
players = db.execute_query("SELECT * FROM players WHERE id = %s", (player_id,))
```

**Estimated Time:** 3-4 hours

---

### Frontend Components (`lichunWebsocket`)

#### Component 5: ContentView Refactoring
**Files:**
- `ContentView.swift` (1844 lines → ~200 lines)
- Create: `HeaderView.swift`, `HomeView.swift`, `CharacterStatsView.swift`

**Implementation:**

**New ContentView.swift (main container only):**
```swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @EnvironmentObject var appViewModel: AppViewModel

    var body: some View {
        ZStack {
            if webSocketService.appLoaded {
                if webSocketService.person.id != 0 {
                    MainGameView()
                } else if webSocketService.creating {
                    CharacterCreationFlow()
                } else if webSocketService.person.dead {
                    DeathView()
                }
            } else {
                LoadingView()
            }

            // Event modal overlay
            if let question = webSocketService.currentQuestion {
                EventModalView(question: question)
            }
        }
        .onAppear {
            webSocketService.connect()
        }
    }
}

struct MainGameView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @EnvironmentObject var appViewModel: AppViewModel

    var body: some View {
        VStack(spacing: 0) {
            HeaderView()

            TabView(selection: $appViewModel.selectedTab) {
                HomeView()
                    .tabItem { Label("Home", systemImage: "house.fill") }
                    .tag(0)

                ActivityView()
                    .tabItem { Label("Activities", systemImage: "figure.run") }
                    .tag(1)

                DatingView()
                    .tabItem { Label("Dating", systemImage: "heart.fill") }
                    .tag(2)

                MessagesView()
                    .tabItem { Label("Messages", systemImage: "message.fill") }
                    .tag(3)

                MoreView()
                    .tabItem { Label("More", systemImage: "ellipsis") }
                    .tag(4)
            }
        }
    }
}
```

**New HeaderView.swift:**
```swift
import SwiftUI

struct HeaderView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @State private var showTimeControl = false
    @State private var showEnergyRefill = false

    var body: some View {
        VStack(spacing: BaoSpacing.sm) {
            // Date and Time
            HStack {
                Button(action: { showTimeControl = true }) {
                    HStack(spacing: BaoSpacing.xs) {
                        Image(systemName: "calendar")
                        Text(webSocketService.player.dateFormatted)
                        Text("•")
                        Text(webSocketService.player.time)
                    }
                    .font(BaoTypography.caption)
                }

                Spacer()

                Text(webSocketService.player.season.capitalized)
                    .font(BaoTypography.caption)
            }

            // Resources
            HStack(spacing: BaoSpacing.md) {
                // Energy
                Button(action: { showEnergyRefill = true }) {
                    ResourceBadge(
                        icon: "bolt.fill",
                        value: "\(webSocketService.person.calcEnergy)/\(webSocketService.person.maxEnergy)",
                        color: .yellow
                    )
                }

                // Money
                ResourceBadge(
                    icon: "dollarsign.circle.fill",
                    value: webSocketService.person.money.formatted(),
                    color: .green
                )

                // Diamonds
                ResourceBadge(
                    icon: "diamond.fill",
                    value: "\(webSocketService.person.diamonds)",
                    color: .blue
                )

                // Prestige
                ResourceBadge(
                    icon: "star.fill",
                    value: "\(webSocketService.person.prestige)",
                    color: .purple
                )
            }
        }
        .padding(BaoSpacing.md)
        .background(BaoColors.cardBackground)
        .sheet(isPresented: $showTimeControl) {
            TimeControlView()
        }
        .sheet(isPresented: $showEnergyRefill) {
            EnergyRefillView()
        }
    }
}

struct ResourceBadge: View {
    let icon: String
    let value: String
    let color: Color

    var body: some View {
        HStack(spacing: 4) {
            Image(systemName: icon)
                .foregroundColor(color)
            Text(value)
                .font(BaoTypography.caption)
        }
        .padding(.horizontal, BaoSpacing.sm)
        .padding(.vertical, BaoSpacing.xs)
        .background(color.opacity(0.1))
        .cornerRadius(8)
    }
}
```

**New HomeView.swift:**
```swift
import SwiftUI

struct HomeView: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        ScrollView {
            VStack(spacing: BaoSpacing.lg) {
                CharacterStatsView()

                CurrentActivitySection()

                RecentEventsSection()

                RelationshipsSection()
            }
            .padding(BaoSpacing.md)
        }
    }
}
```

**New CharacterStatsView.swift:**
```swift
import SwiftUI

struct CharacterStatsView: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var person: Person {
        webSocketService.person
    }

    var body: some View {
        VStack(spacing: BaoSpacing.md) {
            // Character image and name
            HStack {
                SVGImageView(urlString: person.image)
                    .frame(width: 80, height: 80)
                    .clipShape(Circle())

                VStack(alignment: .leading, spacing: BaoSpacing.xs) {
                    Text(person.fullName)
                        .font(BaoTypography.heading2)

                    Text("Age \(person.ageYears)")
                        .font(BaoTypography.body)
                        .foregroundColor(.secondary)

                    if let occupation = person.occupation {
                        Text(occupation)
                            .font(BaoTypography.caption)
                    }
                }

                Spacer()
            }

            // Stats grid
            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: BaoSpacing.sm) {
                StatRow(label: "Health", value: "\(person.health)%", color: .red)
                StatRow(label: "Happiness", value: "\(person.happiness)%", color: .yellow)
                StatRow(label: "Intelligence", value: "\(person.smarts)", color: .blue)
                StatRow(label: "Looks", value: "\(person.looks)", color: .pink)
            }
        }
        .padding(BaoSpacing.md)
        .baoCard()
    }
}

struct StatRow: View {
    let label: String
    let value: String
    let color: Color

    var body: some View {
        HStack {
            Text(label)
                .font(BaoTypography.caption)
                .foregroundColor(.secondary)
            Spacer()
            Text(value)
                .font(BaoTypography.body)
                .foregroundColor(color)
        }
    }
}
```

**Estimated Time:** 6-8 hours

---

#### Component 6: Design System
**Files:** Create `DesignSystem.swift`

**Implementation:**
```swift
import SwiftUI

// MARK: - Colors
enum BaoColors {
    static let primary = Color(hex: "4A90E2")
    static let secondary = Color(hex: "50C878")
    static let accent = Color(hex: "FF6B6B")

    static let background = Color(uiColor: .systemBackground)
    static let secondaryBackground = Color(uiColor: .secondarySystemBackground)
    static let cardBackground = Color(uiColor: .tertiarySystemBackground)

    static let textPrimary = Color(uiColor: .label)
    static let textSecondary = Color(uiColor: .secondaryLabel)

    static let success = Color.green
    static let warning = Color.orange
    static let error = Color.red
    static let info = Color.blue
}

// MARK: - Spacing
enum BaoSpacing {
    static let xs: CGFloat = 4
    static let sm: CGFloat = 8
    static let md: CGFloat = 16
    static let lg: CGFloat = 24
    static let xl: CGFloat = 32
    static let xxl: CGFloat = 48
}

// MARK: - Typography
enum BaoTypography {
    static let largeTitle = Font.system(size: 34, weight: .bold)
    static let heading1 = Font.system(size: 28, weight: .bold)
    static let heading2 = Font.system(size: 22, weight: .semibold)
    static let heading3 = Font.system(size: 18, weight: .semibold)
    static let body = Font.system(size: 16, weight: .regular)
    static let bodyBold = Font.system(size: 16, weight: .semibold)
    static let caption = Font.system(size: 14, weight: .regular)
    static let small = Font.system(size: 12, weight: .regular)
}

// MARK: - Corner Radius
enum BaoRadius {
    static let sm: CGFloat = 8
    static let md: CGFloat = 12
    static let lg: CGFloat = 16
    static let xl: CGFloat = 24
}

// MARK: - Shadow
enum BaoShadow {
    static let sm = Shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
    static let md = Shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
    static let lg = Shadow(color: .black.opacity(0.2), radius: 16, x: 0, y: 8)
}

// MARK: - Helper Extensions
extension Color {
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&int)
        let a, r, g, b: UInt64
        switch hex.count {
        case 3: // RGB (12-bit)
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6: // RGB (24-bit)
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8: // ARGB (32-bit)
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (a, r, g, b) = (255, 0, 0, 0)
        }
        self.init(
            .sRGB,
            red: Double(r) / 255,
            green: Double(g) / 255,
            blue:  Double(b) / 255,
            opacity: Double(a) / 255
        )
    }
}

struct Shadow {
    let color: Color
    let radius: CGFloat
    let x: CGFloat
    let y: CGFloat
}
```

**Estimated Time:** 2-3 hours

---

#### Component 7: WebSocketService Improvements
**Files:** `WebSocketService.swift`

**Implementation:**
```swift
// Add connection state tracking
enum ConnectionState {
    case connecting
    case connected
    case disconnected
    case reconnecting
}

class WebSocketService: ObservableObject {
    // Add published connection state
    @Published var connectionState: ConnectionState = .disconnected

    // Add offline message queue
    private var offlineMessageQueue: [[String: Any]] = []

    // Improved connection handling
    func connect() {
        connectionState = .connecting
        // ... existing connection code
    }

    func didConnect() {
        connectionState = .connected

        // Send queued messages
        if !offlineMessageQueue.isEmpty {
            print("Sending \(offlineMessageQueue.count) queued messages")
            for message in offlineMessageQueue {
                sendMessage(message: message)
            }
            offlineMessageQueue.removeAll()
        }
    }

    func didDisconnect() {
        connectionState = .disconnected
        // Existing reconnection logic
    }

    // Improved message sending with offline queue
    func sendMessage(message: [String: Any]) {
        if connectionState == .connected {
            // Send immediately
            do {
                let jsonData = try JSONSerialization.data(withJSONObject: message)
                let jsonString = String(data: jsonData, encoding: .utf8)!
                webSocket?.send(.string(jsonString))
            } catch {
                print("Error serializing message: \(error)")
            }
        } else {
            // Queue for later
            print("Queueing message (offline): \(message["type"] ?? "unknown")")
            offlineMessageQueue.append(message)
        }
    }

    // Better error propagation
    @Published var lastError: WebSocketError?

    enum WebSocketError: Identifiable {
        case connectionFailed(String)
        case messageError(String)
        case serverError(String)

        var id: String {
            switch self {
            case .connectionFailed(let msg): return "conn_\(msg)"
            case .messageError(let msg): return "msg_\(msg)"
            case .serverError(let msg): return "srv_\(msg)"
            }
        }

        var userMessage: String {
            switch self {
            case .connectionFailed: return "Connection lost. Reconnecting..."
            case .messageError: return "Action couldn't complete. Try again?"
            case .serverError(let msg): return msg
            }
        }
    }

    // Handle server error messages
    func handleErrorMessage(_ data: [String: Any]) {
        if let errorCode = data["error_code"] as? String,
           let message = data["message"] as? String {
            lastError = .serverError(message)
        }
    }
}
```

**Estimated Time:** 4-5 hours

---

#### Component 8: Custom View Modifiers
**Files:** Create `ViewModifiers.swift`

**Implementation:**
```swift
import SwiftUI

// MARK: - Card Modifier
struct BaoCardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .background(BaoColors.cardBackground)
            .cornerRadius(BaoRadius.md)
            .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
    }
}

extension View {
    func baoCard() -> some View {
        modifier(BaoCardModifier())
    }
}

// MARK: - Button Modifier
struct BaoPrimaryButtonModifier: ViewModifier {
    let isEnabled: Bool

    func body(content: Content) -> some View {
        content
            .font(BaoTypography.bodyBold)
            .foregroundColor(.white)
            .frame(maxWidth: .infinity)
            .padding(.vertical, BaoSpacing.md)
            .background(isEnabled ? BaoColors.primary : Color.gray)
            .cornerRadius(BaoRadius.md)
            .opacity(isEnabled ? 1.0 : 0.6)
    }
}

extension View {
    func baoPrimaryButton(enabled: Bool = true) -> some View {
        modifier(BaoPrimaryButtonModifier(isEnabled: enabled))
    }
}

// MARK: - Text Field Modifier
struct BaoTextFieldModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding(BaoSpacing.md)
            .background(BaoColors.secondaryBackground)
            .cornerRadius(BaoRadius.sm)
            .font(BaoTypography.body)
    }
}

extension View {
    func baoTextField() -> some View {
        modifier(BaoTextFieldModifier())
    }
}

// MARK: - Loading Overlay
struct LoadingOverlayModifier: ViewModifier {
    let isLoading: Bool
    let message: String?

    func body(content: Content) -> some View {
        ZStack {
            content
                .disabled(isLoading)

            if isLoading {
                Color.black.opacity(0.4)
                    .ignoresSafeArea()

                VStack(spacing: BaoSpacing.md) {
                    ProgressView()
                        .scaleEffect(1.5)
                        .tint(.white)

                    if let message = message {
                        Text(message)
                            .font(BaoTypography.body)
                            .foregroundColor(.white)
                    }
                }
                .padding(BaoSpacing.xl)
                .background(BaoColors.cardBackground)
                .cornerRadius(BaoRadius.lg)
            }
        }
    }
}

extension View {
    func loadingOverlay(isLoading: Bool, message: String? = nil) -> some View {
        modifier(LoadingOverlayModifier(isLoading: isLoading, message: message))
    }
}
```

**Estimated Time:** 2-3 hours

---

**Phase 0 Total Estimated Time:** 25-33 hours (~1 week at 30 hours/week)

---

## Phase 1: Monetization Foundation (Weeks 2-3)

**Goal:** Implement energy refills and time skips as the core monetization loop.

### Backend Components (`../lichun`)

#### Component 9: Energy Refill System
**Files:** Create `monetization/energy_refills.py`, update database schema

**Database Schema:**
```sql
CREATE TABLE energy_refill_purchases (
    id INT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    refill_type ENUM('small', 'medium', 'full', 'unlimited_24h') NOT NULL,
    energy_amount INT NOT NULL,
    diamond_cost INT NOT NULL,
    energy_before INT NOT NULL,
    energy_after INT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (player_id) REFERENCES players(id),
    INDEX idx_player_timestamp (player_id, timestamp)
);

-- Add unlimited energy tracking to players table
ALTER TABLE players ADD COLUMN unlimited_energy_until DATETIME NULL;
```

**Python Implementation:**
```python
from datetime import datetime, timedelta
from config import Config
from database import DatabaseManager

db = DatabaseManager()

REFILL_TIERS = {
    'small': {'energy': 20, 'diamonds': 10},
    'medium': {'energy': 50, 'diamonds': 20},
    'full': {'energy': 100, 'diamonds': 35},
    'unlimited_24h': {'energy': 0, 'diamonds': 50}  # Special case
}

def purchase_energy_refill(player_id: int, refill_type: str) -> dict:
    """
    Handle energy refill purchase
    Returns: {'success': bool, 'message': str, 'new_balance': dict}
    """
    try:
        # Validate refill type
        if refill_type not in REFILL_TIERS:
            return {'success': False, 'message': 'Invalid refill type'}

        tier = REFILL_TIERS[refill_type]
        diamond_cost = tier['diamonds']
        energy_amount = tier['energy']

        # Get player current state
        player = db.execute_query(
            "SELECT diamonds, energy, max_energy, unlimited_energy_until FROM players WHERE id = %s",
            (player_id,)
        )[0]

        # Check diamond balance
        if player['diamonds'] < diamond_cost:
            return {
                'success': False,
                'message': f'Not enough diamonds. Need {diamond_cost}, have {player["diamonds"]}'
            }

        # Calculate new balances
        new_diamonds = player['diamonds'] - diamond_cost

        if refill_type == 'unlimited_24h':
            # Special case: unlimited energy for 24 hours
            unlimited_until = datetime.now() + timedelta(hours=24)
            new_energy = player['max_energy']  # Fill to max immediately

            db.execute_query(
                """UPDATE players
                   SET diamonds = %s, energy = %s, unlimited_energy_until = %s
                   WHERE id = %s""",
                (new_diamonds, new_energy, unlimited_until, player_id),
                fetch=False
            )
        else:
            # Normal refill: add energy up to max
            new_energy = min(player['energy'] + energy_amount, player['max_energy'])

            db.execute_query(
                "UPDATE players SET diamonds = %s, energy = %s WHERE id = %s",
                (new_diamonds, new_energy, player_id),
                fetch=False
            )

        # Log transaction
        db.execute_query(
            """INSERT INTO energy_refill_purchases
               (player_id, refill_type, energy_amount, diamond_cost, energy_before, energy_after)
               VALUES (%s, %s, %s, %s, %s, %s)""",
            (player_id, refill_type, energy_amount, diamond_cost, player['energy'], new_energy),
            fetch=False
        )

        logging.info(f"Player {player_id} purchased {refill_type} refill for {diamond_cost} diamonds")

        return {
            'success': True,
            'message': 'Energy refilled successfully',
            'new_balance': {
                'energy': new_energy,
                'diamonds': new_diamonds,
                'unlimited_until': unlimited_until.isoformat() if refill_type == 'unlimited_24h' else None
            }
        }

    except Exception as e:
        logging.error(f"Energy refill error for player {player_id}: {e}")
        return {'success': False, 'message': 'Server error. Please try again.'}

def check_unlimited_energy(player_id: int) -> bool:
    """Check if player currently has unlimited energy active"""
    player = db.execute_query(
        "SELECT unlimited_energy_until FROM players WHERE id = %s",
        (player_id,)
    )[0]

    if player['unlimited_energy_until']:
        if datetime.now() < player['unlimited_energy_until']:
            return True
        else:
            # Expired, clear it
            db.execute_query(
                "UPDATE players SET unlimited_energy_until = NULL WHERE id = %s",
                (player_id,),
                fetch=False
            )

    return False

# WebSocket message handler
def handle_purchase_energy_refill(player_id: int, message_data: dict):
    """Handle 'purchaseEnergyRefill' message from client"""
    refill_type = message_data.get('refillType')
    result = purchase_energy_refill(player_id, refill_type)

    if result['success']:
        # Send success response with updated balances
        send_to_client(player_id, {
            'type': 'purchaseComplete',
            'category': 'energy',
            'success': True,
            'newBalance': result['new_balance']
        })
    else:
        # Send error response
        send_to_client(player_id, {
            'type': 'error',
            'error_code': 'PURCHASE_FAILED',
            'message': result['message']
        })
```

**Estimated Time:** 5-6 hours

---

#### Component 10: Time Skip System
**Files:** Create `monetization/time_skips.py`, update database schema

**Database Schema:**
```sql
CREATE TABLE time_skip_purchases (
    id INT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    skip_type ENUM('1hour', '1day', '1week', 'next_event') NOT NULL,
    diamond_cost INT NOT NULL,
    time_before DATETIME NOT NULL,
    time_after DATETIME NOT NULL,
    events_simulated INT DEFAULT 0,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (player_id) REFERENCES players(id),
    INDEX idx_player_timestamp (player_id, timestamp)
);
```

**Python Implementation:**
```python
from datetime import datetime, timedelta
import logging

SKIP_TIERS = {
    '1hour': {'duration': timedelta(hours=1), 'diamonds': 5},
    '1day': {'duration': timedelta(days=1), 'diamonds': 25},
    '1week': {'duration': timedelta(weeks=1), 'diamonds': 100},
    'next_event': {'duration': None, 'diamonds': 50}  # Variable duration
}

def purchase_time_skip(player_id: int, skip_type: str) -> dict:
    """
    Handle time skip purchase and simulation
    Returns: {'success': bool, 'message': str, 'summary': dict}
    """
    try:
        # Validate skip type
        if skip_type not in SKIP_TIERS:
            return {'success': False, 'message': 'Invalid skip type'}

        tier = SKIP_TIERS[skip_type]
        diamond_cost = tier['diamonds']

        # Get player current state
        player = db.execute_query(
            "SELECT diamonds, game_time, next_major_event_time FROM players WHERE id = %s",
            (player_id,)
        )[0]

        # Check diamond balance
        if player['diamonds'] < diamond_cost:
            return {
                'success': False,
                'message': f'Not enough diamonds. Need {diamond_cost}, have {player["diamonds"]}'
            }

        # Calculate skip duration
        time_before = player['game_time']

        if skip_type == 'next_event':
            # Skip to next scheduled event
            if player['next_major_event_time']:
                time_after = player['next_major_event_time']
            else:
                # No upcoming event, skip 1 day
                time_after = time_before + timedelta(days=1)
        else:
            time_after = time_before + tier['duration']

        # Simulate events during skipped period
        events_summary = simulate_time_period(player_id, time_before, time_after)

        # Deduct diamonds
        new_diamonds = player['diamonds'] - diamond_cost
        db.execute_query(
            "UPDATE players SET diamonds = %s, game_time = %s WHERE id = %s",
            (new_diamonds, time_after, player_id),
            fetch=False
        )

        # Log transaction
        db.execute_query(
            """INSERT INTO time_skip_purchases
               (player_id, skip_type, diamond_cost, time_before, time_after, events_simulated)
               VALUES (%s, %s, %s, %s, %s, %s)""",
            (player_id, skip_type, diamond_cost, time_before, time_after, len(events_summary['events'])),
            fetch=False
        )

        logging.info(f"Player {player_id} skipped {skip_type} for {diamond_cost} diamonds")

        return {
            'success': True,
            'message': 'Time skipped successfully',
            'summary': {
                'diamonds': new_diamonds,
                'new_time': time_after.isoformat(),
                'duration_hours': (time_after - time_before).total_seconds() / 3600,
                'events': events_summary['events'],
                'stat_changes': events_summary['stat_changes']
            }
        }

    except Exception as e:
        logging.error(f"Time skip error for player {player_id}: {e}")
        return {'success': False, 'message': 'Server error. Please try again.'}

def simulate_time_period(player_id: int, start_time: datetime, end_time: datetime) -> dict:
    """
    Simulate game events during time skip period
    Returns summary of events and stat changes
    """
    events = []
    stat_changes = {
        'money': 0,
        'health': 0,
        'happiness': 0,
        'relationships': []
    }

    # Get player's current activities and occupation
    player_state = get_player_full_state(player_id)

    current_time = start_time
    simulation_step = timedelta(hours=1)  # Simulate hour by hour

    while current_time < end_time:
        # Check for scheduled events at this time
        scheduled_events = get_scheduled_events(player_id, current_time)
        for event in scheduled_events:
            events.append(process_event(player_id, event))

        # Simulate ongoing activities (work, school, etc.)
        if player_state['current_occupation']:
            if is_work_hours(current_time):
                # Earn money, gain skills
                hourly_wage = player_state['occupation_salary'] / (40 * 4)  # Per hour
                stat_changes['money'] += hourly_wage

                events.append({
                    'type': 'work',
                    'time': current_time.isoformat(),
                    'description': f'Worked at {player_state["occupation_name"]}',
                    'money_earned': hourly_wage
                })

        # Natural stat changes (health decay, happiness decay, etc.)
        stat_changes['health'] -= 0.1  # Slight health decay
        stat_changes['happiness'] -= 0.2  # Slight happiness decay

        # Relationship decay (affinity drops if not maintained)
        for relationship in player_state['relationships']:
            decay_amount = 0.5 / 24  # 0.5 affinity per day
            stat_changes['relationships'].append({
                'person_id': relationship['id'],
                'affinity_change': -decay_amount
            })

        current_time += simulation_step

    # Apply all stat changes to database
    apply_stat_changes(player_id, stat_changes)

    # Check for death during simulation
    player_after = db.execute_query(
        "SELECT health, age_years FROM players WHERE id = %s",
        (player_id,)
    )[0]

    if player_after['health'] <= 0 or player_after['age_years'] >= 100:
        events.append({
            'type': 'death',
            'time': current_time.isoformat(),
            'description': 'Your character passed away during the time skip',
            'cause': 'old_age' if player_after['age_years'] >= 100 else 'health'
        })

    return {
        'events': events,
        'stat_changes': stat_changes
    }

# WebSocket message handler
def handle_purchase_time_skip(player_id: int, message_data: dict):
    """Handle 'purchaseTimeSkip' message from client"""
    skip_type = message_data.get('skipType')
    result = purchase_time_skip(player_id, skip_type)

    if result['success']:
        send_to_client(player_id, {
            'type': 'timeSkipComplete',
            'success': True,
            'summary': result['summary']
        })

        # Send full state update after time skip
        send_full_player_state(player_id)
    else:
        send_to_client(player_id, {
            'type': 'error',
            'error_code': 'TIME_SKIP_FAILED',
            'message': result['message']
        })
```

**Estimated Time:** 8-10 hours (complex simulation logic)

---

#### Component 11: Diamond Economy Adjustments
**Files:** Update existing event handlers throughout codebase

**Implementation:**
```python
# Add diamond rewards to existing events

DIAMOND_REWARDS = {
    'level_up': 10,
    'complete_school_year': 15,
    'graduate_high_school': 25,
    'graduate_college': 50,
    'get_first_job': 15,
    'promotion': 20,
    'become_ceo': 100,
    'get_married': 25,
    'have_first_child': 30,
    'reach_age_milestone': 20,  # Every 10 years
}

def award_diamonds(player_id: int, reason: str, amount: int):
    """Award diamonds to player with tracking"""
    db.execute_query(
        "UPDATE players SET diamonds = diamonds + %s WHERE id = %s",
        (amount, player_id),
        fetch=False
    )

    # Log diamond earn
    db.execute_query(
        """INSERT INTO diamond_transactions
           (player_id, transaction_type, amount, reason, timestamp)
           VALUES (%s, 'earn', %s, %s, NOW())""",
        (player_id, amount, reason),
        fetch=False
    )

    # Send notification to client
    send_to_client(player_id, {
        'type': 'diamondEarned',
        'amount': amount,
        'reason': reason,
        'total': get_player_diamonds(player_id)
    })

    logging.info(f"Player {player_id} earned {amount} diamonds: {reason}")

# Update existing event handlers to call award_diamonds
# Example in education system:
def handle_graduation(player_id: int, education_level: str):
    # ... existing graduation logic ...

    if education_level == 'high_school':
        award_diamonds(player_id, 'graduate_high_school', 25)
    elif education_level == 'college':
        award_diamonds(player_id, 'graduate_college', 50)

# Example in career system:
def handle_promotion(player_id: int):
    # ... existing promotion logic ...

    award_diamonds(player_id, 'promotion', 20)

    # Check if reached CEO
    player = get_player(player_id)
    if player['occupation_title'] == 'CEO':
        award_diamonds(player_id, 'become_ceo', 100)

# Add to birthday handler:
def handle_birthday(player_id: int, new_age: int):
    # ... existing birthday logic ...

    # Every 10 years milestone
    if new_age % 10 == 0 and new_age >= 10:
        award_diamonds(player_id, f'reach_age_{new_age}', 20)
```

**Database Schema:**
```sql
CREATE TABLE diamond_transactions (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    transaction_type ENUM('earn', 'spend') NOT NULL,
    amount INT NOT NULL,
    reason VARCHAR(200),
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (player_id) REFERENCES players(id),
    INDEX idx_player_timestamp (player_id, timestamp)
);
```

**Estimated Time:** 4-5 hours (updating existing event handlers)

---

#### Component 12: Purchase Validation & Anti-Cheat
**Files:** Create `monetization/validation.py`

**Implementation:**
```python
import hashlib
import time
from functools import wraps

# Rate limiting decorator
def rate_limit(max_calls: int, time_window: int):
    """
    Rate limit decorator
    max_calls: Maximum calls allowed
    time_window: Time window in seconds
    """
    calls = {}

    def decorator(func):
        @wraps(func)
        def wrapper(player_id: int, *args, **kwargs):
            now = time.time()

            # Clean old entries
            calls[player_id] = [t for t in calls.get(player_id, []) if now - t < time_window]

            # Check rate limit
            if len(calls.get(player_id, [])) >= max_calls:
                logging.warning(f"Rate limit exceeded for player {player_id} on {func.__name__}")
                return {
                    'success': False,
                    'message': 'Too many requests. Please wait and try again.'
                }

            # Add this call
            if player_id not in calls:
                calls[player_id] = []
            calls[player_id].append(now)

            return func(player_id, *args, **kwargs)
        return wrapper
    return decorator

# Apply rate limiting to purchase functions
@rate_limit(max_calls=10, time_window=60)  # 10 purchases per minute max
def purchase_energy_refill(player_id: int, refill_type: str) -> dict:
    # ... existing implementation ...
    pass

@rate_limit(max_calls=5, time_window=60)  # 5 time skips per minute max
def purchase_time_skip(player_id: int, skip_type: str) -> dict:
    # ... existing implementation ...
    pass

# Idempotency for IAP validation
class IdempotencyManager:
    """Prevent double-processing of IAP receipts"""

    @staticmethod
    def generate_key(player_id: int, transaction_id: str) -> str:
        """Generate idempotency key from transaction"""
        return hashlib.sha256(f"{player_id}:{transaction_id}".encode()).hexdigest()

    @staticmethod
    def check_processed(idempotency_key: str) -> bool:
        """Check if transaction already processed"""
        result = db.execute_query(
            "SELECT COUNT(*) as count FROM processed_transactions WHERE idempotency_key = %s",
            (idempotency_key,)
        )[0]
        return result['count'] > 0

    @staticmethod
    def mark_processed(idempotency_key: str, player_id: int, transaction_data: dict):
        """Mark transaction as processed"""
        db.execute_query(
            """INSERT INTO processed_transactions
               (idempotency_key, player_id, transaction_data, timestamp)
               VALUES (%s, %s, %s, NOW())""",
            (idempotency_key, player_id, json.dumps(transaction_data)),
            fetch=False
        )

def validate_iap_receipt(player_id: int, receipt_data: str, transaction_id: str) -> dict:
    """
    Validate IAP purchase with Apple
    This would integrate with Apple's receipt validation API
    """
    # Generate idempotency key
    idem_key = IdempotencyManager.generate_key(player_id, transaction_id)

    # Check if already processed
    if IdempotencyManager.check_processed(idem_key):
        logging.warning(f"Duplicate IAP transaction attempted: {transaction_id}")
        return {
            'success': False,
            'message': 'Transaction already processed'
        }

    # Validate with Apple (simplified - real implementation would use Apple's API)
    # import requests
    # response = requests.post(
    #     'https://buy.itunes.apple.com/verifyReceipt',
    #     json={'receipt-data': receipt_data}
    # )
    # validation_result = response.json()

    # For now, assume validation succeeds
    validation_result = {'status': 0}  # 0 = valid

    if validation_result['status'] == 0:
        # Mark as processed
        IdempotencyManager.mark_processed(idem_key, player_id, {
            'transaction_id': transaction_id,
            'receipt_data': receipt_data,
            'validation_result': validation_result
        })

        # Award diamonds based on product ID
        # (This would be extracted from the receipt)
        product_id = 'diamond1'  # Example
        diamond_amounts = {
            'diamond1': 100,
            'diamond2': 300
        }

        diamond_amount = diamond_amounts.get(product_id, 0)
        if diamond_amount > 0:
            award_diamonds(player_id, f'iap_purchase_{product_id}', diamond_amount)

        return {
            'success': True,
            'diamonds_awarded': diamond_amount
        }
    else:
        logging.error(f"IAP validation failed for player {player_id}: {validation_result}")
        return {
            'success': False,
            'message': 'Purchase validation failed'
        }

# Database schema for tracking
CREATE TABLE processed_transactions (
    idempotency_key VARCHAR(64) PRIMARY KEY,
    player_id INT NOT NULL,
    transaction_data JSON,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_player_timestamp (player_id, timestamp)
);
```

**Estimated Time:** 4-5 hours

---

### Frontend Components (`lichunWebsocket`)

#### Component 13: Energy Refill UI
**Files:** Create `EnergyRefillView.swift`

**Implementation:**
```swift
import SwiftUI

struct EnergyRefillView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @Environment(\.dismiss) var dismiss

    @State private var selectedTier: RefillTier?
    @State private var showingConfirmation = false
    @State private var isProcessing = false

    enum RefillTier: String, CaseIterable {
        case small = "small"
        case medium = "medium"
        case full = "full"
        case unlimited24h = "unlimited_24h"

        var displayName: String {
            switch self {
            case .small: return "Small Refill"
            case .medium: return "Medium Refill"
            case .full: return "Full Refill"
            case .unlimited24h: return "24h Unlimited"
            }
        }

        var energy: Int {
            switch self {
            case .small: return 20
            case .medium: return 50
            case .full: return 100
            case .unlimited24h: return 0
            }
        }

        var diamondCost: Int {
            switch self {
            case .small: return 10
            case .medium: return 20
            case .full: return 35
            case .unlimited24h: return 50
            }
        }

        var description: String {
            switch self {
            case .small: return "Quick energy boost"
            case .medium: return "Good for several activities"
            case .full: return "Refill to maximum"
            case .unlimited24h: return "No energy cost for 24 hours!"
            }
        }

        var badge: String? {
            switch self {
            case .full: return "Popular"
            case .unlimited24h: return "Best Value"
            default: return nil
            }
        }
    }

    var body: some View {
        NavigationView {
            VStack(spacing: BaoSpacing.lg) {
                // Current energy status
                CurrentEnergyCard()

                // Refill options
                ScrollView {
                    VStack(spacing: BaoSpacing.md) {
                        ForEach(RefillTier.allCases, id: \.self) { tier in
                            RefillOptionCard(
                                tier: tier,
                                canAfford: webSocketService.person.diamonds >= tier.diamondCost
                            )
                            .onTapGesture {
                                if webSocketService.person.diamonds >= tier.diamondCost {
                                    selectedTier = tier
                                    showingConfirmation = true
                                }
                            }
                        }
                    }
                    .padding(BaoSpacing.md)
                }
            }
            .navigationTitle("Energy Refill")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Close") {
                        dismiss()
                    }
                }
            }
            .confirmationDialog(
                "Purchase \(selectedTier?.displayName ?? "")?",
                isPresented: $showingConfirmation,
                presenting: selectedTier
            ) { tier in
                Button("Buy for \(tier.diamondCost) 💎") {
                    purchaseRefill(tier)
                }
                Button("Cancel", role: .cancel) {}
            } message: { tier in
                Text("Spend \(tier.diamondCost) diamonds to \(tier.energy > 0 ? "gain \(tier.energy) energy" : "get unlimited energy for 24 hours")?")
            }
            .loadingOverlay(isLoading: isProcessing, message: "Processing...")
        }
    }

    func purchaseRefill(_ tier: RefillTier) {
        isProcessing = true

        // Send purchase message to backend
        webSocketService.sendMessage(message: [
            "type": "purchaseEnergyRefill",
            "refillType": tier.rawValue
        ])

        // Listen for response (handle in WebSocketService)
        // For now, simulate delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            isProcessing = false
            dismiss()
        }
    }
}

struct CurrentEnergyCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        VStack(spacing: BaoSpacing.sm) {
            Text("Current Energy")
                .font(BaoTypography.caption)
                .foregroundColor(.secondary)

            HStack(spacing: BaoSpacing.xs) {
                Image(systemName: "bolt.fill")
                    .foregroundColor(.yellow)
                    .font(.title)

                Text("\(webSocketService.person.calcEnergy) / \(webSocketService.person.maxEnergy)")
                    .font(BaoTypography.heading1)
            }

            ProgressView(value: Double(webSocketService.person.calcEnergy), total: Double(webSocketService.person.maxEnergy))
                .tint(.yellow)
        }
        .padding(BaoSpacing.lg)
        .baoCard()
        .padding(.horizontal, BaoSpacing.md)
    }
}

struct RefillOptionCard: View {
    let tier: EnergyRefillView.RefillTier
    let canAfford: Bool

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: BaoSpacing.xs) {
                HStack {
                    Text(tier.displayName)
                        .font(BaoTypography.heading3)

                    if let badge = tier.badge {
                        Text(badge)
                            .font(BaoTypography.small)
                            .padding(.horizontal, BaoSpacing.sm)
                            .padding(.vertical, BaoSpacing.xs)
                            .background(BaoColors.accent.opacity(0.2))
                            .foregroundColor(BaoColors.accent)
                            .cornerRadius(BaoRadius.sm)
                    }
                }

                Text(tier.description)
                    .font(BaoTypography.caption)
                    .foregroundColor(.secondary)

                if tier.energy > 0 {
                    HStack(spacing: BaoSpacing.xs) {
                        Image(systemName: "bolt.fill")
                            .foregroundColor(.yellow)
                        Text("+\(tier.energy) Energy")
                            .font(BaoTypography.body)
                    }
                }
            }

            Spacer()

            VStack(spacing: BaoSpacing.xs) {
                HStack(spacing: 4) {
                    Image(systemName: "diamond.fill")
                        .foregroundColor(.blue)
                    Text("\(tier.diamondCost)")
                        .font(BaoTypography.heading2)
                }

                if !canAfford {
                    Text("Not enough")
                        .font(BaoTypography.small)
                        .foregroundColor(.red)
                }
            }
        }
        .padding(BaoSpacing.md)
        .baoCard()
        .opacity(canAfford ? 1.0 : 0.6)
        .overlay(
            RoundedRectangle(cornerRadius: BaoRadius.md)
                .stroke(canAfford ? BaoColors.primary : Color.clear, lineWidth: 2)
        )
    }
}
```

**Estimated Time:** 4-5 hours

---

#### Component 14: Time Control Panel
**Files:** Create `TimeControlView.swift`

**Implementation:**
```swift
import SwiftUI

struct TimeControlView: View {
    @EnvironmentObject var webSocketService: WebSocketService
    @Environment(\.dismiss) var dismiss

    @State private var selectedSkip: TimeSkip?
    @State private var showingConfirmation = false
    @State private var isProcessing = false
    @State private var skipSummary: TimeSkipSummary?
    @State private var showSummary = false

    enum TimeSkip: String, CaseIterable {
        case hour1 = "1hour"
        case day1 = "1day"
        case week1 = "1week"
        case nextEvent = "next_event"

        var displayName: String {
            switch self {
            case .hour1: return "Skip 1 Hour"
            case .day1: return "Skip 1 Day"
            case .week1: return "Skip 1 Week"
            case .nextEvent: return "Skip to Next Event"
            }
        }

        var diamondCost: Int {
            switch self {
            case .hour1: return 5
            case .day1: return 25
            case .week1: return 100
            case .nextEvent: return 50
            }
        }

        var icon: String {
            switch self {
            case .hour1: return "clock"
            case .day1: return "sun.max"
            case .week1: return "calendar"
            case .nextEvent: return "star"
            }
        }

        var description: String {
            switch self {
            case .hour1: return "Fast forward 1 hour"
            case .day1: return "Jump ahead 24 hours"
            case .week1: return "Skip an entire week"
            case .nextEvent: return "Jump to your next major life event"
            }
        }
    }

    struct TimeSkipSummary {
        let durationHours: Double
        let events: [String]
        let moneyEarned: Int
        let statChanges: [String: Int]
    }

    var body: some View {
        NavigationView {
            VStack(spacing: BaoSpacing.lg) {
                // Current time/date
                CurrentTimeCard()

                // Speed controls (existing)
                SpeedControlSection()

                Divider()

                // Time skip purchases
                Text("Time Skips")
                    .font(BaoTypography.heading2)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(.horizontal, BaoSpacing.md)

                ScrollView {
                    VStack(spacing: BaoSpacing.md) {
                        ForEach(TimeSkip.allCases, id: \.self) { skip in
                            TimeSkipOptionCard(
                                skip: skip,
                                canAfford: webSocketService.person.diamonds >= skip.diamondCost
                            )
                            .onTapGesture {
                                if webSocketService.person.diamonds >= skip.diamondCost {
                                    selectedSkip = skip
                                    showingConfirmation = true
                                }
                            }
                        }
                    }
                    .padding(BaoSpacing.md)
                }
            }
            .navigationTitle("Time Control")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Close") {
                        dismiss()
                    }
                }
            }
            .confirmationDialog(
                "Skip time?",
                isPresented: $showingConfirmation,
                presenting: selectedSkip
            ) { skip in
                Button("Skip for \(skip.diamondCost) 💎") {
                    purchaseTimeSkip(skip)
                }
                Button("Cancel", role: .cancel) {}
            } message: { skip in
                Text("You'll experience life events during the skip. This cannot be undone.")
            }
            .loadingOverlay(isLoading: isProcessing, message: "Simulating time...")
            .sheet(isPresented: $showSummary) {
                TimeSkipSummaryView(summary: skipSummary)
            }
        }
    }

    func purchaseTimeSkip(_ skip: TimeSkip) {
        isProcessing = true

        webSocketService.sendMessage(message: [
            "type": "purchaseTimeSkip",
            "skipType": skip.rawValue
        ])

        // Backend will send timeSkipComplete message
        // Handle in WebSocketService and show summary
        // For now, simulate:
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            isProcessing = false

            // Show summary
            skipSummary = TimeSkipSummary(
                durationHours: skip == .hour1 ? 1 : (skip == .day1 ? 24 : 168),
                events: ["Went to work", "Had lunch with friend", "Read a book"],
                moneyEarned: 250,
                statChanges: ["Health": -2, "Happiness": 5]
            )
            showSummary = true
        }
    }
}

struct CurrentTimeCard: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var body: some View {
        VStack(spacing: BaoSpacing.sm) {
            Text(webSocketService.player.dateFormatted)
                .font(BaoTypography.heading2)

            Text(webSocketService.player.time)
                .font(BaoTypography.heading1)

            Text(webSocketService.player.season.capitalized)
                .font(BaoTypography.caption)
                .foregroundColor(.secondary)
        }
        .padding(BaoSpacing.lg)
        .frame(maxWidth: .infinity)
        .baoCard()
        .padding(.horizontal, BaoSpacing.md)
    }
}

struct SpeedControlSection: View {
    @EnvironmentObject var webSocketService: WebSocketService

    let speeds = [
        ("Pause", 5000),
        ("Slow", 1000),
        ("Normal", 500),
        ("Fast", 50),
        ("Very Fast", 20),
        ("Ultra", 1)
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: BaoSpacing.sm) {
            Text("Simulation Speed")
                .font(BaoTypography.heading3)

            HStack(spacing: BaoSpacing.sm) {
                ForEach(speeds, id: \.1) { name, value in
                    Button(action: {
                        webSocketService.sendMessage(message: [
                            "type": "speed",
                            "message": value
                        ])
                    }) {
                        Text(name)
                            .font(BaoTypography.caption)
                            .padding(.horizontal, BaoSpacing.sm)
                            .padding(.vertical, BaoSpacing.xs)
                            .background(BaoColors.secondaryBackground)
                            .cornerRadius(BaoRadius.sm)
                    }
                }
            }
        }
        .padding(.horizontal, BaoSpacing.md)
    }
}

struct TimeSkipOptionCard: View {
    let skip: TimeControlView.TimeSkip
    let canAfford: Bool

    var body: some View {
        HStack {
            Image(systemName: skip.icon)
                .font(.title2)
                .foregroundColor(BaoColors.primary)
                .frame(width: 40)

            VStack(alignment: .leading, spacing: BaoSpacing.xs) {
                Text(skip.displayName)
                    .font(BaoTypography.heading3)

                Text(skip.description)
                    .font(BaoTypography.caption)
                    .foregroundColor(.secondary)
            }

            Spacer()

            VStack(spacing: BaoSpacing.xs) {
                HStack(spacing: 4) {
                    Image(systemName: "diamond.fill")
                        .foregroundColor(.blue)
                    Text("\(skip.diamondCost)")
                        .font(BaoTypography.heading3)
                }

                if !canAfford {
                    Text("Not enough")
                        .font(BaoTypography.small)
                        .foregroundColor(.red)
                }
            }
        }
        .padding(BaoSpacing.md)
        .baoCard()
        .opacity(canAfford ? 1.0 : 0.6)
    }
}

struct TimeSkipSummaryView: View {
    let summary: TimeControlView.TimeSkipSummary?
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: BaoSpacing.lg) {
                    Text("While you were away...")
                        .font(BaoTypography.heading1)

                    // Duration
                    Text("Skipped \(Int(summary?.durationHours ?? 0)) hours")
                        .font(BaoTypography.body)
                        .foregroundColor(.secondary)

                    // Events
                    if let events = summary?.events, !events.isEmpty {
                        VStack(alignment: .leading, spacing: BaoSpacing.sm) {
                            Text("What happened:")
                                .font(BaoTypography.heading3)

                            ForEach(events, id: \.self) { event in
                                HStack {
                                    Image(systemName: "checkmark.circle.fill")
                                        .foregroundColor(.green)
                                    Text(event)
                                        .font(BaoTypography.body)
                                }
                            }
                        }
                        .padding(BaoSpacing.md)
                        .baoCard()
                    }

                    // Money earned
                    if let moneyEarned = summary?.moneyEarned, moneyEarned > 0 {
                        HStack {
                            Image(systemName: "dollarsign.circle.fill")
                                .foregroundColor(.green)
                            Text("Earned $\(moneyEarned)")
                                .font(BaoTypography.heading3)
                        }
                        .padding(BaoSpacing.md)
                        .baoCard()
                    }

                    // Stat changes
                    if let statChanges = summary?.statChanges {
                        VStack(alignment: .leading, spacing: BaoSpacing.sm) {
                            Text("Stat Changes:")
                                .font(BaoTypography.heading3)

                            ForEach(Array(statChanges.keys), id: \.self) { key in
                                let value = statChanges[key] ?? 0
                                HStack {
                                    Text(key)
                                    Spacer()
                                    Text("\(value > 0 ? "+" : "")\(value)")
                                        .foregroundColor(value > 0 ? .green : .red)
                                }
                            }
                        }
                        .padding(BaoSpacing.md)
                        .baoCard()
                    }

                    Button(action: { dismiss() }) {
                        Text("Continue")
                            .baoPrimaryButton()
                    }
                    .padding(.horizontal, BaoSpacing.md)
                }
                .padding(BaoSpacing.lg)
            }
            .navigationTitle("Time Skip Summary")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}
```

**Estimated Time:** 6-7 hours

---

Due to length constraints, I'll create the document with a summary of the remaining components. Let me continue writing...

**Estimated Time:** 6-7 hours

---

#### Component 15: Diamond Rewards System
**Files:** Update `WebSocketService.swift`, create `DiamondRewardToast.swift`

**Implementation:**
```swift
// In WebSocketService.swift
@Published var recentDiamondEarn: DiamondEarnEvent?

struct DiamondEarnEvent: Identifiable {
    let id = UUID()
    let amount: Int
    let reason: String
    let timestamp = Date()
}

// Handle diamondEarned message type
func handleDiamondEarned(_ data: [String: Any]) {
    if let amount = data["amount"] as? Int,
       let reason = data["reason"] as? String {
        let event = DiamondEarnEvent(amount: amount, reason: reason)
        DispatchQueue.main.async {
            self.recentDiamondEarn = event
            self.person.diamonds = data["total"] as? Int ?? self.person.diamonds
        }
    }
}

// DiamondRewardToast.swift
import SwiftUI

struct DiamondRewardToast: View {
    let amount: Int
    let reason: String

    @State private var opacity: Double = 0
    @State private var offset: CGFloat = 50

    var body: some View {
        VStack {
            HStack(spacing: BaoSpacing.sm) {
                Image(systemName: "diamond.fill")
                    .foregroundColor(.blue)
                    .font(.title2)

                Text("+\(amount) 💎")
                    .font(BaoTypography.heading2)
                    .foregroundColor(.blue)
            }

            Text(reason)
                .font(BaoTypography.caption)
                .foregroundColor(.secondary)
        }
        .padding(BaoSpacing.md)
        .background(
            RoundedRectangle(cornerRadius: BaoRadius.md)
                .fill(BaoColors.cardBackground)
                .shadow(color: .black.opacity(0.2), radius: 10)
        )
        .opacity(opacity)
        .offset(y: offset)
        .onAppear {
            withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                opacity = 1
                offset = 0
            }

            // Auto-dismiss after 3 seconds
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                withAnimation {
                    opacity = 0
                    offset = -50
                }
            }
        }
    }
}

// Add to ContentView overlay
if let diamondEarn = webSocketService.recentDiamondEarn {
    DiamondRewardToast(amount: diamondEarn.amount, reason: diamondEarn.reason)
        .position(x: UIScreen.main.bounds.width / 2, y: 100)
        .onAppear {
            // Clear after animation
            DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
                webSocketService.recentDiamondEarn = nil
            }
        }
}
```

**Estimated Time:** 3-4 hours

---

#### Component 16 & 17: Purchase Confirmation & Updated Store View
**Files:** Create `PurchaseConfirmationView.swift`, update `StoreView.swift`

I'll include these in the full design document but summarize here for brevity.

**Estimated Time:** 5-6 hours combined

---

**Phase 1 Total Estimated Time:** 45-55 hours (~2-3 weeks at 30 hours/week)

---

## Phases 2-7 Component Summary

Due to length, I'll create a comprehensive document with all remaining phases in detail. Here's the structure:

### Phase 2: Retention Mechanics (Components 18-26) - Weeks 4-5
- Backend: Achievement system, daily login rewards, quests, collections
- Frontend: Achievement UI, reward modals, quest tracker, statistics

### Phase 3: Onboarding (Components 27-31) - Week 6
- Backend: Tutorial state tracking
- Frontend: 5-step onboarding flow, tooltips

### Phase 4: Dating Improvements (Components 32-41) - Week 7
- Backend: Compatibility algorithm, bio generation, relationship events
- Frontend: Profile cards, event modals, mini-games

### Phase 5: Polish & Stability (Components 42-53) - Week 8
- Backend: Error handling, transactions, performance
- Frontend: Loading states, animations, haptics, accessibility

### Phase 6: Analytics & Testing (Components 54-62) - Week 9
- Backend: Analytics, crash reporting, A/B testing
- Frontend: Firebase integration, testing infrastructure

### Phase 7: App Store Prep (Components 63-72) - Weeks 10-11
- Backend: Legal docs, deployment
- Frontend: App Store assets, submission

Let me write the complete document now...

---

## Phase 2: Retention Mechanics (Weeks 4-5)

**Goal:** Give players compelling reasons to return daily through achievements, login rewards, and quests.

### Backend Components (`../lichun`)

#### Component 18: Achievement System
**Files:** Create `retention/achievements.py`, update database schema

**Database Schema:**
```sql
CREATE TABLE achievements (
    id INT PRIMARY KEY AUTO_INCREMENT,
    key_name VARCHAR(100) UNIQUE NOT NULL,
    display_name VARCHAR(200) NOT NULL,
    description TEXT,
    category ENUM('life_milestone', 'career', 'relationship', 'collection', 'secret') NOT NULL,
    diamond_reward INT NOT NULL,
    icon_name VARCHAR(100),
    progress_max INT DEFAULT 1,  -- For multi-step achievements
    hidden BOOLEAN DEFAULT FALSE,  -- Secret achievements
    INDEX idx_category (category)
);

CREATE TABLE player_achievements (
    id INT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    achievement_id INT NOT NULL,
    progress INT DEFAULT 0,
    unlocked BOOLEAN DEFAULT FALSE,
    unlock_date DATETIME,
    FOREIGN KEY (player_id) REFERENCES players(id),
    FOREIGN KEY (achievement_id) REFERENCES achievements(id),
    UNIQUE KEY unique_player_achievement (player_id, achievement_id),
    INDEX idx_player_unlocked (player_id, unlocked)
);
```

**Achievement Definitions (40-50 achievements):**
```python
ACHIEVEMENT_DEFINITIONS = [
    # Life Milestones
    {'key': 'first_day_school', 'name': 'First Day of School', 'desc': 'Start elementary school', 'category': 'life_milestone', 'reward': 10, 'icon': 'book'},
    {'key': 'graduate_hs', 'name': 'High School Graduate', 'desc': 'Graduate from high school', 'category': 'life_milestone', 'reward': 25, 'icon': 'graduationcap'},
    {'key': 'graduate_college', 'name': 'College Degree', 'desc': 'Graduate from college', 'category': 'life_milestone', 'reward': 50, 'icon': 'scroll'},
    {'key': 'get_married', 'name': 'Happily Ever After', 'desc': 'Get married', 'category': 'life_milestone', 'reward': 30, 'icon': 'heart'},
    {'key': 'have_child', 'name': 'New Parent', 'desc': 'Have your first child', 'category': 'life_milestone', 'reward': 40, 'icon': 'figure.2'},
    {'key': 'live_to_100', 'name': 'Century Club', 'desc': 'Live to 100 years old', 'category': 'life_milestone', 'reward': 200, 'icon': 'star.fill'},
    
    # Career
    {'key': 'first_job', 'name': 'Working Class', 'desc': 'Get your first job', 'category': 'career', 'reward': 10, 'icon': 'briefcase'},
    {'key': 'promotion', 'name': 'Climbing the Ladder', 'desc': 'Get promoted', 'category': 'career', 'reward': 20, 'icon': 'arrow.up'},
    {'key': 'become_manager', 'name': 'Management Material', 'desc': 'Become a manager', 'category': 'career', 'reward': 30, 'icon': 'person.2'},
    {'key': 'become_ceo', 'name': 'Corner Office', 'desc': 'Become a CEO', 'category': 'career', 'reward': 75, 'icon': 'crown'},
    {'key': 'earn_1m_lifetime', 'name': 'Millionaire', 'desc': 'Earn $1,000,000 in your lifetime', 'category': 'career', 'reward': 100, 'icon': 'dollarsign.circle'},
    
    # Relationships
    {'key': 'make_first_friend', 'name': 'Social Butterfly Begins', 'desc': 'Make your first friend', 'category': 'relationship', 'reward': 10, 'icon': 'person'},
    {'key': 'date_10_people', 'name': 'Dating Around', 'desc': 'Date 10 different people', 'category': 'relationship', 'reward': 25, 'icon': 'heart.multiple'},
    {'key': 'golden_anniversary', 'name': 'Golden Anniversary', 'desc': 'Stay married for 50 years', 'category': 'relationship', 'reward': 100, 'icon': 'rings'},
    {'key': 'high_affinity', 'name': 'Best Friends Forever', 'desc': 'Reach 100 affinity with someone', 'category': 'relationship', 'reward': 30, 'icon': 'heart.fill'},
    
    # Collection
    {'key': 'own_25_items', 'name': 'Collector', 'desc': 'Own 25 items', 'category': 'collection', 'reward': 20, 'icon': 'square.grid.3x3'},
    {'key': 'own_50_items', 'name': 'Hoarder', 'desc': 'Own 50 items', 'category': 'collection', 'reward': 40, 'icon': 'square.grid.4x3'},
    {'key': 'max_prestige', 'name': 'Maximum Prestige', 'desc': 'Reach 1000 prestige', 'category': 'collection', 'reward': 150, 'icon': 'star.circle'},
    
    # Secret (hidden until unlocked)
    {'key': 'die_at_69', 'name': 'Nice', 'desc': 'Die at exactly 69 years old', 'category': 'secret', 'reward': 50, 'icon': 'face.smiling', 'hidden': True},
    {'key': 'fired_3_times', 'name': 'Professional Quitter', 'desc': 'Get fired 3 times', 'category': 'secret', 'reward': 25, 'icon': 'xmark.circle', 'hidden': True},
    {'key': 'never_marry', 'name': 'Forever Alone', 'desc': 'Complete a life without marrying', 'category': 'secret', 'reward': 30, 'icon': 'person.fill', 'hidden': True},
]

def initialize_achievements():
    """Insert achievement definitions into database"""
    for ach in ACHIEVEMENT_DEFINITIONS:
        db.execute_query(
            """INSERT INTO achievements 
               (key_name, display_name, description, category, diamond_reward, icon_name, hidden)
               VALUES (%s, %s, %s, %s, %s, %s, %s)
               ON DUPLICATE KEY UPDATE display_name=display_name""",  # Skip if exists
            (ach['key'], ach['name'], ach['desc'], ach['category'], 
             ach['reward'], ach['icon'], ach.get('hidden', False)),
            fetch=False
        )
```

**Achievement Checking Logic:**
```python
def check_achievements(player_id: int, event_type: str, event_data: dict = None):
    """
    Check if any achievements should be unlocked based on event
    Called after major game events
    """
    unlocked = []
    
    # Get player's current state
    player = get_player_full_state(player_id)
    
    # Event-specific checks
    if event_type == 'start_school':
        unlocked.extend(check_and_unlock(player_id, 'first_day_school'))
    
    elif event_type == 'graduate':
        level = event_data.get('level')
        if level == 'high_school':
            unlocked.extend(check_and_unlock(player_id, 'graduate_hs'))
        elif level == 'college':
            unlocked.extend(check_and_unlock(player_id, 'graduate_college'))
    
    elif event_type == 'get_job':
        if player['job_count'] == 1:  # First job
            unlocked.extend(check_and_unlock(player_id, 'first_job'))
    
    elif event_type == 'promotion':
        unlocked.extend(check_and_unlock(player_id, 'promotion'))
        
        # Check title-based achievements
        if player['occupation_title'] == 'Manager':
            unlocked.extend(check_and_unlock(player_id, 'become_manager'))
        elif player['occupation_title'] == 'CEO':
            unlocked.extend(check_and_unlock(player_id, 'become_ceo'))
    
    elif event_type == 'marriage':
        unlocked.extend(check_and_unlock(player_id, 'get_married'))
    
    elif event_type == 'birth_child':
        if player['children_count'] == 1:  # First child
            unlocked.extend(check_and_unlock(player_id, 'have_child'))
    
    elif event_type == 'birthday':
        age = event_data.get('age')
        if age == 100:
            unlocked.extend(check_and_unlock(player_id, 'live_to_100'))
    
    elif event_type == 'death':
        age = player['age_years']
        if age == 69:
            unlocked.extend(check_and_unlock(player_id, 'die_at_69'))
        
        # Check lifetime stats
        if player['lifetime_earnings'] >= 1000000:
            unlocked.extend(check_and_unlock(player_id, 'earn_1m_lifetime'))
        
        if not player['ever_married']:
            unlocked.extend(check_and_unlock(player_id, 'never_marry'))
    
    elif event_type == 'make_friend':
        if player['friends_count'] == 1:
            unlocked.extend(check_and_unlock(player_id, 'make_first_friend'))
    
    elif event_type == 'affinity_milestone':
        affinity = event_data.get('affinity')
        if affinity >= 100:
            unlocked.extend(check_and_unlock(player_id, 'high_affinity'))
    
    # Progressive checks (run periodically, not just on events)
    # Dating count
    if player['people_dated'] >= 10:
        unlocked.extend(check_and_unlock(player_id, 'date_10_people'))
    
    # Marriage duration
    if player['years_married'] >= 50:
        unlocked.extend(check_and_unlock(player_id, 'golden_anniversary'))
    
    # Item collection
    item_count = len(player['owned_items'])
    if item_count >= 25:
        unlocked.extend(check_and_unlock(player_id, 'own_25_items'))
    if item_count >= 50:
        unlocked.extend(check_and_unlock(player_id, 'own_50_items'))
    
    # Prestige
    if player['prestige'] >= 1000:
        unlocked.extend(check_and_unlock(player_id, 'max_prestige'))
    
    # Fired count
    if player['times_fired'] >= 3:
        unlocked.extend(check_and_unlock(player_id, 'fired_3_times'))
    
    return unlocked

def check_and_unlock(player_id: int, achievement_key: str) -> list:
    """
    Check if player has unlocked achievement
    If not yet unlocked, unlock it and return achievement data
    """
    # Get achievement
    achievement = db.execute_query(
        "SELECT * FROM achievements WHERE key_name = %s",
        (achievement_key,)
    )
    
    if not achievement:
        return []
    
    achievement = achievement[0]
    
    # Check if player already has it
    player_ach = db.execute_query(
        """SELECT * FROM player_achievements 
           WHERE player_id = %s AND achievement_id = %s AND unlocked = TRUE""",
        (player_id, achievement['id'])
    )
    
    if player_ach:
        return []  # Already unlocked
    
    # Unlock it
    db.execute_query(
        """INSERT INTO player_achievements 
           (player_id, achievement_id, progress, unlocked, unlock_date)
           VALUES (%s, %s, %s, TRUE, NOW())
           ON DUPLICATE KEY UPDATE unlocked = TRUE, unlock_date = NOW()""",
        (player_id, achievement['id'], achievement['progress_max']),
        fetch=False
    )
    
    # Award diamonds
    award_diamonds(player_id, f"achievement_{achievement_key}", achievement['diamond_reward'])
    
    # Send notification to client
    send_to_client(player_id, {
        'type': 'achievementUnlock',
        'achievement': {
            'id': achievement['id'],
            'name': achievement['display_name'],
            'description': achievement['description'],
            'icon': achievement['icon_name'],
            'reward': achievement['diamond_reward']
        }
    })
    
    logging.info(f"Player {player_id} unlocked achievement: {achievement_key}")
    
    return [achievement]
```

**Estimated Time:** 10-12 hours

---

#### Component 19: Daily Login Reward System
**Files:** Create `retention/daily_rewards.py`, update database schema

**Database Schema:**
```sql
CREATE TABLE daily_login_rewards (
    day_number INT PRIMARY KEY,  -- 1-7, cycles
    reward_type ENUM('diamonds', 'energy', 'item', 'prestige') NOT NULL,
    reward_amount INT NOT NULL,
    reward_item_id INT NULL,  -- If reward_type = 'item'
    display_name VARCHAR(100),
    FOREIGN KEY (reward_item_id) REFERENCES store_items(id)
);

CREATE TABLE player_login_streak (
    player_id INT PRIMARY KEY,
    current_streak INT DEFAULT 0,
    last_login_date DATE,
    total_logins INT DEFAULT 0,
    next_reward_day INT DEFAULT 1,  -- 1-7
    FOREIGN KEY (player_id) REFERENCES players(id)
);
```

**Reward Calendar Setup:**
```python
DAILY_REWARDS = [
    {'day': 1, 'type': 'diamonds', 'amount': 5, 'name': '5 Diamonds'},
    {'day': 2, 'type': 'diamonds', 'amount': 10, 'name': '10 Diamonds'},
    {'day': 3, 'type': 'energy', 'amount': 50, 'name': '50 Energy'},
    {'day': 4, 'type': 'diamonds', 'amount': 15, 'name': '15 Diamonds'},
    {'day': 5, 'type': 'item', 'amount': 1, 'name': 'Premium Item', 'item_id': None},  # Random luxury item
    {'day': 6, 'type': 'diamonds', 'amount': 25, 'name': '25 Diamonds'},
    {'day': 7, 'type': 'diamonds', 'amount': 50, 'name': '50 Diamonds + Bonus'},
]

def initialize_daily_rewards():
    """Insert reward definitions"""
    for reward in DAILY_REWARDS:
        db.execute_query(
            """INSERT INTO daily_login_rewards 
               (day_number, reward_type, reward_amount, reward_item_id, display_name)
               VALUES (%s, %s, %s, %s, %s)
               ON DUPLICATE KEY UPDATE reward_amount=reward_amount""",
            (reward['day'], reward['type'], reward['amount'], 
             reward.get('item_id'), reward['name']),
            fetch=False
        )
```

**Login Check Logic:**
```python
from datetime import date, timedelta

def check_daily_login(player_id: int) -> dict:
    """
    Check and update daily login streak
    Called when player connects to server
    Returns: {'reward_available': bool, 'streak': int, 'reward': dict}
    """
    today = date.today()
    
    # Get player's streak data
    streak_data = db.execute_query(
        "SELECT * FROM player_login_streak WHERE player_id = %s",
        (player_id,)
    )
    
    if not streak_data:
        # First time login
        db.execute_query(
            """INSERT INTO player_login_streak 
               (player_id, current_streak, last_login_date, total_logins, next_reward_day)
               VALUES (%s, 1, %s, 1, 1)""",
            (player_id, today),
            fetch=False
        )
        
        return {
            'reward_available': True,
            'streak': 1,
            'reward': get_daily_reward(1)
        }
    
    streak_data = streak_data[0]
    last_login = streak_data['last_login_date']
    
    # Check if already logged in today
    if last_login == today:
        return {
            'reward_available': False,
            'streak': streak_data['current_streak'],
            'reward': None
        }
    
    # Check if streak broken (more than 1 day gap)
    days_since_login = (today - last_login).days
    
    if days_since_login == 1:
        # Streak continues
        new_streak = streak_data['current_streak'] + 1
        next_day = (streak_data['next_reward_day'] % 7) + 1
    else:
        # Streak broken, restart
        new_streak = 1
        next_day = 1
    
    # Update streak
    db.execute_query(
        """UPDATE player_login_streak 
           SET current_streak = %s, last_login_date = %s, 
               total_logins = total_logins + 1, next_reward_day = %s
           WHERE player_id = %s""",
        (new_streak, today, next_day, player_id),
        fetch=False
    )
    
    # Get today's reward
    reward = get_daily_reward(streak_data['next_reward_day'])
    
    return {
        'reward_available': True,
        'streak': new_streak,
        'reward': reward
    }

def get_daily_reward(day_number: int) -> dict:
    """Get reward for specific day"""
    reward = db.execute_query(
        "SELECT * FROM daily_login_rewards WHERE day_number = %s",
        (day_number,)
    )[0]
    
    return {
        'day': reward['day_number'],
        'type': reward['reward_type'],
        'amount': reward['reward_amount'],
        'item_id': reward['reward_item_id'],
        'name': reward['display_name']
    }

def claim_daily_reward(player_id: int) -> dict:
    """
    Claim today's reward
    Returns: {'success': bool, 'reward': dict}
    """
    # Get streak data
    streak_data = db.execute_query(
        "SELECT * FROM player_login_streak WHERE player_id = %s",
        (player_id,)
    )[0]
    
    reward_day = streak_data['next_reward_day']
    reward = get_daily_reward(reward_day)
    
    # Award based on type
    if reward['type'] == 'diamonds':
        award_diamonds(player_id, f"daily_reward_day{reward_day}", reward['amount'])
    
    elif reward['type'] == 'energy':
        db.execute_query(
            "UPDATE players SET energy = LEAST(energy + %s, max_energy) WHERE id = %s",
            (reward['amount'], player_id),
            fetch=False
        )
    
    elif reward['type'] == 'prestige':
        db.execute_query(
            "UPDATE players SET prestige = prestige + %s WHERE id = %s",
            (reward['amount'], player_id),
            fetch=False
        )
    
    elif reward['type'] == 'item':
        # Award random luxury item
        luxury_items = db.execute_query(
            "SELECT * FROM store_items WHERE category = 'luxury' ORDER BY RAND() LIMIT 1"
        )
        if luxury_items:
            item = luxury_items[0]
            give_item_to_player(player_id, item['id'])
    
    logging.info(f"Player {player_id} claimed daily reward: Day {reward_day}")
    
    return {
        'success': True,
        'reward': reward
    }

# WebSocket handler
def handle_daily_login_check(player_id: int):
    """Called on player connection"""
    result = check_daily_login(player_id)
    
    if result['reward_available']:
        send_to_client(player_id, {
            'type': 'dailyRewardAvailable',
            'streak': result['streak'],
            'reward': result['reward']
        })
```

**Estimated Time:** 6-8 hours

---

#### Component 20: Daily Quest System
**Files:** Create `retention/daily_quests.py`, update database schema

**Database Schema:**
```sql
CREATE TABLE daily_quest_templates (
    id INT PRIMARY KEY AUTO_INCREMENT,
    quest_type VARCHAR(100) NOT NULL,
    description VARCHAR(200) NOT NULL,
    progress_required INT NOT NULL,
    diamond_reward INT NOT NULL,
    difficulty ENUM('easy', 'medium', 'hard') NOT NULL,
    energy_cost INT DEFAULT 0,  -- Estimated energy to complete
    icon_name VARCHAR(100)
);

CREATE TABLE player_daily_quests (
    id INT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    quest_template_id INT NOT NULL,
    progress INT DEFAULT 0,
    completed BOOLEAN DEFAULT FALSE,
    assigned_date DATE NOT NULL,
    completed_date DATETIME,
    FOREIGN KEY (player_id) REFERENCES players(id),
    FOREIGN KEY (quest_template_id) REFERENCES daily_quest_templates(id),
    INDEX idx_player_date (player_id, assigned_date)
);
```

**Quest Templates:**
```python
QUEST_TEMPLATES = [
    # Easy quests (10 energy, quick)
    {'type': 'talk_to_characters', 'desc': 'Talk to 3 different characters', 'required': 3, 'reward': 10, 'difficulty': 'easy', 'energy': 9, 'icon': 'message'},
    {'type': 'buy_item', 'desc': 'Buy an item from the store', 'required': 1, 'reward': 5, 'difficulty': 'easy', 'energy': 0, 'icon': 'cart'},
    {'type': 'attend_class', 'desc': 'Attend 2 classes', 'required': 2, 'reward': 8, 'difficulty': 'easy', 'energy': 6, 'icon': 'book'},
    
    # Medium quests (20-30 energy)
    {'type': 'work_full_day', 'desc': 'Work for 8 hours', 'required': 8, 'reward': 15, 'difficulty': 'medium', 'energy': 24, 'icon': 'briefcase'},
    {'type': 'go_on_date', 'desc': 'Go on a date', 'required': 1, 'reward': 20, 'difficulty': 'medium', 'energy': 15, 'icon': 'heart'},
    {'type': 'complete_activities', 'desc': 'Complete 5 activities', 'required': 5, 'reward': 18, 'difficulty': 'medium', 'energy': 25, 'icon': 'checkmark.circle'},
    
    # Hard quests (40+ energy)
    {'type': 'spend_energy', 'desc': 'Spend 50 energy on activities', 'required': 50, 'reward': 25, 'difficulty': 'hard', 'energy': 50, 'icon': 'bolt'},
    {'type': 'earn_money', 'desc': 'Earn $500', 'required': 500, 'reward': 30, 'difficulty': 'hard', 'energy': 40, 'icon': 'dollarsign'},
    {'type': 'increase_affinity', 'desc': 'Increase affinity by 20 points total', 'required': 20, 'reward': 35, 'difficulty': 'hard', 'energy': 45, 'icon': 'heart.fill'},
]

def initialize_quest_templates():
    """Insert quest templates"""
    for quest in QUEST_TEMPLATES:
        db.execute_query(
            """INSERT INTO daily_quest_templates 
               (quest_type, description, progress_required, diamond_reward, difficulty, energy_cost, icon_name)
               VALUES (%s, %s, %s, %s, %s, %s, %s)
               ON DUPLICATE KEY UPDATE description=description""",
            (quest['type'], quest['desc'], quest['required'], quest['reward'], 
             quest['difficulty'], quest['energy'], quest['icon']),
            fetch=False
        )
```

**Quest Assignment Logic:**
```python
def generate_daily_quests(player_id: int):
    """
    Assign 3 random quests to player each day
    Called at midnight or on first login of the day
    """
    today = date.today()
    
    # Check if already has quests for today
    existing = db.execute_query(
        "SELECT COUNT(*) as count FROM player_daily_quests WHERE player_id = %s AND assigned_date = %s",
        (player_id, today)
    )[0]
    
    if existing['count'] > 0:
        return  # Already assigned
    
    # Get all quest templates
    all_quests = db.execute_query("SELECT * FROM daily_quest_templates")
    
    # Randomly select: 1 easy, 1 medium, 1 hard
    easy_quests = [q for q in all_quests if q['difficulty'] == 'easy']
    medium_quests = [q for q in all_quests if q['difficulty'] == 'medium']
    hard_quests = [q for q in all_quests if q['difficulty'] == 'hard']
    
    import random
    selected = [
        random.choice(easy_quests),
        random.choice(medium_quests),
        random.choice(hard_quests)
    ]
    
    # Assign to player
    for quest in selected:
        db.execute_query(
            """INSERT INTO player_daily_quests 
               (player_id, quest_template_id, assigned_date)
               VALUES (%s, %s, %s)""",
            (player_id, quest['id'], today),
            fetch=False
        )
    
    logging.info(f"Assigned daily quests to player {player_id}")
    
    # Notify client
    send_to_client(player_id, {
        'type': 'dailyQuestsAssigned',
        'quests': selected
    })

def update_quest_progress(player_id: int, quest_type: str, amount: int = 1):
    """
    Update progress on quest of given type
    Called after relevant actions
    """
    today = date.today()
    
    # Find active quest of this type
    active_quests = db.execute_query(
        """SELECT pq.*, qt.quest_type, qt.progress_required, qt.diamond_reward
           FROM player_daily_quests pq
           JOIN daily_quest_templates qt ON pq.quest_template_id = qt.id
           WHERE pq.player_id = %s AND pq.assigned_date = %s 
           AND pq.completed = FALSE AND qt.quest_type = %s""",
        (player_id, today, quest_type)
    )
    
    if not active_quests:
        return
    
    quest = active_quests[0]
    new_progress = min(quest['progress'] + amount, quest['progress_required'])
    
    # Update progress
    db.execute_query(
        "UPDATE player_daily_quests SET progress = %s WHERE id = %s",
        (new_progress, quest['id']),
        fetch=False
    )
    
    # Check if completed
    if new_progress >= quest['progress_required']:
        complete_quest(player_id, quest['id'], quest['diamond_reward'])
    else:
        # Send progress update
        send_to_client(player_id, {
            'type': 'questProgress',
            'quest_id': quest['id'],
            'progress': new_progress,
            'required': quest['progress_required']
        })

def complete_quest(player_id: int, quest_id: int, reward: int):
    """Complete quest and award diamonds"""
    db.execute_query(
        """UPDATE player_daily_quests 
           SET completed = TRUE, completed_date = NOW() 
           WHERE id = %s""",
        (quest_id,),
        fetch=False
    )
    
    # Award diamonds
    award_diamonds(player_id, f"daily_quest_{quest_id}", reward)
    
    # Send completion notification
    send_to_client(player_id, {
        'type': 'questComplete',
        'quest_id': quest_id,
        'reward': reward
    })
    
    logging.info(f"Player {player_id} completed quest {quest_id}")

# Hook quest progress into existing actions
# Example: In conversation handler
def handle_send_message(player_id: int, target_id: int, message: str):
    # ... existing conversation logic ...
    
    # Update quest progress
    update_quest_progress(player_id, 'talk_to_characters', 1)

# Example: In work/school handler
def handle_work_activity(player_id: int, hours: int):
    # ... existing work logic ...
    
    update_quest_progress(player_id, 'work_full_day', hours)
    update_quest_progress(player_id, 'complete_activities', 1)

# Example: In store purchase
def handle_store_purchase(player_id: int, item_id: int):
    # ... existing purchase logic ...
    
    update_quest_progress(player_id, 'buy_item', 1)
```

**Estimated Time:** 8-10 hours

---

#### Component 21: Collection Tracking System
**Files:** Update existing player statistics, create `retention/statistics.py`

**Database Schema:**
```sql
CREATE TABLE player_statistics (
    player_id INT PRIMARY KEY,
    lifetime_earnings BIGINT DEFAULT 0,
    lifetime_spending BIGINT DEFAULT 0,
    total_relationships INT DEFAULT 0,
    total_activities INT DEFAULT 0,
    total_conversations INT DEFAULT 0,
    total_years_lived INT DEFAULT 0,  -- Across all lives
    total_deaths INT DEFAULT 0,
    highest_job_level INT DEFAULT 0,
    max_affinity_reached INT DEFAULT 0,
    FOREIGN KEY (player_id) REFERENCES players(id)
);

CREATE TABLE player_photo_album (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    player_id INT NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    event_description TEXT,
    game_date DATETIME NOT NULL,
    character_age INT,
    snapshot_data JSON,  -- Store relevant state
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (player_id) REFERENCES players(id),
    INDEX idx_player_date (player_id, game_date)
);
```

**Statistics Tracking:**
```python
def increment_stat(player_id: int, stat_name: str, amount: int = 1):
    """Increment a statistic for player"""
    db.execute_query(
        f"UPDATE player_statistics SET {stat_name} = {stat_name} + %s WHERE player_id = %s",
        (amount, player_id),
        fetch=False
    )

def capture_photo_memory(player_id: int, event_type: str, description: str, snapshot_data: dict):
    """Capture a life moment in photo album"""
    player = get_player(player_id)
    
    db.execute_query(
        """INSERT INTO player_photo_album 
           (player_id, event_type, event_description, game_date, character_age, snapshot_data)
           VALUES (%s, %s, %s, %s, %s, %s)""",
        (player_id, event_type, description, player['game_time'], 
         player['age_years'], json.dumps(snapshot_data)),
        fetch=False
    )

# Hook into existing events
def handle_graduation(player_id: int, level: str):
    # ... existing graduation logic ...
    
    # Capture memory
    capture_photo_memory(
        player_id,
        'graduation',
        f'Graduated from {level}',
        {'level': level, 'gpa': player['gpa']}
    )
    
    increment_stat(player_id, 'total_activities', 1)

def handle_marriage(player_id: int, spouse_id: int):
    # ... existing marriage logic ...
    
    capture_photo_memory(
        player_id,
        'marriage',
        f'Married {spouse.name}',
        {'spouse_id': spouse_id, 'spouse_name': spouse.name}
    )
    
    increment_stat(player_id, 'total_relationships', 1)
```

**Estimated Time:** 4-5 hours

---

### Frontend Components (`lichunWebsocket`)

#### Component 22: Achievement Popup & Gallery
**Files:** Create `AchievementUnlockView.swift`, `AchievementGalleryView.swift`

**Implementation in next message due to length...**

**Estimated Time:** 6-8 hours

---

#### Component 23-26: Daily Rewards, Quests, Statistics Views
**Combined Estimated Time:** 12-15 hours

---

**Phase 2 Total Estimated Time:** 50-60 hours (~2 weeks at 30 hours/week)

---

## Phases 3-7 Quick Reference

Due to document length, here's the condensed version with estimated times:

### Phase 3: Onboarding (Week 6) - 25-30 hours
- Component 27-28: Backend tutorial tracking (5h)
- Component 29-31: Frontend onboarding flow (20-25h)

### Phase 4: Dating Improvements (Week 7) - 30-35 hours
- Component 32-35: Backend compatibility, bios, events (18-20h)
- Component 36-41: Frontend profiles, mini-games (12-15h)

### Phase 5: Polish & Stability (Week 8) - 35-40 hours
- Component 42-45: Backend error handling, performance (15-18h)
- Component 46-53: Frontend animations, haptics, accessibility (20-22h)

### Phase 6: Analytics & Testing (Week 9) - 30-35 hours
- Component 54-57: Backend analytics, monitoring (15-18h)
- Component 58-62: Frontend Firebase, testing (15-17h)

### Phase 7: App Store Prep (Weeks 10-11) - 40-45 hours
- Component 63-65: Backend legal, deployment (10-12h)
- Component 66-72: Frontend assets, submission (30-33h)

---

## Implementation Order Summary

### Week-by-Week Breakdown

**Week 1: Technical Debt**
- Backend: Components 1-4 (env config, FPS, connections, DB)
- Frontend: Components 5-8 (refactoring, design system, WebSocket, modifiers)

**Weeks 2-3: Monetization**
- Backend: Components 9-12 (energy, time skips, economy, anti-cheat)
- Frontend: Components 13-17 (energy UI, time control, rewards, store)

**Weeks 4-5: Retention**
- Backend: Components 18-21 (achievements, daily rewards, quests, stats)
- Frontend: Components 22-26 (achievement UI, rewards, quests, statistics)

**Week 6: Onboarding**
- Backend: Components 27-28 (tutorial tracking)
- Frontend: Components 29-31 (5-step flow, tooltips)

**Week 7: Dating**
- Backend: Components 32-35 (compatibility, bios, events, dates)
- Frontend: Components 36-41 (profiles, events, mini-games)

**Week 8: Polish**
- Backend: Components 42-45 (errors, transactions, performance)
- Frontend: Components 46-53 (loading, animations, haptics, accessibility)

**Week 9: Testing**
- Backend: Components 54-57 (analytics, crash reporting, A/B tests)
- Frontend: Components 58-62 (Firebase, testing, TestFlight)

**Weeks 10-11: Launch**
- Backend: Components 63-65 (legal, deployment, data management)
- Frontend: Components 66-72 (assets, listing, submission)

---

## Total Time Estimates

| Phase | Backend | Frontend | Total |
|-------|---------|----------|-------|
| 0: Tech Debt | 12-15h | 13-18h | 25-33h |
| 1: Monetization | 22-26h | 23-29h | 45-55h |
| 2: Retention | 28-35h | 22-25h | 50-60h |
| 3: Onboarding | 5-6h | 20-24h | 25-30h |
| 4: Dating | 18-20h | 12-15h | 30-35h |
| 5: Polish | 15-18h | 20-22h | 35-40h |
| 6: Testing | 15-18h | 15-17h | 30-35h |
| 7: Launch | 10-12h | 30-33h | 40-45h |
| **TOTAL** | **125-150h** | **155-183h** | **280-333h** |

At 30 hours/week = **9.3 - 11.1 weeks**
At 40 hours/week = **7.0 - 8.3 weeks**

---

## Subagent Work Distribution

### Backend Subagent Tasks (`../lichun`)
- All Component 1-4, 9-12, 18-21, 27-28, 32-35, 42-45, 54-57, 63-65
- Total: ~125-150 hours
- Can work independently with occasional syncs for API contracts

### Frontend Subagent Tasks (`lichunWebsocket`)
- All Component 5-8, 13-17, 22-26, 29-31, 36-41, 46-53, 58-62, 66-72
- Total: ~155-183 hours
- Depends on backend API contracts being defined

### Coordination Points
- After Phase 0: Sync on WebSocket message formats
- After Phase 1: Sync on monetization API contracts
- After Phase 2: Sync on retention event triggers
- Weekly: Sync on progress and blockers

---

## Next Steps

1. **Review and approve** this design document
2. **Set up worktrees** for parallel development if desired
3. **Create detailed task breakdowns** for each component
4. **Assign components** to subagents or work sequentially
5. **Begin Phase 0** to establish solid foundation

Ready to proceed?

