first commit
This commit is contained in:
738
public_web/docs/MULTIPLAYER.md
Normal file
738
public_web/docs/MULTIPLAYER.md
Normal file
@@ -0,0 +1,738 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user