first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,767 @@
# Godot Client Architecture
## Overview
The Godot client is a native frontend for Code of Conquest that connects to the Flask backend via REST API. It provides a consistent experience across desktop, mobile, and web platforms.
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Godot Client │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ UI Layer (Scenes) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Auth │ │ Char │ │ Combat │ │ World │ │ │
│ │ │ Screens│ │ Mgmt │ │ UI │ │ UI │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Component Layer │ │
│ │ ┌─────────┐ ┌──────┐ ┌──────────┐ ┌──────┐ │ │
│ │ │ Buttons │ │ Cards│ │FormFields│ │ ... │ │ │
│ │ └─────────┘ └──────┘ └──────────┘ └──────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Service Layer (Singletons) │ │
│ │ ┌──────────────┐ ┌────────────────┐ │ │
│ │ │ HTTPClient │ │ StateManager │ │ │
│ │ │ - API calls │ │ - Session │ │ │
│ │ │ - Auth token │ │ - Characters │ │ │
│ │ │ - Error │ │ - Wizard state │ │ │
│ │ │ handling │ │ - Persistence │ │ │
│ │ └──────────────┘ └────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Data Models (GDScript) │ │
│ │ Character, CharacterClass, Origin, Skill, etc. │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
HTTP/JSON REST API
┌─────────────────────────────────────────────────────────────┐
│ Flask Backend │
│ (Existing REST API) │
│ /api/v1/auth/*, /api/v1/characters/* │
└─────────────────────────────────────────────────────────────┘
```
## Layers
### 1. UI Layer (Scenes)
**Location**: `scenes/`
Individual screens and pages of the application.
**Responsibilities**:
- Render UI elements
- Handle user input
- Display data from StateManager
- Trigger API calls via HTTPClient
- Navigate between scenes
**Examples**:
- `scenes/auth/login.tscn` - Login screen
- `scenes/character/character_list.tscn` - Character list
- `scenes/character/create_wizard_step1.tscn` - Character creation step 1
**Communication**:
- Uses components from Component Layer
- Reads/writes to StateManager
- Calls HTTPClient for API requests
- Emits signals for navigation
### 2. Component Layer
**Location**: `scenes/components/`, `scripts/components/`
Reusable UI components shared across scenes.
**Responsibilities**:
- Provide consistent UI elements
- Encapsulate common patterns
- Handle component-specific logic
- Emit signals for parent scenes
**Examples**:
- `CustomButton` - Themed button with variants
- `Card` - Container with header/footer
- `FormField` - Input field with validation
**Communication**:
- Receives props from parent scenes
- Emits signals to parent scenes
- Uses ThemeColors for styling
- Independent of business logic
### 3. Service Layer (Singletons)
**Location**: `scripts/services/`
Global services accessible from anywhere in the app.
**Responsibilities**:
- Manage API communication
- Store global state
- Handle persistence
- Provide utilities
**Singletons**:
#### HTTPClient
- Base URL configuration
- Authentication token management
- JSON request/response handling
- Error handling and retries
- Timeout management
**API**:
```gdscript
HTTPClient.http_get(endpoint, callback, error_callback)
HTTPClient.http_post(endpoint, data, callback, error_callback)
HTTPClient.http_put(endpoint, data, callback, error_callback)
HTTPClient.http_delete(endpoint, callback, error_callback)
HTTPClient.http_patch(endpoint, data, callback, error_callback)
HTTPClient.set_auth_token(token)
```
#### StateManager
- User session state
- Character data cache
- Wizard state (character creation)
- Navigation history
- Settings and preferences
- Save/load to local storage
**API**:
```gdscript
StateManager.set_user_session(user_data, token)
StateManager.get_characters()
StateManager.set_wizard_origin(origin_data)
StateManager.save_state()
```
**Communication**:
- HTTPClient → Backend API (HTTP)
- StateManager → Local storage (FileAccess)
- Services → UI via signals
**Accessing Autoloads**:
Autoload singletons can be accessed in three ways:
1. **Direct access** (simple, works in `_ready()` and later):
```gdscript
func _ready():
HTTPClient.http_get("/api/v1/characters", _on_success)
```
2. **Get node explicitly** (verbose but always works):
```gdscript
func _on_button_clicked():
get_node("/root/HTTPClient").http_get("/api/v1/characters", _on_success)
```
3. **Cached reference with @onready** (best for multiple uses):
```gdscript
@onready var http_client = get_node("/root/HTTPClient")
@onready var state_manager = get_node("/root/StateManager")
func _ready():
http_client.http_get("/api/v1/characters", _on_success)
```
**Important**: When one autoload needs to reference another (like StateManager calling HTTPClient), use `@onready` to avoid script parsing errors. Direct access (`HTTPClient.method()`) only works during runtime, not during script compilation.
### 4. Data Models
**Location**: `scripts/models/`
GDScript classes that mirror backend data structures.
**Responsibilities**:
- Define typed data structures
- Validate data
- Serialize/deserialize JSON
- Provide helper methods
**Examples**:
```gdscript
class_name Character
var id: String
var name: String
var origin_id: String
var class_id: String
var level: int
var hp: int
var max_hp: int
var gold: int
var skills: Array[Skill]
static func from_json(data: Dictionary) -> Character:
# Parse JSON to Character object
pass
```
**Communication**:
- Created from API responses
- Stored in StateManager
- Displayed in UI scenes
## Data Flow
### Example: User Login
```
1. User enters credentials in LoginScene
2. LoginScene calls HTTPClient.http_post("/api/v1/auth/login", {...})
3. HTTPClient sends HTTP request to Flask backend
4. Backend validates and returns JWT token + user data
5. HTTPClient receives response, creates APIResponse object
6. LoginScene receives APIResponse via callback
7. LoginScene calls StateManager.set_user_session(user_data, token)
8. StateManager stores session and emits user_logged_in signal
9. StateManager saves state to local storage
10. LoginScene navigates to CharacterListScene
```
### Example: Load Characters
```
1. CharacterListScene calls HTTPClient.http_get("/api/v1/characters")
2. HTTPClient includes auth token in headers
3. Backend returns array of character data
4. CharacterListScene receives response
5. CharacterListScene calls StateManager.set_characters(characters)
6. StateManager emits characters_updated signal
7. CharacterListScene updates UI with character cards
```
### Example: Character Creation Wizard
```
1. User selects origin in WizardStep1Scene
2. Scene calls StateManager.set_wizard_origin(origin_data)
3. Scene navigates to WizardStep2Scene
4. User selects class in WizardStep2Scene
5. Scene calls StateManager.set_wizard_class(class_data)
6. User continues through steps 3 and 4
7. WizardStep4Scene (confirmation) calls HTTPClient.http_post(
"/api/v1/characters",
{
"origin_id": StateManager.get_wizard_origin().id,
"class_id": StateManager.get_wizard_class().id,
"name": StateManager.get_wizard_name()
}
)
8. Backend creates character and returns character data
9. Scene calls StateManager.add_character(character_data)
10. Scene calls StateManager.reset_wizard()
11. Scene navigates back to CharacterListScene
```
## Navigation
### Scene Management
Godot's built-in scene tree is used for navigation:
```gdscript
# Navigate to another scene
get_tree().change_scene_to_file("res://scenes/character/character_list.tscn")
# Or use PackedScene for preloading
var next_scene = preload("res://scenes/auth/login.tscn")
get_tree().change_scene_to_packed(next_scene)
```
### Navigation Flow
```
App Start
Main Scene (checks auth)
├─ Authenticated? → CharacterListScene
└─ Not authenticated? → LoginScene
Login successful → CharacterListScene
Create Character → WizardStep1Scene
Step 2 → WizardStep2Scene
Step 3 → WizardStep3Scene
Step 4 → WizardStep4Scene
Complete → CharacterListScene
Select Character → CharacterDetailScene
Start Game → GameWorldScene
Enter Combat → CombatScene
```
### Navigation Helper
Consider creating a navigation manager:
```gdscript
# scripts/services/navigation_manager.gd
extends Node
const SCENE_LOGIN = "res://scenes/auth/login.tscn"
const SCENE_CHARACTER_LIST = "res://scenes/character/character_list.tscn"
const SCENE_WIZARD_STEP_1 = "res://scenes/character/create_wizard_step1.tscn"
# ... etc
func navigate_to(scene_path: String) -> void:
StateManager.set_current_scene(scene_path)
get_tree().change_scene_to_file(scene_path)
func navigate_to_login() -> void:
navigate_to(SCENE_LOGIN)
func navigate_to_character_list() -> void:
navigate_to(SCENE_CHARACTER_LIST)
# etc.
```
## Authentication Flow
### Initial Load
```gdscript
# main.gd (attached to main.tscn, first scene loaded)
extends Control
func _ready() -> void:
# StateManager auto-loads from local storage in its _ready()
# Check if user is authenticated
if StateManager.is_authenticated():
# Validate token by making a test API call
HTTPClient.http_get("/api/v1/auth/validate", _on_token_validated, _on_token_invalid)
else:
# Go to login
NavigationManager.navigate_to_login()
func _on_token_validated(response: HTTPClient.APIResponse) -> void:
if response.is_success():
# Token is valid, go to character list
NavigationManager.navigate_to_character_list()
else:
# Token expired, clear and go to login
StateManager.clear_user_session()
NavigationManager.navigate_to_login()
func _on_token_invalid(response: HTTPClient.APIResponse) -> void:
# Network error or token invalid
StateManager.clear_user_session()
NavigationManager.navigate_to_login()
```
### Login
```gdscript
# scenes/auth/login.gd
extends Control
@onready var email_field: FormField = $EmailField
@onready var password_field: FormField = $PasswordField
@onready var login_button: CustomButton = $LoginButton
@onready var error_label: Label = $ErrorLabel
func _on_login_button_clicked() -> void:
# Validate fields
if not email_field.validate() or not password_field.validate():
return
# Show loading state
login_button.set_loading(true)
error_label.visible = false
# Make API call
var credentials = {
"email": email_field.get_value(),
"password": password_field.get_value()
}
HTTPClient.http_post("/api/v1/auth/login", credentials, _on_login_success, _on_login_error)
func _on_login_success(response: APIResponse) -> void:
login_button.set_loading(false)
if response.is_success():
# Extract user data and token from response
var user_data = response.result.get("user", {})
var token = response.result.get("token", "")
# Store in StateManager
StateManager.set_user_session(user_data, token)
# Navigate to character list
NavigationManager.navigate_to_character_list()
else:
# Show error
error_label.text = response.get_error_message()
error_label.visible = true
func _on_login_error(response: APIResponse) -> void:
login_button.set_loading(false)
error_label.text = "Failed to connect. Please try again."
error_label.visible = true
```
### Logout
```gdscript
func _on_logout_button_clicked() -> void:
# Call logout endpoint (optional, for server-side cleanup)
HTTPClient.http_post("/api/v1/auth/logout", {}, _on_logout_complete)
func _on_logout_complete(response: APIResponse) -> void:
# Clear local session regardless of API response
StateManager.clear_user_session()
# Navigate to login
NavigationManager.navigate_to_login()
```
## State Persistence
### What Gets Saved
StateManager saves to `user://coc_state.save` (platform-specific location):
- User session data
- Auth token (if "remember me")
- Character list cache
- Selected character ID
- Character limits
- Settings/preferences
- Timestamp of last save
### Save Triggers
State is saved automatically:
- On login
- On logout (clears save)
- On character list update
- On character selection
- On settings change
- On app exit (if auto-save enabled)
Manual save:
```gdscript
StateManager.save_state()
```
### Load on Startup
StateManager automatically loads on `_ready()`:
- Restores user session if valid
- Restores character list
- Sets HTTPClient auth token
## Error Handling
### API Errors
```gdscript
HTTPClient.http_get("/api/v1/characters", _on_success, _on_error)
func _on_success(response: APIResponse) -> void:
if response.is_success():
# Handle success
var characters = response.result
else:
# API returned error (4xx, 5xx)
_show_error(response.get_error_message())
func _on_error(response: APIResponse) -> void:
# Network error or JSON parse error
_show_error("Failed to connect to server")
```
### Network Errors
HTTPClient handles:
- Connection timeout (30s default)
- DNS resolution failure
- TLS/SSL errors
- No response from server
- Invalid JSON responses
All errors return an `APIResponse` with:
- `status`: HTTP status code (or 500 for network errors)
- `error.message`: Human-readable error message
- `error.code`: Machine-readable error code
### User-Facing Errors
Show errors to users:
```gdscript
# Toast notification (create a notification system)
NotificationManager.show_error("Failed to load characters")
# Inline error label
error_label.text = response.get_error_message()
error_label.visible = true
# Error dialog
var dialog = AcceptDialog.new()
dialog.dialog_text = "An error occurred. Please try again."
dialog.popup_centered()
```
## Platform Considerations
### Mobile
**Touch Input**:
- Larger tap targets (minimum 44x44 dp)
- No hover states
- Swipe gestures for navigation
**Screen Sizes**:
- Responsive layouts
- Safe areas (notches, rounded corners)
- Portrait and landscape orientations
**Performance**:
- Limit draw calls
- Use compressed textures
- Reduce particle effects
**Networking**:
- Handle intermittent connectivity
- Show loading indicators
- Cache data locally
### Desktop
**Input**:
- Keyboard shortcuts
- Mouse hover effects
- Scroll wheel support
**Window Management**:
- Resizable windows
- Fullscreen mode
- Multiple monitor support
### Web
**Limitations**:
- No threading (slower)
- No file system access (use IndexedDB)
- Larger download size
- CORS restrictions
**Requirements**:
- HTTPS for SharedArrayBuffer
- Proper MIME types
- COOP/COEP headers
## Security Considerations
### Token Storage
**Desktop/Mobile**: Tokens stored in encrypted local storage
**Web**: Tokens stored in localStorage (less secure, consider sessionStorage)
### HTTPS Only
Always use HTTPS in production for API calls.
### Token Expiration
Handle expired tokens:
```gdscript
func _on_api_error(response: HTTPClient.APIResponse) -> void:
if response.status == 401: # Unauthorized
# Token expired, logout
StateManager.clear_user_session()
NavigationManager.navigate_to_login()
```
### Input Validation
Always validate user input:
- FormField components have built-in validation
- Re-validate on backend (never trust client)
- Sanitize before displaying (prevent XSS if user-generated content)
## Testing Strategy
### Unit Tests
Test individual components:
- HTTPClient request/response handling
- StateManager save/load
- Data model serialization
- Component validation logic
### Integration Tests
Test scene flows:
- Login → Character List
- Character Creation wizard
- Character selection → Game
### Platform Tests
Test on each platform:
- Desktop: Windows, Mac, Linux
- Mobile: Android, iOS
- Web: Chrome, Firefox, Safari
### API Mocking
For offline testing, create mock responses:
```gdscript
# scripts/services/mock_http_client.gd
extends Node
# Same API as HTTPClient but returns mock data
func get(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void:
match endpoint:
"/api/v1/characters":
callback.call(_mock_character_list_response())
_:
error_callback.call(_mock_error_response())
func _mock_character_list_response() -> HTTPClient.APIResponse:
var mock_data = {
"app": "Code of Conquest",
"version": "0.1.0",
"status": 200,
"timestamp": Time.get_datetime_string_from_system(),
"result": [
{"id": "1", "name": "Warrior", "level": 5, "hp": 100, "max_hp": 100},
{"id": "2", "name": "Mage", "level": 3, "hp": 60, "max_hp": 60}
],
"error": {}
}
return HTTPClient.APIResponse.new(mock_data)
```
## Performance Optimization
### Minimize API Calls
- Cache data in StateManager
- Only refetch when necessary
- Use pagination for large lists
### Optimize Rendering
- Use object pooling for lists
- Minimize StyleBox allocations
- Use TextureRect instead of Sprite when possible
- Reduce shadow/glow effects on mobile
### Lazy Loading
Load scenes only when needed:
```gdscript
# Preload for instant transition
const CharacterList = preload("res://scenes/character/character_list.tscn")
# Or load on-demand
var scene = load("res://scenes/character/character_list.tscn")
```
## Debugging
### Enable Logging
```gdscript
# Add to HTTPClient, StateManager, etc.
print("[HTTPClient] GET /api/v1/characters")
print("[StateManager] User logged in: ", user_data.get("email"))
```
### Remote Debugging
For mobile:
1. Enable remote debug in Godot
2. Connect device via USB
3. Run with "Remote Debug" option
### Network Inspector
Monitor network traffic:
- Desktop: Use browser dev tools for Godot Web export
- Mobile: Use Charles Proxy or similar
- Check request/response bodies, headers, timing
## Next Steps (Phase 2)
After Phase 1 (Foundation) is complete:
1. **Implement Auth Screens** (Week 2)
- Login, Register, Password Reset
- Email verification
- Form validation
2. **Implement Character Management** (Weeks 3-4)
- Character creation wizard
- Character list
- Character detail
- Delete character
3. **Platform Optimization** (Week 5)
- Mobile touch controls
- Desktop keyboard shortcuts
- Web export configuration
4. **Future Features** (Week 6+)
- Combat UI
- World exploration
- Quest system
- Multiplayer
## Resources
- [Godot Documentation](https://docs.godotengine.org/)
- [GDScript Style Guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html)
- [Godot UI Tutorials](https://docs.godotengine.org/en/stable/tutorials/ui/index.html)
- [HTTP Request](https://docs.godotengine.org/en/stable/classes/class_httprequest.html)
- [FileAccess](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html)

413
godot_client/docs/EXPORT.md Normal file
View File

@@ -0,0 +1,413 @@
# Export Configuration Guide
## Overview
Code of Conquest targets multiple platforms:
- Desktop: Windows, macOS, Linux
- Mobile: Android, iOS
- Web: HTML5/WebAssembly
Each platform requires specific configuration in Godot's export system.
## Prerequisites
### 1. Install Export Templates
In Godot Editor:
1. Editor → Manage Export Templates
2. Click "Download and Install"
3. Wait for templates to download (~300-500 MB)
Alternatively, download manually from [Godot downloads](https://godotengine.org/download).
### 2. Platform-Specific Requirements
#### Windows
- **Template**: Included by default
- **Optional**: Custom icon (.ico file)
- **Optional**: Code signing certificate for production
#### macOS
- **Template**: Included by default
- **Requirements** (for distribution):
- macOS machine for final build
- Apple Developer account ($99/year)
- Code signing certificate
- Notarization for Gatekeeper
#### Linux
- **Template**: Included by default
- **Note**: Export on Linux for best results
- **Distribution**: AppImage, Flatpak, or raw binary
#### Android
- **Android SDK**: API level 33+ (Android 13)
- Download via Android Studio or command-line tools
- Set path in Godot: Editor → Editor Settings → Export → Android → SDK Path
- **Java JDK**: Version 17 or later
- **Keystore for Signing**:
```bash
keytool -genkey -v -keystore code_of_conquest.keystore \
-alias coc_key -keyalg RSA -keysize 2048 -validity 10000
```
- **Package Name**: `com.codeofconquest.game`
#### iOS
- **Requirements**:
- macOS with Xcode installed
- Apple Developer account ($99/year)
- Provisioning profile
- Code signing certificate
- **Bundle ID**: `com.codeofconquest.game`
- **Export Process**:
1. Export Xcode project from Godot
2. Open in Xcode
3. Configure signing
4. Build for device or App Store
#### Web (HTML5)
- **Template**: Included by default
- **Server Requirements**:
- HTTPS (for SharedArrayBuffer support)
- CORS headers for cross-origin resources
- Proper MIME types for .wasm files
- **PWA Configuration** (optional):
- Manifest file for installability
- Service worker for offline support
- Icons in multiple sizes
## Configuration Steps
### Step 1: Open Export Window
In Godot:
1. Project → Export
2. Click "Add..." to add a new preset
### Step 2: Configure Each Platform
#### Windows Export
1. Add → Windows Desktop
2. Configure:
- **Executable Name**: `CodeOfConquest.exe`
- **Export Path**: `builds/windows/CodeOfConquest.exe`
- **Icon**: `assets/ui/icon.ico` (optional)
- **Embed PCK**: `true` (for single-file distribution)
3. Features:
- 64-bit: Recommended for modern systems
- 32-bit: Optional for older systems
#### Linux Export
1. Add → Linux/X11
2. Configure:
- **Executable Name**: `CodeOfConquest.x86_64`
- **Export Path**: `builds/linux/CodeOfConquest.x86_64`
- **Embed PCK**: `true`
3. After export, set executable permissions:
```bash
chmod +x CodeOfConquest.x86_64
```
#### macOS Export
1. Add → macOS
2. Configure:
- **App Name**: `Code of Conquest`
- **Bundle ID**: `com.codeofconquest.game`
- **Export Path**: `builds/macos/CodeOfConquest.dmg`
- **Icon**: `assets/ui/icon.icns` (optional)
3. For distribution:
- Set code signing identity
- Enable hardened runtime
- Enable notarization
#### Android Export
1. Add → Android
2. Configure:
- **Package Name**: `com.codeofconquest.game`
- **Version Code**: `1` (increment for each release)
- **Version Name**: `0.1.0`
- **Min SDK**: `21` (Android 5.0)
- **Target SDK**: `33` (Android 13)
3. Keystore:
- **Debug Keystore**: Auto-generated (for testing)
- **Release Keystore**: Path to your `.keystore` file
- **Release User**: Keystore alias (e.g., `coc_key`)
- **Release Password**: Your keystore password
4. Permissions (add in export preset):
- `INTERNET` - Required for API calls
- `ACCESS_NETWORK_STATE` - Check connectivity
5. Screen Orientation:
- Landscape or Portrait (choose based on game design)
6. Graphics:
- Renderer: GL Compatibility (best mobile support)
7. Export:
- **APK**: For direct installation/testing
- **AAB** (Android App Bundle): For Google Play Store
#### iOS Export
1. Add → iOS
2. Configure:
- **Bundle ID**: `com.codeofconquest.game`
- **Version**: `0.1.0`
- **Icon**: Multiple sizes required (see Xcode)
- **Orientation**: Landscape or Portrait
3. Provisioning:
- **Team ID**: From Apple Developer account
- **Provisioning Profile**: Development or Distribution
4. Export:
- Export as Xcode project
- Open in Xcode for final build
5. Capabilities (configure in Xcode):
- Network requests
- Push notifications (if needed later)
#### Web Export
1. Add → HTML5
2. Configure:
- **Export Path**: `builds/web/index.html`
- **Export Type**: Regular or PWA
- **Head Include**: Custom HTML header (optional)
3. Features:
- **Thread Support**: Enable for better performance (requires COOP/COEP headers)
- **SharedArrayBuffer**: Requires HTTPS + specific headers
4. Server Configuration:
- Must serve with proper MIME types:
- `.wasm`: `application/wasm`
- `.pck`: `application/octet-stream`
5. CORS Headers (for threaded builds):
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
6. PWA Manifest (optional):
```json
{
"name": "Code of Conquest",
"short_name": "CoC",
"start_url": ".",
"display": "standalone",
"background_color": "#1a1a2e",
"theme_color": "#d4af37",
"icons": [...]
}
```
## Testing Exports
### Desktop Testing
1. Export to local directory
2. Run executable directly
3. Test with backend running on localhost
4. Verify:
- Window sizing and fullscreen
- Keyboard/mouse input
- API connectivity
- Save/load functionality
### Mobile Testing
#### Android
1. Enable USB debugging on device
2. Connect via USB
3. Use `adb install` to install APK:
```bash
adb install builds/android/CodeOfConquest.apk
```
4. Or export to device directly from Godot (Debug → Deploy)
5. Verify:
- Touch input responsiveness
- Screen orientation
- Battery usage
- Network connectivity
- Permissions granted
#### iOS
1. Configure development provisioning
2. Export Xcode project
3. Open in Xcode
4. Build to connected device
5. Verify same as Android
### Web Testing
1. Export to directory
2. Serve locally:
```bash
python3 -m http.server 8000
```
3. Open `http://localhost:8000` in browser
4. Verify:
- Loading performance
- Browser compatibility (Chrome, Firefox, Safari)
- WebGL rendering
- Network requests
- Mobile browser (responsive)
## Distribution
### Desktop
**Windows**:
- Distribute .exe directly
- Or create installer (e.g., Inno Setup, NSIS)
- Upload to Steam, itch.io, etc.
**macOS**:
- Create .dmg for distribution
- Notarize for Gatekeeper
- Upload to Mac App Store or direct download
**Linux**:
- Raw binary
- AppImage (self-contained)
- Flatpak (Flathub distribution)
- Snap package
### Mobile
**Android**:
- Google Play Store (requires AAB)
- Amazon Appstore
- F-Droid (open source)
- Direct APK download (sideloading)
**iOS**:
- App Store (requires App Store Connect)
- TestFlight (beta testing)
- Enterprise distribution (if applicable)
### Web
**Hosting Options**:
- Static hosting: Netlify, Vercel, GitHub Pages
- CDN: Cloudflare, AWS S3 + CloudFront
- Game platforms: itch.io (supports HTML5)
**Deployment**:
1. Export web build
2. Upload all files (index.html, .wasm, .pck, etc.)
3. Configure COOP/COEP headers if using threads
4. Test on production URL
## Performance Optimization
### General
- Use compressed textures (ETC2 for mobile, S3TC for desktop)
- Minimize draw calls
- Use LOD (Level of Detail) for 3D
- Profile with Godot profiler
### Mobile Specific
- Reduce resolution for lower-end devices
- Limit particle effects
- Use simpler shaders
- Test on low-end devices
- Monitor battery usage
### Web Specific
- Optimize asset sizes (compress images, audio)
- Use progressive loading for large assets
- Minimize WASM binary size
- Cache assets with service worker
## Platform-Specific Considerations
### Windows
- Antivirus may flag unsigned executables
- Consider code signing for production
- Test on Windows 10 and 11
### macOS
- Gatekeeper will block unsigned apps
- Notarization required for distribution
- Test on both Intel and Apple Silicon (M1/M2)
### Linux
- Provide both 32-bit and 64-bit builds
- Test on multiple distros (Ubuntu, Fedora, Arch)
- Include library dependencies or use static linking
### Android
- Target latest API level for Play Store
- Test on multiple screen sizes and densities
- Handle back button properly
- Support safe areas (notches, rounded corners)
### iOS
- Follow Apple Human Interface Guidelines
- Support safe areas
- Handle app lifecycle (background/foreground)
- Test on multiple devices (iPhone, iPad)
### Web
- Fallback for browsers without WebGL
- Loading screen for asset downloads
- Handle browser resize events
- Support both desktop and mobile browsers
## Troubleshooting
### "Missing export template"
→ Download templates via Editor → Manage Export Templates
### "Android SDK not found"
→ Set SDK path in Editor Settings → Export → Android
### "Code signing failed" (macOS/iOS)
→ Verify certificate and provisioning profile
### "Web build won't load"
→ Check browser console, verify MIME types, check CORS
### "App crashes on mobile"
→ Check device logs (adb logcat for Android, Xcode console for iOS)
## Resources
- [Godot Export Documentation](https://docs.godotengine.org/en/stable/tutorials/export/)
- [Android Publishing Guide](https://developer.android.com/studio/publish)
- [iOS App Distribution](https://developer.apple.com/documentation/xcode/distributing-your-app-for-beta-testing-and-releases)
- [PWA Documentation](https://web.dev/progressive-web-apps/)
## Automation (Future)
Consider automating builds with CI/CD:
- GitHub Actions
- GitLab CI
- Jenkins
Example workflow:
1. Commit to `main` branch
2. Trigger automated export for all platforms
3. Run automated tests
4. Upload builds to distribution channels
5. Tag release with version number

View File

@@ -0,0 +1,381 @@
# Getting Started with Godot Client
## Phase 1 Complete! ✓
The foundation for the Godot client is now in place. Here's what's been created:
### What's Built
**Project Structure**: Complete directory organization
**Core Services**: HTTPClient and StateManager singletons
**Theme System**: Color palette and styling utilities
**UI Components**: CustomButton, Card, FormField
**Documentation**: Architecture, themes, components, export
### Project Structure
```
godot_client/
├── project.godot # Main project file
├── README.md # Project overview
├── ARCHITECTURE.md # Architecture documentation
├── THEME_SETUP.md # Theme and font setup guide
├── EXPORT.md # Export configuration guide
├── GETTING_STARTED.md # This file
├── scenes/
│ ├── auth/ # Authentication screens (TODO)
│ ├── character/ # Character management (TODO)
│ ├── combat/ # Combat UI (TODO)
│ ├── world/ # World exploration (TODO)
│ └── components/ # Reusable UI components
│ └── README.md # Component documentation
├── scripts/
│ ├── services/
│ │ ├── http_client.gd # ✓ API communication service
│ │ └── state_manager.gd # ✓ Global state management
│ ├── models/ # Data models (TODO)
│ ├── utils/
│ │ └── theme_colors.gd # ✓ Color palette
│ └── components/
│ ├── custom_button.gd # ✓ Custom button component
│ ├── card.gd # ✓ Card container component
│ └── form_field.gd # ✓ Form input component
└── assets/
├── fonts/ # Fonts (TODO: download)
│ └── README.md # Font installation guide
├── themes/ # Theme resources (TODO: create in editor)
└── ui/ # UI assets (icons, etc.)
```
## Next Steps
### 1. Open Project in Godot
1. **Download Godot 4.5** from [godotengine.org](https://godotengine.org/)
2. **Launch Godot and import project**:
- Click "Import"
- Navigate to `godot_client/project.godot`
- Click "Import & Edit"
3. **Install export templates** (for exporting to platforms):
- Editor → Manage Export Templates
- Download and Install
### 2. Set Up Fonts
1. **Download fonts**:
- [Cinzel](https://fonts.google.com/specimen/Cinzel) - for headings
- [Lato](https://fonts.google.com/specimen/Lato) - for body text
2. **Copy to project**:
- Extract .ttf files
- Copy to `godot_client/assets/fonts/`
- Files needed:
- Cinzel-Regular.ttf, Cinzel-Medium.ttf, Cinzel-SemiBold.ttf, Cinzel-Bold.ttf
- Lato-Regular.ttf, Lato-Bold.ttf, Lato-Italic.ttf
3. **Reimport in Godot** (automatic, or click "Reimport" in FileSystem panel)
See `THEME_SETUP.md` for details.
### 3. Create Main Theme
1. **Create theme resource**:
- Right-click `assets/themes/` in Godot FileSystem
- New Resource → Theme
- Save as `main_theme.tres`
2. **Configure theme** (see `THEME_SETUP.md`):
- Set default font to Lato-Regular.ttf
- Configure button colors and styleboxes
- Configure panel styleboxes
- Configure input field styles
3. **Set project theme**:
- Project → Project Settings → GUI → Theme
- Set Custom Theme to `res://assets/themes/main_theme.tres`
### 4. Configure Backend URL
Edit `scripts/services/http_client.gd`:
```gdscript
# Line 16
const API_BASE_URL := "http://localhost:5000" # Change if needed
```
For production, you might use:
- `https://api.codeofconquest.com`
- Or make it configurable via settings
### 5. Create Main Scene
Create `scenes/main.tscn`:
1. **Scene → New Scene**
2. **Root node**: Control
3. **Add script**: `scripts/main.gd`
**main.gd**:
```gdscript
extends Control
func _ready() -> void:
print("Code of Conquest starting...")
# Check if authenticated
if StateManager.is_authenticated():
print("User authenticated, loading character list...")
# TODO: Navigate to character list
else:
print("Not authenticated, showing login...")
# TODO: Navigate to login
```
4. **Set as main scene**:
- Project → Project Settings → Application → Run
- Set Main Scene to `res://scenes/main.tscn`
### 6. Test Services
Create a test scene to verify services work:
**scenes/test_services.tscn**:
- Root: Control
- Add: Button ("Test API")
**test_services.gd**:
```gdscript
extends Control
@onready var test_button = $TestButton
func _ready():
test_button.pressed.connect(_on_test_clicked)
func _on_test_clicked():
print("Testing HTTPClient...")
# Test API call (replace with real endpoint)
# Note: Can use HTTPClient directly in _ready() or later
HTTPClient.http_get("/api/v1/health", _on_api_success, _on_api_error)
# Alternative (always works):
# get_node("/root/HTTPClient").http_get("/api/v1/health", _on_api_success, _on_api_error)
func _on_api_success(response):
print("API Success!")
print("Status: ", response.status)
print("Result: ", response.result)
func _on_api_error(response):
print("API Error!")
print("Error: ", response.get_error_message())
```
**Note**: If you get "Static function not found" errors, use `get_node("/root/HTTPClient")` instead of direct `HTTPClient` access.
Run this scene (F6) and click the button to test API connectivity.
## Phase 2: Authentication Screens
Once the foundation is set up, proceed with Phase 2 (Week 2):
### Auth Screens to Build
1. **Login Screen** (`scenes/auth/login.tscn`)
- Email and password fields (using FormField)
- Login button (using CustomButton)
- Links to register and forgot password
2. **Register Screen** (`scenes/auth/register.tscn`)
- Email, password, confirm password
- Validation
- Register button
3. **Forgot Password** (`scenes/auth/forgot_password.tscn`)
- Email field
- Send reset link button
4. **Reset Password** (`scenes/auth/reset_password.tscn`)
- New password and confirm
- Reset button
5. **Email Verification** (`scenes/auth/verify_email.tscn`)
- Verification status display
### Example: Login Screen
1. **Create scene**: `scenes/auth/login.tscn`
2. **Root**: Control node
3. **Add**:
- Card container (center of screen)
- FormField for email
- FormField for password
- CustomButton for login
- Label for errors
4. **Attach script**: `scenes/auth/login.gd`
See `ARCHITECTURE.md` for complete login implementation example.
## Helpful Resources
### Documentation Created
- **README.md** - Project overview, setup, and development guide
- **ARCHITECTURE.md** - Complete architecture documentation with examples
- **THEME_SETUP.md** - Theme creation and font setup
- **EXPORT.md** - Platform export configuration
- **scenes/components/README.md** - UI component guide
### External Resources
- [Godot Documentation](https://docs.godotengine.org/en/stable/)
- [GDScript Reference](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/index.html)
- [Godot UI Tutorials](https://docs.godotengine.org/en/stable/tutorials/ui/index.html)
## Common Issues
### "Autoload not found" error
**Problem**: HTTPClient or StateManager not found
**Solution**: Check `project.godot` has these lines in `[autoload]` section:
```
HTTPClient="*res://scripts/services/http_client.gd"
StateManager="*res://scripts/services/state_manager.gd"
```
### "Class ThemeColors not found"
**Problem**: ThemeColors class not recognized
**Solution**:
1. Check file exists at `scripts/utils/theme_colors.gd`
2. Restart Godot (to refresh script cache)
3. Ensure `class_name ThemeColors` is at top of file
### API calls failing
**Problem**: HTTPClient can't connect to Flask backend
**Solution**:
1. Ensure Flask backend is running on `localhost:5000`
2. Check `API_BASE_URL` in `http_client.gd`
3. Check Flask CORS settings if running on different port/domain
### Fonts not showing
**Problem**: Custom fonts not displaying
**Solution**:
1. Ensure .ttf files are in `assets/fonts/`
2. Check font imports (click font in FileSystem)
3. Verify theme is set in Project Settings
## Development Workflow
### Typical Development Session
1. **Start Flask backend**:
```bash
cd /home/ptarrant/repos/coc
source venv/bin/activate
python -m flask run
```
2. **Open Godot project**:
- Launch Godot
- Open `godot_client`
3. **Work on scene**:
- Edit scene in Godot editor
- Attach/edit scripts
- Test with F6 (run scene) or F5 (run project)
4. **Debug**:
- Check Output panel for print statements
- Check Debugger panel for errors
- Use Godot debugger (breakpoints, step through)
5. **Test on device**:
- Export to platform (see `EXPORT.md`)
- Test on real device
### Git Workflow
```bash
# Create feature branch
git checkout -b feature/login-screen
# Work on feature
# (edit files in Godot)
# Commit changes
git add godot_client/
git commit -m "feat: implement login screen UI"
# Merge to dev
git checkout dev
git merge feature/login-screen
# When ready for production
git checkout master
git merge dev
```
## Testing
### Manual Testing Checklist
Before each release:
- [ ] Login/logout works
- [ ] Character creation works
- [ ] Character list displays correctly
- [ ] API errors are handled gracefully
- [ ] Network errors show user-friendly messages
- [ ] State persists after app restart
- [ ] Works on Windows
- [ ] Works on macOS
- [ ] Works on Linux
- [ ] Works on Android
- [ ] Works on iOS
- [ ] Works in web browser
### Performance Checklist
- [ ] App starts in < 3 seconds
- [ ] API calls complete in < 5 seconds
- [ ] UI is responsive (no lag)
- [ ] Memory usage is reasonable
- [ ] Battery usage is acceptable (mobile)
## Estimated Timeline
- **Phase 1: Foundation** - ✅ Complete
- **Phase 2: Authentication** (Week 2) - Ready to start
- **Phase 3: Character Management** (Weeks 3-4) - Documented
- **Phase 4: Platform Optimization** (Week 5) - Planned
- **Phase 5: Future Features** (Week 6+) - Ongoing
With full-time focus, you should have a working app with auth and character management in 4-5 weeks.
## Questions?
- Check documentation in this directory
- Read Godot documentation
- Check Flask backend API documentation in `/docs`
## Ready to Start?
1. ✅ Open project in Godot
2. ✅ Download and install fonts
3. ✅ Create main theme
4. ✅ Configure backend URL
5. ✅ Create main scene
6. ✅ Test services
7. 🚀 Start building auth screens!
Good luck with the migration! The foundation is solid, and you're ready to build out the UI.

View File

@@ -0,0 +1,823 @@
# Multiplayer System - Godot Client
**Status:** Planned
**Phase:** 6 (Multiplayer Sessions)
**Last Updated:** November 18, 2025
---
## Overview
The Godot Client provides a rich, native multiplayer experience with realtime synchronization, interactive combat UI, and smooth animations for multiplayer sessions.
**Client Responsibilities:**
- Render multiplayer session creation UI
- Display lobby with player list and ready status
- Show active session UI (timer, party status, combat animations)
- Handle realtime updates via WebSocket (Appwrite Realtime or custom)
- Submit player actions to API backend via HTTP
- Display session completion and rewards with animations
**Technical Stack:**
- **Engine**: Godot 4.5
- **Networking**: HTTP requests to API + WebSocket for realtime
- **UI**: Godot Control nodes (responsive layouts)
- **Animations**: AnimationPlayer, Tweens
---
## Scene Structure
### Multiplayer Scenes Hierarchy
```
scenes/multiplayer/
├── MultiplayerMenu.tscn # Entry point (create/join session)
├── SessionCreate.tscn # Session creation form
├── Lobby.tscn # Lobby screen with party list
├── ActiveSession.tscn # Main session scene
│ ├── Timer.tscn # Session timer component
│ ├── PartyStatus.tscn # Party member HP/status display
│ ├── NarrativePanel.tscn # Narrative/conversation display
│ └── CombatUI.tscn # Combat interface
│ ├── TurnOrder.tscn # Turn order display
│ ├── ActionButtons.tscn # Combat action buttons
│ └── TargetSelector.tscn # Enemy target selection
└── SessionComplete.tscn # Completion/rewards screen
```
### Component Scenes
```
scenes/components/multiplayer/
├── PartyMemberCard.tscn # Single party member display
├── InviteLinkCopy.tscn # Invite link copy widget
├── ReadyToggle.tscn # Ready status toggle button
├── CombatantCard.tscn # Combat turn order entry
└── RewardDisplay.tscn # Reward summary component
```
---
## GDScript Implementation
### Multiplayer Manager (Singleton)
**Script:** `scripts/singletons/MultiplayerManager.gd`
```gdscript
extends Node
# Signals for multiplayer events
signal session_created(session_id: String)
signal player_joined(member: Dictionary)
signal player_ready_changed(user_id: String, is_ready: bool)
signal session_started()
signal combat_started(encounter: Dictionary)
signal turn_changed(current_turn: int)
signal session_completed(rewards: Dictionary)
signal session_expired()
# Current session data
var current_session: Dictionary = {}
var is_host: bool = false
var my_character_id: String = ""
# API configuration
var api_base_url: String = "http://localhost:5000"
# WebSocket for realtime updates
var ws_client: WebSocketPeer
var is_connected: bool = false
func _ready():
# Initialize WebSocket
ws_client = WebSocketPeer.new()
func create_session(max_players: int, difficulty: String) -> void:
"""Create a new multiplayer session."""
var body = {
"max_players": max_players,
"difficulty": difficulty,
"tier_required": "premium"
}
var response = await APIClient.post("/api/v1/sessions/multiplayer/create", body)
if response.status == 200:
current_session = response.result
is_host = true
emit_signal("session_created", current_session.session_id)
# Connect to realtime updates
connect_realtime(current_session.session_id)
else:
push_error("Failed to create session: " + str(response.error))
func join_session(invite_code: String, character_id: String) -> void:
"""Join a multiplayer session via invite code."""
# First, get session info
var info_response = await APIClient.get("/api/v1/sessions/multiplayer/join/" + invite_code)
if info_response.status != 200:
push_error("Invalid invite code")
return
# Join with selected character
var join_body = {"character_id": character_id}
var join_response = await APIClient.post("/api/v1/sessions/multiplayer/join/" + invite_code, join_body)
if join_response.status == 200:
current_session = join_response.result
my_character_id = character_id
is_host = false
# Connect to realtime updates
connect_realtime(current_session.session_id)
emit_signal("player_joined", {"character_id": character_id})
else:
push_error("Failed to join session: " + str(join_response.error))
func connect_realtime(session_id: String) -> void:
"""Connect to Appwrite Realtime for session updates."""
# Use Appwrite SDK or custom WebSocket
# For Appwrite Realtime, use their JavaScript SDK bridged via JavaScriptBridge
# Or implement custom WebSocket to backend realtime endpoint
var ws_url = "wss://cloud.appwrite.io/v1/realtime?project=" + Config.appwrite_project_id
var channels = ["databases." + Config.appwrite_database_id + ".collections.multiplayer_sessions.documents." + session_id]
# Connect WebSocket
var err = ws_client.connect_to_url(ws_url)
if err != OK:
push_error("Failed to connect WebSocket: " + str(err))
return
is_connected = true
func _process(_delta):
"""Process WebSocket messages."""
if not is_connected:
return
ws_client.poll()
var state = ws_client.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
while ws_client.get_available_packet_count():
var packet = ws_client.get_packet()
var message = packet.get_string_from_utf8()
handle_realtime_message(JSON.parse_string(message))
elif state == WebSocketPeer.STATE_CLOSED:
is_connected = false
push_warning("WebSocket disconnected")
func handle_realtime_message(data: Dictionary) -> void:
"""Handle realtime event from backend."""
var event_type = data.get("events", [])[0] if data.has("events") else ""
var payload = data.get("payload", {})
match event_type:
"databases.*.collections.*.documents.*.update":
# Session updated
current_session = payload
# Check for status changes
if payload.status == "active":
emit_signal("session_started")
elif payload.status == "completed":
emit_signal("session_completed", payload.campaign.rewards)
elif payload.status == "expired":
emit_signal("session_expired")
# Check for combat updates
if payload.has("combat_encounter") and payload.combat_encounter != null:
emit_signal("combat_started", payload.combat_encounter)
emit_signal("turn_changed", payload.combat_encounter.current_turn_index)
func toggle_ready() -> void:
"""Toggle ready status in lobby."""
var response = await APIClient.post("/api/v1/sessions/multiplayer/" + current_session.session_id + "/ready", {})
if response.status != 200:
push_error("Failed to toggle ready status")
func start_session() -> void:
"""Start the session (host only)."""
if not is_host:
push_error("Only host can start session")
return
var response = await APIClient.post("/api/v1/sessions/multiplayer/" + current_session.session_id + "/start", {})
if response.status != 200:
push_error("Failed to start session: " + str(response.error))
func take_combat_action(action_type: String, ability_id: String = "", target_id: String = "") -> void:
"""Submit combat action to backend."""
var body = {
"action_type": action_type,
"ability_id": ability_id,
"target_id": target_id
}
var response = await APIClient.post("/api/v1/sessions/multiplayer/" + current_session.session_id + "/combat/action", body)
if response.status != 200:
push_error("Failed to take action: " + str(response.error))
func leave_session() -> void:
"""Leave the current session."""
var response = await APIClient.post("/api/v1/sessions/multiplayer/" + current_session.session_id + "/leave", {})
# Disconnect WebSocket
if is_connected:
ws_client.close()
is_connected = false
current_session = {}
is_host = false
```
**Usage:**
```gdscript
# Autoload as singleton: Project Settings > Autoload > MultiplayerManager
```
---
## Scene Implementation
### Session Create Scene
**Scene:** `scenes/multiplayer/SessionCreate.tscn`
**Script:** `scripts/multiplayer/session_create.gd`
```gdscript
extends Control
@onready var party_size_group: ButtonGroup = $VBoxContainer/PartySize/ButtonGroup
@onready var difficulty_group: ButtonGroup = $VBoxContainer/Difficulty/ButtonGroup
@onready var create_button: Button = $VBoxContainer/CreateButton
func _ready():
create_button.pressed.connect(_on_create_pressed)
func _on_create_pressed():
var max_players = int(party_size_group.get_pressed_button().text.split(" ")[0])
var difficulty = difficulty_group.get_pressed_button().text.to_lower()
# Disable button to prevent double-click
create_button.disabled = true
# Create session via MultiplayerManager
await MultiplayerManager.create_session(max_players, difficulty)
# Navigate to lobby
get_tree().change_scene_to_file("res://scenes/multiplayer/Lobby.tscn")
```
**UI Layout:**
```
VBoxContainer
├── Label "Create Multiplayer Session"
├── HBoxContainer (Party Size)
│ ├── RadioButton "2 Players"
│ ├── RadioButton "3 Players"
│ └── RadioButton "4 Players"
├── HBoxContainer (Difficulty)
│ ├── RadioButton "Easy"
│ ├── RadioButton "Medium"
│ ├── RadioButton "Hard"
│ └── RadioButton "Deadly"
└── Button "Create Session"
```
### Lobby Scene
**Scene:** `scenes/multiplayer/Lobby.tscn`
**Script:** `scripts/multiplayer/lobby.gd`
```gdscript
extends Control
@onready var invite_code_label: Label = $VBoxContainer/InviteSection/CodeLabel
@onready var invite_link_input: LineEdit = $VBoxContainer/InviteSection/LinkInput
@onready var copy_button: Button = $VBoxContainer/InviteSection/CopyButton
@onready var party_list: VBoxContainer = $VBoxContainer/PartyList
@onready var ready_button: Button = $VBoxContainer/Actions/ReadyButton
@onready var start_button: Button = $VBoxContainer/Actions/StartButton
@onready var leave_button: Button = $VBoxContainer/Actions/LeaveButton
# Party member card scene
var party_member_card_scene = preload("res://scenes/components/multiplayer/PartyMemberCard.tscn")
func _ready():
# Connect signals
MultiplayerManager.player_joined.connect(_on_player_joined)
MultiplayerManager.player_ready_changed.connect(_on_player_ready_changed)
MultiplayerManager.session_started.connect(_on_session_started)
# Setup UI
var session = MultiplayerManager.current_session
invite_code_label.text = "Session Code: " + session.invite_code
invite_link_input.text = "https://codeofconquest.com/join/" + session.invite_code
copy_button.pressed.connect(_on_copy_pressed)
ready_button.pressed.connect(_on_ready_pressed)
start_button.pressed.connect(_on_start_pressed)
leave_button.pressed.connect(_on_leave_pressed)
# Show/hide start button based on host status
start_button.visible = MultiplayerManager.is_host
ready_button.visible = !MultiplayerManager.is_host
# Populate party list
refresh_party_list()
func refresh_party_list():
"""Refresh the party member list."""
# Clear existing cards
for child in party_list.get_children():
child.queue_free()
var session = MultiplayerManager.current_session
var party_members = session.get("party_members", [])
# Add party member cards
for member in party_members:
var card = party_member_card_scene.instantiate()
card.set_member_data(member)
party_list.add_child(card)
# Add empty slots
var max_players = session.get("max_players", 4)
for i in range(max_players - party_members.size()):
var card = party_member_card_scene.instantiate()
card.set_empty_slot()
party_list.add_child(card)
# Check if all ready
check_all_ready()
func check_all_ready():
"""Check if all players are ready and enable/disable start button."""
if not MultiplayerManager.is_host:
return
var session = MultiplayerManager.current_session
var party_members = session.get("party_members", [])
var all_ready = true
for member in party_members:
if not member.is_ready:
all_ready = false
break
start_button.disabled = !all_ready
func _on_player_joined(member: Dictionary):
refresh_party_list()
func _on_player_ready_changed(user_id: String, is_ready: bool):
refresh_party_list()
func _on_session_started():
# Navigate to active session
get_tree().change_scene_to_file("res://scenes/multiplayer/ActiveSession.tscn")
func _on_copy_pressed():
DisplayServer.clipboard_set(invite_link_input.text)
# Show toast notification
show_toast("Invite link copied!")
func _on_ready_pressed():
await MultiplayerManager.toggle_ready()
func _on_start_pressed():
start_button.disabled = true
await MultiplayerManager.start_session()
func _on_leave_pressed():
await MultiplayerManager.leave_session()
get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")
func show_toast(message: String):
# Implement toast notification (simple Label with animation)
var toast = Label.new()
toast.text = message
toast.position = Vector2(400, 50)
add_child(toast)
# Fade out animation
var tween = create_tween()
tween.tween_property(toast, "modulate:a", 0.0, 2.0)
tween.tween_callback(toast.queue_free)
```
### Party Member Card Component
**Scene:** `scenes/components/multiplayer/PartyMemberCard.tscn`
**Script:** `scripts/components/party_member_card.gd`
```gdscript
extends PanelContainer
@onready var crown_icon: TextureRect = $HBoxContainer/CrownIcon
@onready var username_label: Label = $HBoxContainer/UsernameLabel
@onready var character_label: Label = $HBoxContainer/CharacterLabel
@onready var ready_status: Label = $HBoxContainer/ReadyStatus
func set_member_data(member: Dictionary):
"""Populate card with member data."""
crown_icon.visible = member.is_host
username_label.text = member.username + (" (Host)" if member.is_host else "")
character_label.text = member.character_snapshot.name + " - " + member.character_snapshot.player_class.name + " Lvl " + str(member.character_snapshot.level)
if member.is_ready:
ready_status.text = "✅ Ready"
ready_status.modulate = Color.GREEN
else:
ready_status.text = "⏳ Not Ready"
ready_status.modulate = Color.YELLOW
func set_empty_slot():
"""Display as empty slot."""
crown_icon.visible = false
username_label.text = "[Waiting for player...]"
character_label.text = ""
ready_status.text = ""
```
### Active Session Scene
**Scene:** `scenes/multiplayer/ActiveSession.tscn`
**Script:** `scripts/multiplayer/active_session.gd`
```gdscript
extends Control
@onready var campaign_title: Label = $Header/TitleLabel
@onready var timer_label: Label = $Header/TimerLabel
@onready var progress_bar: ProgressBar = $ProgressSection/ProgressBar
@onready var party_status: HBoxContainer = $PartyStatus
@onready var narrative_panel: RichTextLabel = $NarrativePanel/TextLabel
@onready var combat_ui: Control = $CombatUI
var time_remaining: int = 7200 # 2 hours in seconds
func _ready():
# Connect signals
MultiplayerManager.combat_started.connect(_on_combat_started)
MultiplayerManager.turn_changed.connect(_on_turn_changed)
MultiplayerManager.session_completed.connect(_on_session_completed)
MultiplayerManager.session_expired.connect(_on_session_expired)
# Setup UI
var session = MultiplayerManager.current_session
campaign_title.text = session.campaign.title
time_remaining = session.time_remaining_seconds
update_progress_bar()
update_party_status()
update_narrative()
# Start timer countdown
var timer = Timer.new()
timer.wait_time = 1.0
timer.timeout.connect(_on_timer_tick)
add_child(timer)
timer.start()
func _on_timer_tick():
"""Update timer every second."""
time_remaining -= 1
if time_remaining <= 0:
timer_label.text = "⏱️ Time's Up!"
return
var hours = time_remaining / 3600
var minutes = (time_remaining % 3600) / 60
var seconds = time_remaining % 60
timer_label.text = "⏱️ %d:%02d:%02d Remaining" % [hours, minutes, seconds]
# Warnings
if time_remaining == 600:
show_warning("⚠️ 10 minutes remaining!")
elif time_remaining == 300:
show_warning("⚠️ 5 minutes remaining!")
elif time_remaining == 60:
show_warning("🚨 1 minute remaining!")
func update_progress_bar():
var session = MultiplayerManager.current_session
var total_encounters = session.campaign.encounters.size()
var current = session.current_encounter_index
progress_bar.max_value = total_encounters
progress_bar.value = current
func update_party_status():
# Clear existing party status
for child in party_status.get_children():
child.queue_free()
var session = MultiplayerManager.current_session
for member in session.party_members:
var status_label = Label.new()
var char = member.character_snapshot
status_label.text = "%s (HP: %d/%d)" % [char.name, char.current_hp, char.max_hp]
if not member.is_connected:
status_label.text += " ⚠️ Disconnected"
party_status.add_child(status_label)
func update_narrative():
var session = MultiplayerManager.current_session
narrative_panel.clear()
for entry in session.conversation_history:
narrative_panel.append_text("[b]%s:[/b] %s\n\n" % [entry.role.capitalize(), entry.content])
func _on_combat_started(encounter: Dictionary):
combat_ui.visible = true
combat_ui.setup_combat(encounter)
func _on_turn_changed(current_turn: int):
combat_ui.update_turn_order(current_turn)
func _on_session_completed(rewards: Dictionary):
get_tree().change_scene_to_file("res://scenes/multiplayer/SessionComplete.tscn")
func _on_session_expired():
show_warning("Session time limit reached. Redirecting...")
await get_tree().create_timer(3.0).timeout
get_tree().change_scene_to_file("res://scenes/multiplayer/SessionComplete.tscn")
func show_warning(message: String):
# Display warning notification (AcceptDialog or custom popup)
var dialog = AcceptDialog.new()
dialog.dialog_text = message
add_child(dialog)
dialog.popup_centered()
```
### Combat UI Component
**Scene:** `scenes/multiplayer/CombatUI.tscn`
**Script:** `scripts/multiplayer/combat_ui.gd`
```gdscript
extends Control
@onready var turn_order_list: VBoxContainer = $TurnOrder/List
@onready var action_buttons: HBoxContainer = $Actions/Buttons
@onready var attack_button: Button = $Actions/Buttons/AttackButton
@onready var ability_dropdown: OptionButton = $Actions/Buttons/AbilityDropdown
@onready var defend_button: Button = $Actions/Buttons/DefendButton
var is_my_turn: bool = false
var current_encounter: Dictionary = {}
func setup_combat(encounter: Dictionary):
"""Setup combat UI for new encounter."""
current_encounter = encounter
populate_turn_order(encounter)
setup_action_buttons()
func populate_turn_order(encounter: Dictionary):
"""Display turn order list."""
# Clear existing
for child in turn_order_list.get_children():
child.queue_free()
var turn_order = encounter.turn_order
var current_turn = encounter.current_turn_index
for i in range(turn_order.size()):
var combatant_id = turn_order[i]
var label = Label.new()
label.text = get_combatant_name(combatant_id)
# Highlight current turn
if i == current_turn:
label.modulate = Color.YELLOW
label.text = "" + label.text
turn_order_list.add_child(label)
# Check if it's my turn
check_if_my_turn(current_turn, turn_order)
func update_turn_order(current_turn: int):
"""Update turn order highlighting."""
var labels = turn_order_list.get_children()
for i in range(labels.size()):
var label = labels[i]
if i == current_turn:
label.modulate = Color.YELLOW
label.text = "" + get_combatant_name(current_encounter.turn_order[i])
else:
label.modulate = Color.WHITE
label.text = get_combatant_name(current_encounter.turn_order[i])
# Check if it's my turn
check_if_my_turn(current_turn, current_encounter.turn_order)
func check_if_my_turn(current_turn: int, turn_order: Array):
"""Check if current combatant is controlled by this player."""
var current_combatant_id = turn_order[current_turn]
# Check if this combatant belongs to me
is_my_turn = is_my_combatant(current_combatant_id)
# Enable/disable action buttons
action_buttons.visible = is_my_turn
func is_my_combatant(combatant_id: String) -> bool:
"""Check if combatant belongs to current player."""
# Logic to determine if this combatant_id matches my character
return combatant_id == MultiplayerManager.my_character_id
func setup_action_buttons():
"""Setup combat action button handlers."""
attack_button.pressed.connect(_on_attack_pressed)
defend_button.pressed.connect(_on_defend_pressed)
# Populate ability dropdown
# (Load character abilities from MultiplayerManager.current_session)
func _on_attack_pressed():
# Show target selection
show_target_selection("attack")
func _on_defend_pressed():
await MultiplayerManager.take_combat_action("defend")
func show_target_selection(action_type: String):
"""Show enemy target selection UI."""
# Display list of enemies, allow player to select target
# For simplicity, just take first enemy
var enemies = get_enemies_from_encounter()
if enemies.size() > 0:
await MultiplayerManager.take_combat_action(action_type, "", enemies[0].combatant_id)
func get_combatant_name(combatant_id: String) -> String:
# Lookup combatant name from encounter data
return combatant_id # Placeholder
func get_enemies_from_encounter() -> Array:
# Extract enemy combatants from encounter
return [] # Placeholder
```
---
## WebSocket Integration
### Appwrite Realtime via GDScript
Godot doesn't natively support Appwrite SDK, so we'll use WebSocketPeer:
**Custom WebSocket Client:**
```gdscript
# In MultiplayerManager.gd
func connect_realtime(session_id: String) -> void:
var project_id = Config.appwrite_project_id
var db_id = Config.appwrite_database_id
# Construct Appwrite Realtime WebSocket URL
var ws_url = "wss://cloud.appwrite.io/v1/realtime?project=" + project_id
ws_client = WebSocketPeer.new()
var err = ws_client.connect_to_url(ws_url)
if err != OK:
push_error("WebSocket connection failed: " + str(err))
return
is_connected = true
# After connection, subscribe to channels
await get_tree().create_timer(1.0).timeout # Wait for connection
subscribe_to_channels(session_id)
func subscribe_to_channels(session_id: String):
"""Send subscribe message to Appwrite Realtime."""
var subscribe_message = {
"type": "subscribe",
"channels": [
"databases." + Config.appwrite_database_id + ".collections.multiplayer_sessions.documents." + session_id
]
}
ws_client.send_text(JSON.stringify(subscribe_message))
```
**Alternative:** Use HTTP polling as fallback if WebSocket fails.
---
## Testing Checklist
### Manual Testing Tasks (Godot Client)
- [ ] Session creation UI works correctly
- [ ] Invite link copies to clipboard
- [ ] Lobby updates when players join
- [ ] Ready status toggle works
- [ ] Host can start session when all ready
- [ ] Timer displays and counts down correctly
- [ ] Party HP updates during combat
- [ ] Combat action buttons submit correctly
- [ ] Turn order highlights current player
- [ ] Realtime updates work (test with web client simultaneously)
- [ ] Session completion screen displays rewards
- [ ] Disconnection handling shows warnings
- [ ] UI is responsive on different screen sizes
- [ ] Animations play smoothly
---
## Animation Guidelines
### Combat Animations
**Attack Animation:**
```gdscript
func play_attack_animation(attacker_id: String, target_id: String):
var attacker_sprite = get_combatant_sprite(attacker_id)
var target_sprite = get_combatant_sprite(target_id)
var tween = create_tween()
# Move attacker forward
tween.tween_property(attacker_sprite, "position:x", target_sprite.position.x - 50, 0.3)
# Flash target red (damage)
tween.parallel().tween_property(target_sprite, "modulate", Color.RED, 0.1)
tween.tween_property(target_sprite, "modulate", Color.WHITE, 0.1)
# Move attacker back
tween.tween_property(attacker_sprite, "position:x", attacker_sprite.position.x, 0.3)
```
**Damage Number Popup:**
```gdscript
func show_damage_number(damage: int, position: Vector2):
var label = Label.new()
label.text = "-" + str(damage)
label.position = position
label.modulate = Color.RED
add_child(label)
var tween = create_tween()
tween.tween_property(label, "position:y", position.y - 50, 1.0)
tween.parallel().tween_property(label, "modulate:a", 0.0, 1.0)
tween.tween_callback(label.queue_free)
```
---
## Related Documentation
- **[/api/docs/MULTIPLAYER.md](../../api/docs/MULTIPLAYER.md)** - Backend API endpoints and business logic
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Godot client architecture
- **[GETTING_STARTED.md](GETTING_STARTED.md)** - Setup guide
---
**Document Version:** 1.0 (Microservices Split)
**Created:** November 18, 2025
**Last Updated:** November 18, 2025

103
godot_client/docs/README.md Normal file
View File

@@ -0,0 +1,103 @@
# Scene Documentation
This directory contains specifications for building Godot scenes.
## Workflow
### 1. Read the Scene Spec
Each scene has a detailed specification document in `scenes/` that includes:
- Visual mockup
- Complete node hierarchy
- Exact node names (important!)
- Step-by-step build instructions
- Colors and styling
- What the script will handle
### 2. Build in Godot Editor
Follow the spec to create the scene visually:
- Add nodes in the specified hierarchy
- Name them **exactly** as documented
- Set properties as specified
- Apply colors and styling
### 3. Save the Scene
Save to the location specified in the doc (e.g., `scenes/auth/login.tscn`)
### 4. Notify
Let Claude know the scene is ready
### 5. Script Creation
Claude will:
- Create the `.gd` script file
- Add all the logic and API integration
- Attach it to your scene
## Available Specs
### Scenes
- [Login Screen](scenes/login_screen.md) - User authentication
### Components
- CustomButton - See `scripts/components/custom_button.gd`
- Card - See `scripts/components/card.gd`
- FormField - See `scripts/components/form_field.gd`
## Color Reference
Quick reference for the theme colors:
```
Backgrounds:
Primary: #1a1a2e (very dark blue-gray)
Secondary: #16213e (slightly lighter)
Card: #1e1e2f (card backgrounds)
Text:
Primary: #e4e4e7 (light gray)
Secondary: #a1a1aa (medium gray)
Disabled: #71717a (dark gray)
Accent:
Gold: #d4af37 (primary)
Gold Light: #f4d03f (hover)
Gold Dark: #b8930a (pressed)
Status:
Error: #ef4444 (red)
Success: #10b981 (green)
Warning: #f59e0b (orange)
Borders:
Default: #3f3f46
Accent: #d4af37 (gold)
```
## Tips
### Node Naming
- Use exact names from the spec (case-sensitive!)
- Names must match so the script can find them
- Example: `EmailInput` not `email_input`
### Styling
- Use StyleBoxFlat for custom backgrounds
- Set theme overrides in Inspector
- Colors are specified as hex codes
### Layout
- Use Container nodes for automatic layout
- Set Custom Minimum Size for spacing/sizing
- Use Control nodes for spacers
### Testing
- Run the scene (F6) to preview
- Check for errors in debugger
- Verify all nodes are named correctly
## Questions?
Check the spec document for details. Each spec includes:
- What nodes to create
- What properties to set
- Visual reference
- Step-by-step instructions

View File

@@ -0,0 +1,361 @@
# Theme Setup Guide
This guide explains how to set up the Code of Conquest theme in Godot, including fonts, colors, and UI styling.
## Overview
The theme recreates the web UI's RPG/fantasy aesthetic:
- **Color Palette**: Dark slate gray backgrounds with gold accents
- **Fonts**: Cinzel (headings), Lato (body text)
- **Style**: Medieval/fantasy with ornate borders and rich colors
## Color Palette
All colors are defined in `scripts/utils/theme_colors.gd`:
### Backgrounds
- Primary: `#1a1a2e` - Very dark blue-gray
- Secondary: `#16213e` - Slightly lighter
- Card: `#1e1e2f` - Card backgrounds
### Text
- Primary: `#e4e4e7` - Light gray (main text)
- Secondary: `#a1a1aa` - Medium gray (secondary text)
- Disabled: `#71717a` - Dark gray (disabled)
### Accents
- Gold: `#d4af37` - Primary accent color
- Gold Light: `#f4d03f`
- Gold Dark: `#b8930a`
### Status
- Success: `#10b981` - Green
- Error: `#ef4444` - Red
- Warning: `#f59e0b` - Orange
- Info: `#3b82f6` - Blue
## Font Setup
### 1. Download Fonts
**Cinzel** (for headings):
- Download from [Google Fonts](https://fonts.google.com/specimen/Cinzel)
- Download all weights (Regular, Medium, SemiBold, Bold)
**Lato** (for body text):
- Download from [Google Fonts](https://fonts.google.com/specimen/Lato)
- Download Regular, Bold, and Italic
### 2. Install Fonts in Project
1. Extract downloaded font files (.ttf or .otf)
2. Copy to `godot_client/assets/fonts/`:
```
assets/fonts/
├── Cinzel-Regular.ttf
├── Cinzel-Medium.ttf
├── Cinzel-SemiBold.ttf
├── Cinzel-Bold.ttf
├── Lato-Regular.ttf
├── Lato-Bold.ttf
└── Lato-Italic.ttf
```
3. In Godot, the fonts will auto-import
4. Check import settings (select font → Import tab):
- Antialiasing: Enabled
- Hinting: Full
- Subpixel Positioning: Auto
### 3. Create Font Resources
For each font file:
1. Right-click font in FileSystem
2. Select "New Resource"
3. Choose "FontVariation" (or use font directly)
4. Configure size and other properties
5. Save as `.tres` resource
Example font resources to create:
- `Cinzel_Heading_Large.tres` (Cinzel-Bold, 32px)
- `Cinzel_Heading_Medium.tres` (Cinzel-SemiBold, 24px)
- `Cinzel_Heading_Small.tres` (Cinzel-Medium, 18px)
- `Lato_Body.tres` (Lato-Regular, 14px)
- `Lato_Body_Bold.tres` (Lato-Bold, 14px)
## Theme Resource Setup
### 1. Create Main Theme
1. In Godot: Right-click `assets/themes/` → New Resource
2. Select "Theme"
3. Save as `main_theme.tres`
### 2. Configure Default Font
1. Select `main_theme.tres`
2. In Inspector → Theme → Default Font:
- Set to `Lato-Regular.ttf`
3. Default Font Size: `14`
### 3. Configure Colors
For each Control type (Button, Label, LineEdit, etc.):
1. In Theme editor, expand control type
2. Add color overrides:
**Button**:
- `font_color`: `#e4e4e7` (TEXT_PRIMARY)
- `font_hover_color`: `#f4d03f` (GOLD_LIGHT)
- `font_pressed_color`: `#d4af37` (GOLD_ACCENT)
- `font_disabled_color`: `#71717a` (TEXT_DISABLED)
**Label**:
- `font_color`: `#e4e4e7` (TEXT_PRIMARY)
**LineEdit**:
- `font_color`: `#e4e4e7` (TEXT_PRIMARY)
- `font_placeholder_color`: `#a1a1aa` (TEXT_SECONDARY)
- `caret_color`: `#d4af37` (GOLD_ACCENT)
- `selection_color`: `#d4af37` (GOLD_ACCENT with alpha)
### 4. Configure StyleBoxes
Create StyleBoxFlat resources for backgrounds and borders:
**Button Normal**:
1. Create New StyleBoxFlat
2. Settings:
- Background Color: `#1e1e2f` (BACKGROUND_CARD)
- Border Width: `2` (all sides)
- Border Color: `#3f3f46` (BORDER_DEFAULT)
- Corner Radius: `4` (all corners)
**Button Hover**:
- Background Color: `#16213e` (BACKGROUND_SECONDARY)
- Border Color: `#d4af37` (GOLD_ACCENT)
**Button Pressed**:
- Background Color: `#0f3460` (BACKGROUND_TERTIARY)
- Border Color: `#d4af37` (GOLD_ACCENT)
**Button Disabled**:
- Background Color: `#1a1a2e` (BACKGROUND_PRIMARY)
- Border Color: `#27272a` (DIVIDER)
**Panel**:
- Background Color: `#1e1e2f` (BACKGROUND_CARD)
- Border Width: `1`
- Border Color: `#3f3f46` (BORDER_DEFAULT)
- Corner Radius: `8`
**LineEdit Normal**:
- Background Color: `#16213e` (BACKGROUND_SECONDARY)
- Border Width: `2`
- Border Color: `#3f3f46` (BORDER_DEFAULT)
- Corner Radius: `4`
**LineEdit Focus**:
- Border Color: `#d4af37` (GOLD_ACCENT)
### 5. Set Theme in Project
1. Project → Project Settings → GUI → Theme
2. Set Custom Theme to `res://assets/themes/main_theme.tres`
Or set per-scene by selecting root Control node and setting Theme property.
## Using the Theme
### In Scenes
Most UI elements will automatically use the theme. For custom styling:
```gdscript
# Access theme colors
var label = Label.new()
label.add_theme_color_override("font_color", ThemeColors.GOLD_ACCENT)
# Access theme fonts
var heading = Label.new()
heading.add_theme_font_override("font", preload("res://assets/fonts/Cinzel_Heading_Large.tres"))
# Access theme styleboxes
var panel = Panel.new()
var stylebox = get_theme_stylebox("panel", "Panel").duplicate()
stylebox.bg_color = ThemeColors.BACKGROUND_TERTIARY
panel.add_theme_stylebox_override("panel", stylebox)
```
### Creating Custom Styles
```gdscript
# Create a card-style panel
var stylebox = StyleBoxFlat.new()
stylebox.bg_color = ThemeColors.BACKGROUND_CARD
stylebox.border_width_all = 2
stylebox.border_color = ThemeColors.BORDER_DEFAULT
stylebox.corner_radius_all = 8
stylebox.shadow_color = ThemeColors.SHADOW
stylebox.shadow_size = 4
panel.add_theme_stylebox_override("panel", stylebox)
```
### Reusable Component Scenes
Create reusable UI components in `scenes/components/`:
**Card.tscn**:
- PanelContainer with card styling
- Margins for content padding
- Optional header and footer sections
**CustomButton.tscn**:
- Button with custom styling
- Hover effects
- Icon support
**FormField.tscn**:
- Label + LineEdit combo
- Validation error display
- Help text support
## Theme Variations
### Headings
Create different heading styles:
```gdscript
# H1 - Large heading
var h1 = Label.new()
h1.add_theme_font_override("font", preload("res://assets/fonts/Cinzel_Heading_Large.tres"))
h1.add_theme_color_override("font_color", ThemeColors.GOLD_ACCENT)
# H2 - Medium heading
var h2 = Label.new()
h2.add_theme_font_override("font", preload("res://assets/fonts/Cinzel_Heading_Medium.tres"))
h2.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
# H3 - Small heading
var h3 = Label.new()
h3.add_theme_font_override("font", preload("res://assets/fonts/Cinzel_Heading_Small.tres"))
h3.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
```
### Status Messages
```gdscript
# Success message
var success_label = Label.new()
success_label.add_theme_color_override("font_color", ThemeColors.SUCCESS)
# Error message
var error_label = Label.new()
error_label.add_theme_color_override("font_color", ThemeColors.ERROR)
```
### Progress Bars
```gdscript
# HP Bar
var hp_bar = ProgressBar.new()
hp_bar.add_theme_color_override("fill_color", ThemeColors.HP_COLOR)
# Mana Bar
var mana_bar = ProgressBar.new()
mana_bar.add_theme_color_override("fill_color", ThemeColors.MANA_COLOR)
```
## Responsive Scaling
### DPI Scaling
Godot handles DPI scaling automatically. For custom scaling:
```gdscript
# Get DPI scale
var scale = DisplayServer.screen_get_scale()
# Adjust font size
var font_size = 14 * scale
label.add_theme_font_size_override("font_size", int(font_size))
```
### Mobile Adjustments
```gdscript
# Detect platform
var is_mobile = OS.get_name() in ["Android", "iOS"]
if is_mobile:
# Larger touch targets
button.custom_minimum_size = Vector2(60, 60)
# Larger fonts
label.add_theme_font_size_override("font_size", 16)
else:
# Smaller desktop sizes
button.custom_minimum_size = Vector2(40, 40)
label.add_theme_font_size_override("font_size", 14)
```
## Testing the Theme
### Visual Consistency
Create a theme test scene with all UI elements:
- Buttons (normal, hover, pressed, disabled)
- Labels (headings, body, secondary)
- Input fields (normal, focused, error)
- Panels and cards
- Progress bars
- Lists and grids
### Cross-Platform
Test theme on:
- Desktop (Windows/Mac/Linux) - Check with different DPI settings
- Mobile (Android/iOS) - Check on different screen sizes
- Web - Check in different browsers
## Advanced: Dynamic Theming
For future dark/light mode support:
```gdscript
# Theme manager (future)
class ThemeManager:
static var current_theme = "dark"
static func switch_theme(theme_name: String):
match theme_name:
"dark":
# Load dark theme
get_tree().root.theme = preload("res://assets/themes/dark_theme.tres")
"light":
# Load light theme
get_tree().root.theme = preload("res://assets/themes/light_theme.tres")
```
## Resources
- [Godot Theme Documentation](https://docs.godotengine.org/en/stable/tutorials/ui/gui_using_theme_editor.html)
- [Google Fonts](https://fonts.google.com/)
- [Color Palette Tool](https://coolors.co/)
## Quick Start Checklist
- [ ] Download Cinzel and Lato fonts
- [ ] Copy fonts to `assets/fonts/`
- [ ] Create `main_theme.tres` in `assets/themes/`
- [ ] Set default font to Lato-Regular
- [ ] Configure Button colors and styleboxes
- [ ] Configure Panel styleboxes
- [ ] Configure LineEdit colors and styleboxes
- [ ] Create heading font variations
- [ ] Set project theme in settings
- [ ] Test in a sample scene
- [ ] Create reusable component scenes

View File

@@ -0,0 +1,452 @@
# Character List Scene - Implementation Plan
**Status:** Draft - Pending Approval
**Date:** November 20, 2025
---
## Overview
This document outlines the implementation plan for the Character List scene in the Godot client. This scene displays all characters owned by the authenticated user and allows them to select a character to play or create a new one.
---
## API Endpoint Reference
**Endpoint:** `GET /api/v1/characters`
**Authentication:** Required
**Source:** `api/app/api/characters.py:140-221`
### Response Format
```json
{
"result": {
"characters": [
{
"character_id": "char_001",
"name": "Thorin Ironheart",
"class": "vanguard",
"class_name": "Vanguard",
"level": 5,
"experience": 250,
"gold": 1000,
"current_location": "town_square",
"origin": "soul_revenant"
}
],
"count": 1,
"tier": "free",
"limit": 1
}
}
```
---
## Scene Architecture
### File Structure
```
godot_client/
├── scenes/
│ └── character/
│ ├── character_list.tscn # Main scene
│ └── character_card.tscn # Reusable card component
└── scripts/
└── character/
├── character_list.gd # List controller
└── character_card.gd # Card component script
```
### Scene Hierarchy
```
CharacterList (Control)
├── VBoxContainer
│ ├── Header (HBoxContainer)
│ │ ├── TitleLabel ("Your Characters")
│ │ └── TierInfo (Label - "Free: 1/1")
│ ├── LoadingIndicator (CenterContainer)
│ │ └── ProgressIndicator
│ ├── ErrorContainer (MarginContainer)
│ │ └── ErrorLabel
│ ├── CharacterScrollContainer (ScrollContainer)
│ │ └── CharacterGrid (GridContainer or VBoxContainer)
│ │ └── [CharacterCard instances]
│ └── ActionContainer (HBoxContainer)
│ ├── CreateButton ("Create New Character")
│ └── RefreshButton
└── EmptyState (CenterContainer)
└── EmptyStateCard
├── Icon
├── MessageLabel ("No characters yet")
└── CreateFirstButton
```
---
## Implementation Steps
### Phase 1: Scene Setup
1. **Create character_list.tscn**
- Root node: Control (full rect)
- Add VBoxContainer with margins
- Set up header with title and tier info
- Add ScrollContainer for character list
- Add action buttons at bottom
2. **Create character_card.tscn** (reusable component)
- Use existing Card component as base
- Layout: horizontal with left info, right actions
- Display: name, class icon, level, gold, location
- Buttons: Select, Delete
### Phase 2: Script Implementation
#### character_list.gd
```gdscript
extends Control
## Character List Screen
##
## Displays user's characters and allows selection or creation.
#region Signals
signal character_selected(character_id: String)
signal create_requested
#endregion
#region Node References
@onready var character_grid: Container = $VBoxContainer/CharacterScrollContainer/CharacterGrid
@onready var loading_indicator: Control = $VBoxContainer/LoadingIndicator
@onready var error_container: Control = $VBoxContainer/ErrorContainer
@onready var error_label: Label = $VBoxContainer/ErrorContainer/ErrorLabel
@onready var empty_state: Control = $EmptyState
@onready var tier_label: Label = $VBoxContainer/Header/TierInfo
@onready var create_button: Button = $VBoxContainer/ActionContainer/CreateButton
@onready var refresh_button: Button = $VBoxContainer/ActionContainer/RefreshButton
#endregion
#region Service References
@onready var http_client: Node = get_node("/root/HTTPClient")
@onready var state_manager: Node = get_node("/root/StateManager")
#endregion
#region Private Variables
var _characters: Array = []
var _is_loading: bool = false
var _tier: String = "free"
var _limit: int = 1
#endregion
#region Lifecycle
func _ready() -> void:
_connect_signals()
load_characters()
func _connect_signals() -> void:
create_button.pressed.connect(_on_create_button_pressed)
refresh_button.pressed.connect(_on_refresh_button_pressed)
#endregion
#region Public Methods
func load_characters() -> void:
"""Fetch characters from API."""
if _is_loading:
return
_set_loading(true)
_hide_error()
http_client.http_get(
"/api/v1/characters",
_on_characters_loaded,
_on_characters_error
)
func refresh() -> void:
"""Refresh character list."""
load_characters()
#endregion
#region Private Methods
func _set_loading(loading: bool) -> void:
_is_loading = loading
loading_indicator.visible = loading
character_grid.visible = not loading
refresh_button.disabled = loading
func _show_error(message: String) -> void:
error_label.text = message
error_container.visible = true
func _hide_error() -> void:
error_container.visible = false
func _update_ui() -> void:
# Update tier info
tier_label.text = "%s: %d/%d" % [_tier.capitalize(), _characters.size(), _limit]
# Enable/disable create button based on limit
create_button.disabled = _characters.size() >= _limit
# Show empty state or character list
if _characters.is_empty():
empty_state.visible = true
character_grid.visible = false
else:
empty_state.visible = false
character_grid.visible = true
func _populate_character_list() -> void:
# Clear existing cards
for child in character_grid.get_children():
child.queue_free()
# Create card for each character
var card_scene = preload("res://scenes/character/character_card.tscn")
for char_data in _characters:
var card = card_scene.instantiate()
card.set_character_data(char_data)
card.selected.connect(_on_character_card_selected)
card.delete_requested.connect(_on_character_delete_requested)
character_grid.add_child(card)
#endregion
#region Signal Handlers
func _on_characters_loaded(response: APIResponse) -> void:
_set_loading(false)
if not response.is_success():
_on_characters_error(response)
return
var result = response.result
_characters = result.get("characters", [])
_tier = result.get("tier", "free")
_limit = result.get("limit", 1)
_populate_character_list()
_update_ui()
func _on_characters_error(response: APIResponse) -> void:
_set_loading(false)
var message = response.get_error_message()
if message.is_empty():
message = "Failed to load characters"
if response.status == 401:
# Redirect to login
get_tree().change_scene_to_file("res://scenes/auth/login.tscn")
return
_show_error(message)
func _on_character_card_selected(character_id: String) -> void:
state_manager.set_value("current_character_id", character_id)
character_selected.emit(character_id)
# Navigate to main game or character detail
# get_tree().change_scene_to_file("res://scenes/game/main_game.tscn")
func _on_character_delete_requested(character_id: String) -> void:
# Show confirmation dialog
# Then call DELETE endpoint
pass
func _on_create_button_pressed() -> void:
create_requested.emit()
# Navigate to character creation wizard
# get_tree().change_scene_to_file("res://scenes/character/character_create.tscn")
func _on_refresh_button_pressed() -> void:
refresh()
#endregion
```
#### character_card.gd
```gdscript
extends Control
class_name CharacterCard
## Character Card Component
##
## Displays a single character's summary information.
#region Signals
signal selected(character_id: String)
signal delete_requested(character_id: String)
#endregion
#region Node References
@onready var name_label: Label = $Card/HBox/InfoVBox/NameLabel
@onready var class_label: Label = $Card/HBox/InfoVBox/ClassLabel
@onready var level_label: Label = $Card/HBox/InfoVBox/LevelLabel
@onready var gold_label: Label = $Card/HBox/InfoVBox/GoldLabel
@onready var location_label: Label = $Card/HBox/InfoVBox/LocationLabel
@onready var select_button: Button = $Card/HBox/ActionVBox/SelectButton
@onready var delete_button: Button = $Card/HBox/ActionVBox/DeleteButton
#endregion
#region Private Variables
var _character_id: String = ""
var _character_data: Dictionary = {}
#endregion
#region Lifecycle
func _ready() -> void:
select_button.pressed.connect(_on_select_pressed)
delete_button.pressed.connect(_on_delete_pressed)
#endregion
#region Public Methods
func set_character_data(data: Dictionary) -> void:
"""Set character data and update display."""
_character_data = data
_character_id = data.get("character_id", "")
name_label.text = data.get("name", "Unknown")
class_label.text = data.get("class_name", "Unknown Class")
level_label.text = "Level %d" % data.get("level", 1)
gold_label.text = "%d Gold" % data.get("gold", 0)
location_label.text = _format_location(data.get("current_location", ""))
func get_character_id() -> String:
return _character_id
#endregion
#region Private Methods
func _format_location(location_id: String) -> String:
# Convert location_id to display name
return location_id.replace("_", " ").capitalize()
#endregion
#region Signal Handlers
func _on_select_pressed() -> void:
selected.emit(_character_id)
func _on_delete_pressed() -> void:
delete_requested.emit(_character_id)
#endregion
```
### Phase 3: UI Polish
1. **Styling**
- Use Card component for character cards
- Apply theme colors from ThemeColors
- Add class icons (placeholder or loaded)
- Add hover/focus states
2. **Animations**
- Fade in cards on load
- Button press feedback
- Loading spinner animation
3. **Responsive Layout**
- GridContainer for desktop (2-3 columns)
- VBoxContainer for mobile (single column)
- Handle different screen sizes
### Phase 4: Integration
1. **Navigation Flow**
- Login success → Character List
- Character selected → Main Game
- Create button → Character Creation Wizard
- Empty state create → Character Creation Wizard
2. **State Management**
- Store selected character_id in StateManager
- Handle session expiration (401 errors)
3. **Delete Confirmation**
- Add confirmation dialog component
- Call DELETE /api/v1/characters/{id}
- Refresh list on success
---
## UI Mockup
```
┌─────────────────────────────────────────┐
│ Your Characters Free: 2/3 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ Thorin Ironheart [Select] │ │
│ │ Vanguard • Level 5 │ │
│ │ 1,000 Gold │ │
│ │ Town Square [Delete] │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Elara Shadowstep [Select] │ │
│ │ Assassin • Level 3 │ │
│ │ 500 Gold │ │
│ │ Dark Forest [Delete] │ │
│ └─────────────────────────────────┘ │
│ │
├─────────────────────────────────────────┤
│ [Create New Character] [Refresh] │
└─────────────────────────────────────────┘
```
---
## Error Handling
| Error Type | User Message | Action |
|------------|--------------|--------|
| 401 Unauthorized | "Session expired" | Redirect to login |
| 500 Server Error | "Server error. Try again." | Show retry button |
| Network Error | "Cannot connect to server" | Show retry button |
| Empty Response | "No characters found" | Show empty state |
---
## Testing Checklist
- [ ] Characters load on scene ready
- [ ] Loading indicator shows during fetch
- [ ] Error message displays on API error
- [ ] 401 redirects to login
- [ ] Empty state shows when no characters
- [ ] Tier info updates correctly
- [ ] Create button disabled when at limit
- [ ] Select button navigates correctly
- [ ] Delete shows confirmation
- [ ] Delete refreshes list on success
- [ ] Refresh button reloads list
---
## Dependencies
- **Existing:** HTTPClient, StateManager, APIResponse, Card component
- **New:** CharacterCard component, ConfirmDialog (for delete)
---
## Estimated Effort
- Phase 1 (Scene Setup): 1-2 hours
- Phase 2 (Scripts): 2-3 hours
- Phase 3 (UI Polish): 1-2 hours
- Phase 4 (Integration): 1-2 hours
**Total:** 5-9 hours
---
## Notes
- Consider caching character list in StateManager for faster navigation
- Add pull-to-refresh for mobile
- Consider adding character preview image in future
- Location names should come from a mapping (not raw IDs)