# Multiplayer System - Web Frontend

**Status:** Planned
**Phase:** 6 (Multiplayer Sessions)
**Last Updated:** November 18, 2025

---

## Overview

The Web Frontend handles the UI/UX for multiplayer sessions, including lobby screens, active session displays, combat interfaces, and realtime synchronization via JavaScript/HTMX patterns.

**Frontend Responsibilities:**
- Render multiplayer session creation forms
- Display lobby with player list and ready status
- Show active session UI (timer, party status, combat)
- Handle realtime updates via Appwrite Realtime WebSocket
- Submit player actions to API backend
- Display session completion and rewards

**Technical Stack:**
- **Templates**: Jinja2
- **Interactivity**: HTMX for AJAX interactions
- **Realtime**: Appwrite JavaScript SDK for WebSocket subscriptions
- **Styling**: Custom CSS (responsive design)

---

## UI/UX Design

### Session Creation Screen (Host)

**Route:** `/multiplayer/create`

**Template:** `templates/multiplayer/create.html`

```html
{% extends "base.html" %}

{% block content %}
<div class="multiplayer-create">
    <h1>Create Multiplayer Session <span class="badge-premium">Premium</span></h1>
    <p>Invite your friends to a 2-hour co-op adventure!</p>

    <form hx-post="/api/v1/sessions/multiplayer/create"
          hx-target="#session-result"
          hx-swap="innerHTML">

        <div class="form-group">
            <label>Party Size:</label>
            <div class="radio-group">
                <input type="radio" name="max_players" value="2" id="size-2">
                <label for="size-2">2 Players</label>

                <input type="radio" name="max_players" value="3" id="size-3">
                <label for="size-3">3 Players</label>

                <input type="radio" name="max_players" value="4" id="size-4" checked>
                <label for="size-4">4 Players</label>
            </div>
        </div>

        <div class="form-group">
            <label>Difficulty:</label>
            <div class="radio-group">
                <input type="radio" name="difficulty" value="easy" id="diff-easy">
                <label for="diff-easy">Easy</label>

                <input type="radio" name="difficulty" value="medium" id="diff-medium" checked>
                <label for="diff-medium">Medium</label>

                <input type="radio" name="difficulty" value="hard" id="diff-hard">
                <label for="diff-hard">Hard</label>

                <input type="radio" name="difficulty" value="deadly" id="diff-deadly">
                <label for="diff-deadly">Deadly</label>
            </div>
        </div>

        <p class="info-text">
            AI will generate a custom campaign for your party based on your
            characters' levels and the selected difficulty.
        </p>

        <button type="submit" class="btn btn-primary">Create Session</button>
    </form>

    <div id="session-result"></div>
</div>
{% endblock %}
```

**HTMX Pattern:**
- Form submission via `hx-post` to API endpoint
- Response replaces `#session-result` div
- API returns session details and redirects to lobby

### Lobby Screen

**Route:** `/multiplayer/lobby/{session_id}`

**Template:** `templates/multiplayer/lobby.html`

```html
{% extends "base.html" %}

{% block content %}
<div class="multiplayer-lobby">
    <h1>Multiplayer Lobby
        <button class="btn btn-secondary"
                hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/leave"
                hx-confirm="Are you sure you want to leave?">
            Leave Lobby
        </button>
    </h1>

    <div class="invite-section">
        <p><strong>Session Code:</strong> {{ session.invite_code }}</p>
        <p>
            <strong>Invite Link:</strong>
            <input type="text"
                   id="invite-link"
                   value="https://codeofconquest.com/join/{{ session.invite_code }}"
                   readonly>
            <button onclick="copyInviteLink()" class="btn btn-sm">Copy Link</button>
        </p>
    </div>

    <div class="lobby-info">
        <p><strong>Difficulty:</strong> {{ session.campaign.difficulty|title }}</p>
        <p><strong>Duration:</strong> 2 hours</p>
    </div>

    <div id="party-list" class="party-list">
        <!-- Dynamically updated via realtime -->
        {% for member in session.party_members %}
        <div class="party-member" data-user-id="{{ member.user_id }}">
            {% if member.is_host %}
            <span class="crown">👑</span>
            {% endif %}
            <span class="username">{{ member.username }} {% if member.is_host %}(Host){% endif %}</span>
            <span class="character">{{ member.character_snapshot.name }} - {{ member.character_snapshot.player_class.name }} Lvl {{ member.character_snapshot.level }}</span>
            <span class="ready-status">
                {% if member.is_ready %}
                ✅ Ready
                {% else %}
                ⏳ Not Ready
                {% endif %}
            </span>
        </div>
        {% endfor %}

        <!-- Empty slots -->
        {% for i in range(session.max_players - session.party_members|length) %}
        <div class="party-member empty">
            <span>[Waiting for player...]</span>
        </div>
        {% endfor %}
    </div>

    <div class="lobby-actions">
        {% if current_user.user_id == session.host_user_id %}
        <!-- Host controls -->
        <button id="start-session-btn"
                class="btn btn-primary"
                hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/start"
                hx-target="#lobby-status"
                {% if not all_ready %}disabled{% endif %}>
            Start Session
        </button>
        {% else %}
        <!-- Player ready toggle -->
        <button class="btn btn-primary"
                hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/ready"
                hx-target="#lobby-status">
            {% if current_member.is_ready %}Not Ready{% else %}Ready{% endif %}
        </button>
        {% endif %}
    </div>

    <div id="lobby-status"></div>
</div>

<script>
// Realtime subscription for lobby updates
const { Client, Databases } = Appwrite;

const client = new Client()
    .setEndpoint('{{ appwrite_endpoint }}')
    .setProject('{{ appwrite_project_id }}');

const databases = new Databases(client);

// Subscribe to session updates
client.subscribe(`databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session.session_id }}`, response => {
    console.log('Session updated:', response);

    // Update party list
    updatePartyList(response.payload);

    // Check if all ready, enable/disable start button
    checkAllReady(response.payload);

    // If session started, redirect to active session
    if (response.payload.status === 'active') {
        window.location.href = '/multiplayer/session/{{ session.session_id }}';
    }
});

function updatePartyList(sessionData) {
    // Update party member ready status dynamically
    sessionData.party_members.forEach(member => {
        const memberEl = document.querySelector(`.party-member[data-user-id="${member.user_id}"]`);
        if (memberEl) {
            const statusEl = memberEl.querySelector('.ready-status');
            statusEl.innerHTML = member.is_ready ? '✅ Ready' : '⏳ Not Ready';
        }
    });
}

function checkAllReady(sessionData) {
    const allReady = sessionData.party_members.every(m => m.is_ready);
    const startBtn = document.getElementById('start-session-btn');
    if (startBtn) {
        startBtn.disabled = !allReady;
    }
}

function copyInviteLink() {
    const linkInput = document.getElementById('invite-link');
    linkInput.select();
    document.execCommand('copy');
    alert('Invite link copied to clipboard!');
}
</script>
{% endblock %}
```

**Realtime Pattern:**
- JavaScript subscribes to Appwrite Realtime for session updates
- Updates player ready status dynamically
- Redirects to active session when host starts
- No page reload required

### Active Session Screen

**Route:** `/multiplayer/session/{session_id}`

**Template:** `templates/multiplayer/session.html`

```html
{% extends "base.html" %}

{% block content %}
<div class="multiplayer-session">
    <div class="session-header">
        <h1>{{ session.campaign.title }}</h1>
        <div class="timer" id="session-timer">
            ⏱️ <span id="time-remaining">{{ time_remaining }}</span> Remaining
        </div>
    </div>

    <div class="campaign-progress">
        <p>Campaign Progress:
            <span class="progress-bar">
                <span class="progress-fill" style="width: {{ (session.current_encounter_index / session.campaign.encounters|length) * 100 }}%"></span>
            </span>
            ({{ session.current_encounter_index }}/{{ session.campaign.encounters|length }} encounters)
        </p>
    </div>

    <div class="party-status">
        <h3>Party:</h3>
        <div class="party-members" id="party-status">
            {% for member in session.party_members %}
            <div class="party-member-status">
                <span class="name">{{ member.character_snapshot.name }}</span>
                <span class="hp">HP: <span id="hp-{{ member.character_id }}">{{ member.character_snapshot.current_hp }}</span>/{{ member.character_snapshot.max_hp }}</span>
                {% if not member.is_connected %}
                <span class="disconnected">⚠️ Disconnected</span>
                {% endif %}
            </div>
            {% endfor %}
        </div>
    </div>

    <div class="narrative-panel" id="narrative-panel">
        <h3>Narrative:</h3>
        <div class="conversation-history">
            {% for entry in session.conversation_history %}
            <div class="conversation-entry {{ entry.role }}">
                <strong>{{ entry.role|title }}:</strong> {{ entry.content }}
            </div>
            {% endfor %}
        </div>
    </div>

    {% if session.combat_encounter %}
    <!-- Combat UI -->
    <div class="combat-section" id="combat-section">
        <h3>Combat: {{ current_encounter.title }}</h3>

        <div class="turn-order" id="turn-order">
            <h4>Turn Order:</h4>
            <ol>
                {% for combatant_id in session.combat_encounter.turn_order %}
                <li class="{% if loop.index0 == session.combat_encounter.current_turn_index %}current-turn{% endif %}">
                    {{ get_combatant_name(combatant_id) }}
                    {% if is_current_user_turn(combatant_id) %}
                    <strong>(Your Turn!)</strong>
                    {% endif %}
                </li>
                {% endfor %}
            </ol>
        </div>

        {% if is_current_user_turn() %}
        <!-- Combat action form (only visible on player's turn) -->
        <div class="combat-actions">
            <h4>Your Action:</h4>
            <form hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/combat/action"
                  hx-target="#combat-result"
                  hx-swap="innerHTML">

                <button type="submit" name="action_type" value="attack" class="btn btn-action">Attack</button>

                <select name="ability_id" class="ability-select">
                    <option value="">Use Ability ▼</option>
                    {% for ability in current_character.abilities %}
                    <option value="{{ ability.ability_id }}">{{ ability.name }}</option>
                    {% endfor %}
                </select>

                <select name="item_id" class="item-select">
                    <option value="">Use Item ▼</option>
                    {% for item in current_character.inventory %}
                    <option value="{{ item.item_id }}">{{ item.name }}</option>
                    {% endfor %}
                </select>

                <button type="submit" name="action_type" value="defend" class="btn btn-action">Defend</button>
            </form>
            <div id="combat-result"></div>
        </div>
        {% else %}
        <p class="waiting-turn">Waiting for other players...</p>
        {% endif %}
    </div>
    {% endif %}
</div>

<script>
// Realtime subscription for combat and session updates
const client = new Client()
    .setEndpoint('{{ appwrite_endpoint }}')
    .setProject('{{ appwrite_project_id }}');

// Subscribe to session updates
client.subscribe(`databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session.session_id }}`, response => {
    console.log('Session updated:', response);

    // Update timer
    updateTimer(response.payload.time_remaining_seconds);

    // Update party status
    updatePartyStatus(response.payload.party_members);

    // Update combat state
    if (response.payload.combat_encounter) {
        updateCombatUI(response.payload.combat_encounter);
    }

    // Check for session expiration
    if (response.payload.status === 'expired' || response.payload.status === 'completed') {
        window.location.href = `/multiplayer/complete/{{ session.session_id }}`;
    }
});

// Timer countdown
let timeRemaining = {{ session.time_remaining_seconds }};
setInterval(() => {
    if (timeRemaining > 0) {
        timeRemaining--;
        updateTimerDisplay(timeRemaining);
    }
}, 1000);

function updateTimerDisplay(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;

    const display = `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
    document.getElementById('time-remaining').innerText = display;

    // Warnings
    if (seconds === 600) alert('⚠️ 10 minutes remaining!');
    if (seconds === 300) alert('⚠️ 5 minutes remaining!');
    if (seconds === 60) alert('🚨 1 minute remaining!');
}

function updateTimer(seconds) {
    timeRemaining = seconds;
}

function updatePartyStatus(partyMembers) {
    partyMembers.forEach(member => {
        const hpEl = document.getElementById(`hp-${member.character_id}`);
        if (hpEl) {
            hpEl.innerText = member.character_snapshot.current_hp;
        }
    });
}

function updateCombatUI(combat) {
    // Update turn order highlighting
    const turnItems = document.querySelectorAll('.turn-order li');
    turnItems.forEach((item, index) => {
        item.classList.toggle('current-turn', index === combat.current_turn_index);
    });

    // Reload page section if turn changed (HTMX can handle partial updates)
    htmx.ajax('GET', `/multiplayer/session/{{ session.session_id }}/combat-ui`, {target: '#combat-section', swap: 'outerHTML'});
}
</script>
{% endblock %}
```

**Realtime Pattern:**
- Subscribe to session document changes
- Update timer countdown locally
- Update party HP dynamically
- Reload combat UI section when turn changes
- Redirect to completion screen when session ends

### Session Complete Screen

**Route:** `/multiplayer/complete/{session_id}`

**Template:** `templates/multiplayer/complete.html`

```html
{% extends "base.html" %}

{% block content %}
<div class="session-complete">
    <h1>Campaign Complete!</h1>

    <div class="completion-banner">
        <h2>🎉 {{ session.campaign.title }} - VICTORY! 🎉</h2>
        <p>{{ session.campaign.description }}</p>
    </div>

    <div class="completion-stats">
        <p><strong>Time:</strong> {{ session_duration }} (Completion Bonus: +{{ completion_bonus_percent }}%)</p>
    </div>

    <div class="rewards-summary">
        <h3>Rewards:</h3>
        <ul>
            <li>💰 {{ rewards.gold_per_player }} gold ({{ base_gold }} + {{ bonus_gold }} bonus)</li>
            <li>⭐ {{ rewards.experience_per_player }} XP ({{ base_xp }} + {{ bonus_xp }} bonus)</li>
            <li>📦 Loot: {{ rewards.shared_items|join(', ') }}</li>
        </ul>

        {% if leveled_up %}
        <p class="level-up">🎊 Level Up! {{ current_character.name }} reached level {{ current_character.level }}!</p>
        {% endif %}
    </div>

    <div class="party-stats">
        <h3>Party Members:</h3>
        <ul>
            {% for member in session.party_members %}
            <li>
                <strong>{{ member.username }}</strong> ({{ member.character_snapshot.name }})
                {% if member.mvp_badge %}
                - {{ member.mvp_badge }}
                {% endif %}
            </li>
            {% endfor %}
        </ul>
    </div>

    <div class="actions">
        <a href="/sessions/history/{{ session.session_id }}" class="btn btn-secondary">View Full Session Log</a>
        <a href="/dashboard" class="btn btn-primary">Return to Main Menu</a>
    </div>
</div>
{% endblock %}
```

---

## HTMX Integration Patterns

### Form Submissions

All multiplayer actions use HTMX for seamless AJAX submissions:

**Pattern:**
```html
<form hx-post="/api/v1/sessions/multiplayer/{session_id}/action"
      hx-target="#result-div"
      hx-swap="innerHTML">
    <!-- form fields -->
    <button type="submit">Submit</button>
</form>
```

**Benefits:**
- No page reload
- Partial page updates
- Progressive enhancement (works without JS)

### Realtime Updates

Combine HTMX with Appwrite Realtime for optimal UX:

**Pattern:**
```javascript
// Listen for realtime events
client.subscribe(`channel`, response => {
    // Update via HTMX partial reload
    htmx.ajax('GET', '/partial-url', {
        target: '#target-div',
        swap: 'outerHTML'
    });
});
```

### Polling Fallback

For browsers without WebSocket support, use HTMX polling:

```html
<div hx-get="/api/v1/sessions/multiplayer/{session_id}/status"
     hx-trigger="every 5s"
     hx-target="#status-div">
    <!-- Status updates every 5 seconds -->
</div>
```

---

## Template Organization

### Directory Structure

```
templates/multiplayer/
├── create.html              # Session creation form
├── lobby.html               # Lobby screen
├── session.html             # Active session UI
├── complete.html            # Session complete screen
├── partials/
│   ├── party_list.html      # Reusable party member list
│   ├── combat_ui.html       # Combat interface partial
│   └── timer.html           # Timer component
└── components/
    ├── invite_link.html     # Invite link copy widget
    └── ready_toggle.html    # Ready status toggle button
```

### Partial Template Pattern

**Example:** `templates/multiplayer/partials/party_list.html`

```html
<!-- Reusable party list component -->
<div class="party-list">
    {% for member in party_members %}
    <div class="party-member" data-user-id="{{ member.user_id }}">
        {% if member.is_host %}
        <span class="crown">👑</span>
        {% endif %}
        <span class="username">{{ member.username }}</span>
        <span class="character">{{ member.character_snapshot.name }} - {{ member.character_snapshot.player_class.name }} Lvl {{ member.character_snapshot.level }}</span>
        <span class="ready-status">
            {% if member.is_ready %}✅ Ready{% else %}⏳ Not Ready{% endif %}
        </span>
    </div>
    {% endfor %}
</div>
```

**Usage:**
```html
{% include 'multiplayer/partials/party_list.html' with party_members=session.party_members %}
```

---

## JavaScript/Appwrite Realtime

### Setup

**Base template** (`templates/base.html`):

```html
<head>
    <!-- Appwrite SDK -->
    <script src="https://cdn.jsdelivr.net/npm/appwrite@latest"></script>
</head>
```

### Subscription Patterns

**Session Updates:**
```javascript
const client = new Appwrite.Client()
    .setEndpoint('{{ config.appwrite_endpoint }}')
    .setProject('{{ config.appwrite_project_id }}');

// Subscribe to specific session
client.subscribe(
    `databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session_id }}`,
    response => {
        console.log('Session updated:', response.payload);

        // Handle different event types
        switch(response.events[0]) {
            case 'databases.*.collections.*.documents.*.update':
                handleSessionUpdate(response.payload);
                break;
        }
    }
);
```

**Combat Updates:**
```javascript
client.subscribe(
    `databases.{{ db_id }}.collections.combat_encounters.documents.{{ encounter_id }}`,
    response => {
        updateCombatUI(response.payload);
    }
);
```

### Error Handling

```javascript
client.subscribe(channel, response => {
    // Handle updates
}, error => {
    console.error('Realtime error:', error);
    // Fallback to polling
    startPolling();
});

function startPolling() {
    setInterval(() => {
        htmx.ajax('GET', '/api/v1/sessions/multiplayer/{{ session_id }}', {
            target: '#session-container',
            swap: 'outerHTML'
        });
    }, 5000);
}
```

---

## View Layer Implementation

### Flask View Functions

**Create Session View:**
```python
@multiplayer_bp.route('/create', methods=['GET', 'POST'])
@require_auth
def create_session():
    """Render session creation form."""

    if request.method == 'POST':
        # Forward to API backend
        response = requests.post(
            f"{API_BASE_URL}/api/v1/sessions/multiplayer/create",
            json=request.form.to_dict(),
            headers={"Authorization": f"Bearer {session['auth_token']}"}
        )

        if response.status_code == 200:
            session_data = response.json()['result']
            return redirect(url_for('multiplayer.lobby', session_id=session_data['session_id']))
        else:
            flash('Failed to create session', 'error')

    return render_template('multiplayer/create.html')

@multiplayer_bp.route('/lobby/<session_id>')
@require_auth
def lobby(session_id):
    """Display lobby screen."""

    # Fetch session from API
    response = requests.get(
        f"{API_BASE_URL}/api/v1/sessions/multiplayer/{session_id}",
        headers={"Authorization": f"Bearer {session['auth_token']}"}
    )

    session_data = response.json()['result']
    return render_template('multiplayer/lobby.html', session=session_data)
```

---

## Testing Checklist

### Manual Testing Tasks

- [ ] Session creation form submits correctly
- [ ] Invite link copies to clipboard
- [ ] Lobby updates when players join
- [ ] Ready status toggles work
- [ ] Host can start session when all ready
- [ ] Timer displays and counts down correctly
- [ ] Party HP updates during combat
- [ ] Combat actions submit correctly
- [ ] Turn order highlights current player
- [ ] Realtime updates work across multiple browsers
- [ ] Session completion screen displays rewards
- [ ] Disconnection handling shows warnings

---

## Related Documentation

- **[/api/docs/MULTIPLAYER.md](../../api/docs/MULTIPLAYER.md)** - Backend API endpoints and business logic
- **[TEMPLATES.md](TEMPLATES.md)** - Template structure and conventions
- **[HTMX_PATTERNS.md](HTMX_PATTERNS.md)** - HTMX integration patterns
- **[TESTING.md](TESTING.md)** - Manual testing guide

---

**Document Version:** 1.0 (Microservices Split)
**Created:** November 18, 2025
**Last Updated:** November 18, 2025
