Files
Code_of_Conquest/godot_client/docs/ARCHITECTURE.md
2025-11-24 23:10:55 -06:00

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 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:

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:

  1. Direct access (simple, works in _ready() and later):
func _ready():
    HTTPClient.http_get("/api/v1/characters", _on_success)
  1. Get node explicitly (verbose but always works):
func _on_button_clicked():
    get_node("/root/HTTPClient").http_get("/api/v1/characters", _on_success)
  1. 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 message
  • error.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:

  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