22 KiB
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 screenscenes/character/character_list.tscn- Character listscenes/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 variantsCard- Container with header/footerFormField- 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:
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:
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:
- Direct access (simple, works in
_ready()and later):
func _ready():
HTTPClient.http_get("/api/v1/characters", _on_success)
- Get node explicitly (verbose but always works):
func _on_button_clicked():
get_node("/root/HTTPClient").http_get("/api/v1/characters", _on_success)
- Cached reference with @onready (best for multiple uses):
@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:
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:
# 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:
# 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
# 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
# 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
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:
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
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 messageerror.code: Machine-readable error code
User-Facing Errors
Show errors to users:
# 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:
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:
# 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:
# 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
# Add to HTTPClient, StateManager, etc.
print("[HTTPClient] GET /api/v1/characters")
print("[StateManager] User logged in: ", user_data.get("email"))
Remote Debugging
For mobile:
- Enable remote debug in Godot
- Connect device via USB
- 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:
-
Implement Auth Screens (Week 2)
- Login, Register, Password Reset
- Email verification
- Form validation
-
Implement Character Management (Weeks 3-4)
- Character creation wizard
- Character list
- Character detail
- Delete character
-
Platform Optimization (Week 5)
- Mobile touch controls
- Desktop keyboard shortcuts
- Web export configuration
-
Future Features (Week 6+)
- Combat UI
- World exploration
- Quest system
- Multiplayer