first commit
This commit is contained in:
767
godot_client/docs/ARCHITECTURE.md
Normal file
767
godot_client/docs/ARCHITECTURE.md
Normal 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
413
godot_client/docs/EXPORT.md
Normal 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
|
||||
381
godot_client/docs/GETTING_STARTED.md
Normal file
381
godot_client/docs/GETTING_STARTED.md
Normal 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.
|
||||
823
godot_client/docs/MULTIPLAYER.md
Normal file
823
godot_client/docs/MULTIPLAYER.md
Normal 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
103
godot_client/docs/README.md
Normal 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
|
||||
361
godot_client/docs/THEME_SETUP.md
Normal file
361
godot_client/docs/THEME_SETUP.md
Normal 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
|
||||
452
godot_client/docs/scene_char_list.md
Normal file
452
godot_client/docs/scene_char_list.md
Normal 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)
|
||||
Reference in New Issue
Block a user