# Event Avatars & Location Images Design

**Date:** 2025-11-13
**Status:** Approved
**Target:** Backend (Python) + iOS Frontend (SwiftUI)

## Overview

Expand the event system to display character avatars and optional location images in event displays, enhancing visual storytelling and immersion.

## Problem Statement

Currently, events show only text messages without visual context about:
- **Who is involved** - Events mention characters by name but don't show their avatars
- **Where it happens** - No visual sense of location (school, home, park, etc.)

This reduces immersion and makes events feel less personal.

## Solution

**Add two optional visual elements to events:**
1. **Character avatars** - Display up to 4 character avatars involved in the event
2. **Location images** - Optional background/overlay image showing where the event occurs

## Requirements

### Functional Requirements

1. Events can specify 0-4 characters to display (player and NPCs)
2. Character avatars show existing person.image URLs (DiceBear SVG avatars)
3. Location images are manually assigned per event (not auto-generated)
4. Both features are **optional** - events without them continue to work
5. iOS frontend renders avatars and location overlays
6. Web frontend is **out of scope** for this phase

### Non-Functional Requirements

1. **Backward compatibility** - Existing events without these fields work unchanged
2. **Minimal payload** - Only send necessary character data (id, firstname, lastname, image)
3. **Performance** - No impact on event processing speed
4. **Maintainability** - Event authors easily add avatars/images to new events

## Architecture

### Backend Changes

**File:** `ws/events/base.py`

#### 1. Extend `messageEvent` class

```python
class messageEvent:
    def __init__(self):
        self.id = ""
        self.date = ""
        self.hour = ""
        self.type = 'messageEvent'
        self.energyCost = 0
        self.diamondCost = 0
        self.moneyCost = 0
        self.affinityChange = 0
        self.message = ""
        self.title = ""
        self.image = ""           # EXISTING: Location/scene image URL
        self.characters = []      # NEW: Array of character data
```

#### 2. Update `messageFunction()` helper

```python
def messageFunction(fname, message, player=False, check=False, title="", image="",
                    characters=None, energyCost=0, diamondCost=0, moneyCost=0, affinityChange=0):
    if (check):
        m = messageEvent()
        m.id = fname
        m.message = message
        m.date = player.date
        m.hour = player.hourOfDay
        m.title = title
        m.image = image
        m.energyCost = energyCost
        m.diamondCost = diamondCost
        m.moneyCost = moneyCost
        m.affinityChange = affinityChange

        # NEW: Serialize characters to reduce payload size
        if characters:
            m.characters = [
                {
                    'id': char.id,
                    'firstname': char.firstname,
                    'lastname': char.lastname,
                    'image': char.image
                }
                for char in characters
            ]

        return m
```

#### 3. Update `questionEvent` class similarly

```python
class questionEvent:
    def __init__(self):
        self.query = ""
        self.answers = [answerOption("Yes"), answerOption("No")]
        self.type = 'questionEvent'
        self.characters = []      # NEW
        self.image = ""           # NEW
```

Update `questionFunction()`:

```python
def questionFunction(fname, message, player=False, check=False, answerOptions=False,
                     characters=None, image=""):
    if (check):
        player.previousGameSpeed = player.gameSpeed
        player.gameSpeed = config.SPEED_QUESTION_PAUSE

        m = questionEvent()
        m.id = fname

        if (hasattr(message, 'message')):
            m.message = message.message
            m.objectId = message.id
        else:
            m.message = message

        if (answerOptions):
            if (type(answerOptions[0]) == str):
                answerOptions = [answerOption(option, str(i)) for i, option in enumerate(answerOptions)]
            m.answers = answerOptions

        # NEW: Add characters and image
        if characters:
            m.characters = [
                {
                    'id': char.id,
                    'firstname': char.firstname,
                    'lastname': char.lastname,
                    'image': char.image
                }
                for char in characters
            ]

        m.image = image

        return m
```

### iOS Frontend Changes

#### 1. Update `MessageEvent.swift` model

**File:** `lichunWebsocket/Core/Models/MessageEvent.swift`

```swift
// NEW: Lightweight person representation for events
struct SimplePerson: Codable, Identifiable {
    let id: String
    let firstname: String
    let lastname: String
    let image: String
}

struct MessageEvent: Identifiable, Codable {
    var id: String
    var message: String
    var type: String
    var date: String?
    var hour: String?
    var energyCost: Int?
    var diamondCost: Int?
    var moneyCost: Int?
    var affinityChange: Int?
    var title: String?
    var image: String?              // EXISTING: Location image
    var characters: [SimplePerson]? // NEW: People involved

    var claimed: Bool = false
    var claimedAt: Date?
    var category: EventCategory = .neutral

    // ... existing computed properties ...
}
```

#### 2. Update `Question.swift` model

**File:** `lichunWebsocket/Core/Models/Question.swift`

```swift
struct Question: Identifiable, Equatable {
    let id: String
    let question: String
    let answers: [AnswerOption]
    let characters: [SimplePerson]? // NEW
    let image: String?              // NEW

    static func == (lhs: Question, rhs: Question) -> Bool {
        return lhs.id == rhs.id && lhs.question == rhs.question
    }
}
```

#### 3. Update `LifeEventCard.swift` component

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

Add character avatars row and location background:

```swift
var body: some View {
    Button(action: { ... }) {
        VStack(alignment: .leading, spacing: AppSpacing.xs) {

            // NEW: Character avatars row
            if let characters = event.characters, !characters.isEmpty {
                HStack(spacing: -8) {
                    ForEach(characters.prefix(4)) { character in
                        AvatarView(
                            person: personFrom(character),
                            size: 32,
                            showBorder: true,
                            borderColor: .white
                        )
                    }
                }
                .padding(.bottom, 4)
            }

            // EXISTING: Event content
            HStack(alignment: .top, spacing: AppSpacing.xs) {
                Text(event.category.rawValue)
                    .font(.system(size: 24))

                VStack(alignment: .leading, spacing: 2) {
                    // ... existing event display ...
                }

                Spacer()
            }
        }
        .padding(AppSpacing.sm)
        .background(backgroundContent)  // UPDATED: Add location image
        // ... existing styling ...
    }
}

// NEW: Background with optional location image
@ViewBuilder
var backgroundContent: some View {
    ZStack {
        // Base background color
        backgroundColor

        // Optional location image overlay
        if let imageURL = event.image, let url = URL(string: imageURL) {
            AsyncImage(url: url) { image in
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .opacity(0.15)
                    .blur(radius: 2)
            } placeholder: {
                EmptyView()
            }
        }
    }
}

// Helper to convert SimplePerson to Person
func personFrom(_ simple: SimplePerson) -> Person {
    let person = Person()
    person.id = simple.id
    person.firstname = simple.firstname
    person.lastname = simple.lastname
    person.image = simple.image
    return person
}
```

#### 4. Update `CozyEventModal.swift` component

**File:** `lichunWebsocket/Shared/Components/Modals/CozyEventModal.swift`

Add character avatars and location background to question modals:

```swift
struct CozyEventModal: View {
    let questionPretext: String
    let questionText: String
    let answerTexts: [String]
    let answers: [String]
    let characters: [SimplePerson]?  // NEW
    let locationImage: String?       // NEW
    let onAnswer: (String) -> Void

    var body: some View {
        ZStack {
            Color.black.opacity(0.5)
                .ignoresSafeArea()
                .onTapGesture { }

            VStack(spacing: AppSpacing.lg) {
                // NEW: Character avatars at top
                if let characters = characters, !characters.isEmpty {
                    HStack(spacing: 8) {
                        ForEach(characters.prefix(4)) { character in
                            AvatarView(
                                person: personFrom(character),
                                size: 48,
                                showBorder: true,
                                borderColor: .white
                            )
                        }
                    }
                    .padding(.top, AppSpacing.sm)
                }

                // Decorative top accent bar
                RoundedRectangle(cornerRadius: 4)
                    .fill(...)

                // Question content
                VStack(spacing: AppSpacing.md) {
                    // ... existing question display ...
                }

                // Answer options
                VStack(spacing: AppSpacing.md) {
                    // ... existing answers ...
                }
            }
            .padding(.vertical, AppSpacing.xl)
            .frame(maxWidth: 400)
            .background(backgroundContent)  // UPDATED
            // ... existing styling ...
        }
    }

    // NEW: Background with optional location image
    @ViewBuilder
    var backgroundContent: some View {
        ZStack {
            AppColors.surfaceElevated

            if let imageURL = locationImage, let url = URL(string: imageURL) {
                AsyncImage(url: url) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .opacity(0.1)
                        .blur(radius: 3)
                } placeholder: {
                    EmptyView()
                }
            }
        }
    }

    func personFrom(_ simple: SimplePerson) -> Person {
        let person = Person()
        person.id = simple.id
        person.firstname = simple.firstname
        person.lastname = simple.lastname
        person.image = simple.image
        return person
    }
}
```

#### 5. Update WebSocketService parsing

**File:** `lichunWebsocket/WebSocketService.swift`

Ensure `characters` and `image` fields are parsed from incoming question events:

```swift
// In the message parsing section where questionEvent is handled
if event.type == "questionEvent" {
    let question = Question(
        id: event.id,
        question: event.message,
        answers: event.answers,
        characters: event.characters,  // NEW
        image: event.image             // NEW
    )
    // ... rest of question handling ...
}
```

## Data Flow

```
┌─────────────────────────────────────────────────────────────┐
│ 1. Event Function (Backend)                                 │
│    - Event author specifies characters=[player.c, friend]   │
│    - Event author optionally sets image="https://..."       │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. messageFunction() / questionFunction()                   │
│    - Serializes characters to lightweight dict              │
│      {'id': '123', 'firstname': 'John', 'lastname': 'Doe',  │
│       'image': 'https://api.dicebear.com/...'}             │
│    - Attaches to messageEvent.characters array              │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│ 3. WebSocket Send (app.py)                                  │
│    - Event object sent to client via websocket.send()      │
│    - JSON includes optional 'characters' and 'image' fields │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│ 4. iOS WebSocketService                                     │
│    - Parses MessageEvent/Question with SimplePerson array   │
│    - Stores in @Published properties                        │
└────────────────────┬────────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│ 5. UI Components                                            │
│    - LifeEventCard: Renders avatars + location background   │
│    - CozyEventModal: Renders avatars + location background  │
│    - AvatarView: Displays person.image SVG                  │
└─────────────────────────────────────────────────────────────┘
```

## Example Usage

### Example 1: School Fight Event

```python
def schoolFight(player):
    bully = find_character(player, role="bully")
    friend = find_character(player, role="friend")

    return messageFunction(
        'schoolFight',
        f"You got into a fight with {bully.firstname}! {friend.firstname} tried to help.",
        player,
        True,
        title="School Fight",
        characters=[player.c, bully, friend],  # Show 3 avatars
        image="https://cdn.../school_hallway.png",
        energyCost=-10,
        affinityChange=-5
    )
```

**iOS Display:**
- 3 overlapping circular avatars (you, bully, friend)
- School hallway image as subtle background (15% opacity, blurred)
- Event message and costs below

### Example 2: Birthday Party Question

```python
def birthdayPartyQuestion(player):
    parents = [player.r[0], player.r[1]]  # Mom and Dad

    return questionFunction(
        'birthdayParty',
        "Your parents are throwing you a birthday party! What theme do you want?",
        player,
        True,
        answerOptions=["Dinosaurs", "Superheroes", "Princess", "Video Games"],
        characters=[player.c] + parents,  # Show child + parents
        image="https://cdn.../birthday_home.png"
    )
```

**iOS Display:**
- 3 avatars at top of modal (child, mom, dad)
- Home interior as background overlay
- Question text and 4 answer buttons

### Example 3: Simple Event (No Visuals)

```python
def morningWakeup(player):
    return messageFunction(
        'morningWakeup',
        "You wake up feeling refreshed!",
        player,
        True,
        energyCost=20
    )
```

**iOS Display:**
- Standard event card (no avatars, no location)
- Works exactly as before - backward compatible

## Migration Strategy

### Phase 1: Infrastructure (This Phase)
- ✅ Add `characters` and `image` fields to event classes
- ✅ Update helper functions to accept new parameters
- ✅ Update iOS models and UI components
- ✅ All changes are backward compatible

### Phase 2: Gradual Event Updates (Future)
- Event authors incrementally add `characters` and `image` to existing events
- Prioritize high-visibility events (first day of school, romance, major milestones)
- Low-priority events can remain text-only

### Phase 3: Location Image Library (Future - Optional)
- Build library of reusable location images (school, home, park, restaurant, etc.)
- Create helper function to auto-assign common locations
- Not required for this phase - manual assignment only

## Testing Plan

### Backend Tests

1. **Event serialization**
   - Create messageEvent with characters array
   - Verify characters serialize to lightweight dicts
   - Verify missing characters field defaults to empty array

2. **Backward compatibility**
   - Send event without characters/image fields
   - Verify event processes normally
   - Verify iOS parses without errors

### iOS Tests

1. **Model parsing**
   - Parse MessageEvent JSON with characters array
   - Parse Question JSON with characters and image
   - Parse events without optional fields

2. **UI rendering**
   - LifeEventCard with 1-4 character avatars
   - LifeEventCard with location image overlay
   - CozyEventModal with avatars and location
   - Events without avatars/images render normally

3. **Avatar display**
   - Avatars overlap correctly (negative spacing)
   - SVG images load via AvatarView
   - Border and sizing correct

## Success Criteria

- ✅ Backend can send character data with events
- ✅ Backend can send location images with events
- ✅ iOS displays character avatars in event cards
- ✅ iOS displays character avatars in question modals
- ✅ iOS displays location images as background overlays
- ✅ Existing events without these fields work unchanged
- ✅ No performance degradation
- ✅ Payload size increase is minimal (<500 bytes per event with 4 characters)

## Future Enhancements (Out of Scope)

1. **Web frontend support** - Add avatar/location rendering to main.js
2. **Auto-location detection** - Derive location from player.c.location
3. **Location image library** - Pre-generated scene images for common locations
4. **Character limit customization** - Allow events to show more than 4 avatars
5. **Avatar animations** - Subtle entrance animations for avatars
6. **Smart image caching** - Cache location images for repeated events

## Files Modified

### Backend (Python)
- `ws/events/base.py` - Add characters field to event classes, update helper functions

### iOS Frontend (SwiftUI)
- `lichunWebsocket/Core/Models/MessageEvent.swift` - Add SimplePerson and characters field
- `lichunWebsocket/Core/Models/Question.swift` - Add characters and image fields
- `lichunWebsocket/Features/Home/Components/LifeEventCard.swift` - Render avatars and location
- `lichunWebsocket/Shared/Components/Modals/CozyEventModal.swift` - Render avatars and location
- `lichunWebsocket/WebSocketService.swift` - Parse characters and image from events

### Documentation
- `docs/plans/2025-11-13-event-avatars-locations-design.md` - This document

## Timeline Estimate

- Backend changes: 1-2 hours
- iOS model updates: 30 minutes
- iOS UI components: 2-3 hours
- Testing: 1-2 hours
- **Total: 5-8 hours**

## Approval

- [x] Design approved
- [ ] Backend implementation complete
- [ ] iOS frontend implementation complete
- [ ] Testing complete
- [ ] Deployed to production
