# 🎭 Hearthside Secrets - Portrait System

**Version**: 1.0.0  
**Status**: Implemented

A participant portrait system that displays real WoW character portraits for active speakers using party/raid unit tokens and GUID tracking.

---

## Overview

The portrait system provides immersive character representation during mystery gameplay by showing real player portraits when they speak, narrate, or are the focus of a scene.

---

## Architecture

### **Core Components**

1. **ParticipantRoster** - Tracks all participants with GUID/unit token mapping
2. **PortraitManager** - Resolves participants and updates portrait display
3. **SpeakerFrame** - UI component showing portrait and dialogue
4. **Event Handlers** - Monitors group roster changes

### **Data Flow**

```
Group Roster Update
        ↓
ParticipantRoster:ScanGroup()
        ↓
GUID → UnitToken Mapping
        ↓
PortraitManager:SetActiveSpeaker()
        ↓
SetPortraitTexture(unitToken)
        ↓
SpeakerFrame displays portrait
```

---

## Modules

### **ParticipantRoster.lua**

**Purpose**: Manage participant roster with GUID tracking

**Key Functions**:
- `ScanGroup()` - Scan party/raid and build roster
- `GetByGUID(guid)` - Get participant by GUID
- `GetByName(name)` - Get participant by name
- `GetUnitToken(guid)` - Get current unit token for GUID
- `ValidateUnitToken(token)` - Check if unit token is valid
- `SetGameRole(guid, role, type)` - Assign mystery role

**Data Structure**:
```lua
{
  byGUID = { [guid] = Participant },
  byUnit = { [unitToken] = guid },
  byName = { [name] = guid }
}

Participant = {
  guid = "Player-1234-ABCD5678",
  name = "Playername-Realm",
  unitToken = "party2",
  class = "WARRIOR",
  classColor = {r, g, b},
  gameRole = "The Thief",
  roleType = "GUILTY",
  isActive = true
}
```

### **PortraitManager.lua**

**Purpose**: Resolve participants and update portrait display

**Key Functions**:
- `Init(texture, nameText, roleText)` - Initialize with UI elements
- `SetActiveSpeakerByGUID(guid)` - Set speaker by GUID
- `SetActiveSpeakerByName(name)` - Set speaker by name
- `UpdatePortrait(participant, unitToken)` - Update portrait texture
- `ShowFallback(participant, name)` - Show fallback icon
- `RefreshCurrentSpeaker()` - Refresh after roster update

**Fallback Strategy**:
1. Try to use unit token with `SetPortraitTexture()`
2. If invalid, refresh roster and retry
3. If still invalid, show class icon
4. If no class, show question mark icon

### **SpeakerFrame.lua**

**Purpose**: UI component for speaker portrait and dialogue

**Key Functions**:
- `Create(parent)` - Create speaker frame UI
- `SetSpeakerByGUID(guid)` - Set speaker by GUID
- `SetSpeakerByName(name)` - Set speaker by name
- `ShowDialogue(text)` - Display dialogue text
- `ShowDialogueWithTypewriter(text, speed)` - Typewriter effect
- `ClearSpeaker()` - Clear current speaker

**UI Layout**:
```
┌────────────────────────────────────┐
│  ┌──────────┐                      │
│  │          │  Speaker Name         │
│  │ Portrait │  "The Thief"          │
│  │          │                       │
│  └──────────┘  "I was in the       │
│                 cellar all night!"  │
└────────────────────────────────────┘
```

---

## Integration

### **GameController Integration**

**Event Handling**:
```lua
-- Registered events
GROUP_ROSTER_UPDATE
RAID_ROSTER_UPDATE
PARTY_CONVERTED_TO_RAID

-- Handler
function OnGroupRosterUpdate(event)
  ParticipantRoster:ScanGroup()
  PortraitManager:RefreshCurrentSpeaker()
end
```

**Game Start**:
```lua
-- Initialize roster
ParticipantRoster:Clear()
ParticipantRoster:ScanGroup()

-- Show host portrait for intro
local hostGUID = UnitGUID("player")
SpeakerFrame:SetSpeakerAndDialogue(
  hostGUID,
  "GUID",
  scenario.story.intro
)
```

### **Role Assignment Integration**

When roles are assigned, the participant roster is updated:

```lua
-- In RoleManager:AssignRoles()
local participant = ParticipantRoster:GetByName(playerName)
if participant then
  ParticipantRoster:SetGameRole(
    participant.guid,
    role.name,
    role.type
  )
end
```

---

## Usage Examples

### **Show Host Narration**

```lua
local hostGUID = UnitGUID("player")
HS.UI.SpeakerFrame:SetSpeakerAndDialogue(
  hostGUID,
  "GUID",
  "The fire burns low. Something is not right tonight..."
)
```

### **Show Player Speaking**

```lua
-- By name
HS.UI.SpeakerFrame:SetSpeakerByName("Playername-Realm")
HS.UI.SpeakerFrame:ShowDialogue("I was in the cellar all night!")

-- By GUID
local participant = HS.Core.ParticipantRoster:GetByName("Playername-Realm")
HS.UI.SpeakerFrame:SetSpeakerByGUID(participant.guid)
```

### **Typewriter Effect**

```lua
HS.UI.SpeakerFrame:SetSpeakerByGUID(guid)
HS.UI.SpeakerFrame:ShowDialogueWithTypewriter(
  "The truth comes to light...",
  0.05  -- speed in seconds per character
)
```

### **Clear Speaker**

```lua
HS.UI.SpeakerFrame:ClearSpeaker()
HS.UI.SpeakerFrame:Hide()
```

---

## Technical Details

### **GUID vs Unit Token**

**GUID** (Globally Unique Identifier):
- Permanent identifier for a character
- Format: `Player-ServerID-CharacterID`
- Never changes, even if group changes
- Used for long-term tracking

**Unit Token**:
- Temporary identifier for group position
- Examples: `player`, `party1`, `raid5`
- Changes when group reorders or converts
- Required for `SetPortraitTexture()`

**Why Both?**:
- GUIDs provide stable identity
- Unit tokens provide WoW API access
- System maps between them dynamically

### **Group Types**

**Solo** (no group):
- Only `player` unit available
- Host can show own portrait
- Other participants use fallbacks

**Party** (2-5 players):
- Units: `player`, `party1`, `party2`, `party3`, `party4`
- All participants should have valid units

**Raid** (6-40 players):
- Units: `raid1`, `raid2`, ..., `raid40`
- Party units become invalid
- Roster automatically rescans

### **Portrait API**

**SetPortraitTexture(texture, unitToken)**:
- WoW API function
- Requires valid unit token
- Generates 2D character portrait
- Automatically handles race/gender/customization

**Fallback Icons**:
- Class icons: `Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes`
- Question mark: `Interface\\Icons\\INV_Misc_QuestionMark`
- Custom scenario portraits (future)

---

## Edge Cases

### **Player Disconnects**

```lua
-- Participant marked inactive
participant.isActive = false

-- Portrait shows class icon fallback
PortraitManager:ShowFallback(participant, participant.name)
```

### **Party → Raid Conversion**

```lua
-- Event: PARTY_CONVERTED_TO_RAID
-- Unit tokens change: party1 → raid1

-- Automatic handling:
ParticipantRoster:ScanGroup()  -- Rebuilds unit mappings
PortraitManager:RefreshCurrentSpeaker()  -- Updates portrait
```

### **Player Leaves Group**

```lua
-- Unit token becomes invalid
-- GUID remains in roster

-- Fallback shown until player rejoins
if not ValidateUnitToken(unitToken) then
  ShowFallback(participant)
end
```

### **Cross-Realm Players**

```lua
-- Full name includes realm
name = "Playername-Realm"

-- GUIDs work across realms
guid = "Player-1234-ABCD5678"

-- Portraits work for any group member
SetPortraitTexture(texture, "party2")  -- Works cross-realm
```

---

## Performance

### **Optimization Strategies**

1. **Scan on Events Only** - Not every frame
2. **Cache GUIDs** - Don't call `UnitGUID()` repeatedly
3. **Lazy Validation** - Only validate when setting speaker
4. **Efficient Lookups** - Multiple index tables (GUID, name, unit)

### **Memory Usage**

- ~200 bytes per participant
- Max 40 participants (raid size)
- Total: ~8 KB maximum

### **CPU Usage**

- Roster scan: ~1ms for 40 players
- Portrait update: <0.1ms
- Event handling: Negligible

---

## Future Enhancements

### **Planned Features**

1. **Custom Scenario Portraits** - Artist-drawn character cards
2. **3D Model Portraits** - Use `PlayerModel` frames
3. **Portrait Animations** - Fade in/out, pulse effects
4. **Voice Indicators** - Show who's speaking in voice chat
5. **Portrait History** - Recent speakers sidebar
6. **Spectator Portraits** - Custom icons for non-group spectators

### **API Extensions**

```lua
-- Future API ideas
SpeakerFrame:SetPortraitStyle("2D" | "3D" | "CUSTOM")
SpeakerFrame:AddPortraitAnimation("PULSE" | "GLOW")
SpeakerFrame:ShowMultipleSpeakers({guid1, guid2})
ParticipantRoster:AddSpectator(name, customIcon)
```

---

## Troubleshooting

### **Portrait Not Showing**

1. Check if player is in group: `IsInGroup()`
2. Verify unit token: `UnitExists(unitToken)`
3. Check GUID mapping: `ParticipantRoster:DebugPrint()`
4. Refresh roster: `ParticipantRoster:ScanGroup()`

### **Wrong Portrait Displayed**

1. Roster may be stale - refresh it
2. Unit token may have changed - rescan group
3. GUID mapping may be incorrect - clear and rebuild

### **Fallback Icon Showing**

1. Player not in group - expected behavior
2. Player disconnected - expected behavior
3. Unit token invalid - roster needs refresh
4. Cross-realm issue - check full name format

### **Debug Commands**

```lua
-- Print roster
/run GatheringStone.HS.Core.ParticipantRoster:DebugPrint()

-- Force roster scan
/run GatheringStone.HS.Core.ParticipantRoster:ScanGroup()

-- Check current speaker
/run print(GatheringStone.HS.Core.PortraitManager:GetCurrentSpeaker().name)

-- Test portrait
/run GatheringStone.HS.UI.SpeakerFrame:SetSpeakerByGUID(UnitGUID("player"))
```

---

## Credits

**Design**: Based on social deduction game UX patterns  
**Implementation**: The Gathering Stone Team  
**Inspired by**: Among Us, Werewolf, Clue character reveals

---

## Summary

The portrait system provides immersive character representation by:

✓ Tracking participants via GUID (stable identity)  
✓ Mapping to unit tokens (WoW API access)  
✓ Displaying real character portraits  
✓ Handling group changes gracefully  
✓ Providing fallbacks when needed  
✓ Integrating seamlessly with game phases  

**Result**: Players see real character faces during mystery gameplay, creating a more engaging and immersive social experience.
