first commit

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

View File

@@ -0,0 +1,96 @@
extends PanelContainer
class_name CharacterCard
## Character Card Component
##
## Displays a single character's summary information with select/delete actions.
#region Signals
signal selected(character_id: String)
signal delete_requested(character_id: String)
#endregion
#region Node References
@onready var name_label: Label = $MarginContainer/HBoxContainer/InfoVBox/NameLabel
@onready var class_label: Label = $MarginContainer/HBoxContainer/InfoVBox/ClassLabel
@onready var level_label: Label = $MarginContainer/HBoxContainer/InfoVBox/LevelLabel
@onready var gold_label: Label = $MarginContainer/HBoxContainer/InfoVBox/GoldLabel
@onready var location_label: Label = $MarginContainer/HBoxContainer/InfoVBox/LocationLabel
@onready var select_button: Button = $MarginContainer/HBoxContainer/ActionVBox/SelectButton
@onready var delete_button: Button = $MarginContainer/HBoxContainer/ActionVBox/DeleteButton
#endregion
#region Private Variables
var _character_id: String = ""
var _character_data: Dictionary = {}
var _pending_data: Dictionary = {} # Store data if set before _ready
#endregion
#region Lifecycle
func _ready() -> void:
#_apply_style()
select_button.pressed.connect(_on_select_pressed)
delete_button.pressed.connect(_on_delete_pressed)
# Apply pending data if set before _ready
if not _pending_data.is_empty():
_apply_character_data(_pending_data)
_pending_data = {}
#endregion
#region Public Methods
func set_character_data(data: Dictionary) -> void:
"""Set character data and update display."""
if not is_node_ready():
# Store for later if called before _ready
_pending_data = data
return
_apply_character_data(data)
func _apply_character_data(data: Dictionary) -> void:
"""Actually apply the character data to UI elements."""
_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 _apply_style() -> void:
#"""Apply card styling."""
#var style = StyleBoxFlat.new()
#style.bg_color = ThemeColors.BACKGROUND_CARD
#style.border_width_all = 1
#style.border_color = ThemeColors.BORDER_DEFAULT
#style.corner_radius_all = 8
#style.content_margin_left = 0
#style.content_margin_right = 0
#style.content_margin_top = 0
#style.content_margin_bottom = 0
#add_theme_stylebox_override("panel", style)
func _format_location(location_id: String) -> String:
"""Convert location_id to display name."""
if location_id.is_empty():
return "Unknown"
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

View File

@@ -0,0 +1 @@
uid://sxgrib8ck0wx

View File

@@ -0,0 +1,200 @@
extends Control
## Character List Screen
##
## Displays user's characters and allows selection or creation.
## Fetches data from GET /api/v1/characters endpoint.
#region Signals
signal character_selected(character_id: String)
signal create_requested
#endregion
#region Node References
@onready var character_container: VBoxContainer = $VBoxContainer/CharacterScrollContainer/CharacterVBox
@onready var loading_indicator: CenterContainer = $VBoxContainer/LoadingIndicator
@onready var error_container: MarginContainer = $VBoxContainer/ErrorContainer
@onready var error_label: Label = $VBoxContainer/ErrorContainer/ErrorLabel
@onready var empty_state: CenterContainer = $EmptyState
@onready var tier_label: Label = $VBoxContainer/Header/TierLabel
@onready var create_button: Button = $VBoxContainer/ActionContainer/CreateButton
@onready var refresh_button: Button = $VBoxContainer/ActionContainer/RefreshButton
@onready var scroll_container: ScrollContainer = $VBoxContainer/CharacterScrollContainer
@onready var empty_create_button: Button = $EmptyState/VBoxContainer/CreateFirstButton
#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:
print("[CharacterList] Initialized")
_connect_signals()
_hide_error()
empty_state.visible = false
load_characters()
func _connect_signals() -> void:
create_button.pressed.connect(_on_create_button_pressed)
refresh_button.pressed.connect(_on_refresh_button_pressed)
empty_create_button.pressed.connect(_on_create_button_pressed)
#endregion
#region Public Methods
func load_characters() -> void:
"""Fetch characters from API."""
if _is_loading:
return
print("[CharacterList] Loading characters...")
_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
scroll_container.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
scroll_container.visible = false
else:
empty_state.visible = false
scroll_container.visible = true
func _populate_character_list() -> void:
# Clear existing cards
for child in character_container.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_container.add_child(card)
print("[CharacterList] Populated %d character cards" % _characters.size())
#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)
print("[CharacterList] Loaded %d characters (tier=%s, limit=%d)" % [_characters.size(), _tier, _limit])
_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:
print("[CharacterList] Session expired, redirecting to login")
get_tree().change_scene_to_file("res://scenes/auth/login.tscn")
return
print("[CharacterList] Error: %s (status=%d)" % [message, response.status])
_show_error(message)
func _on_character_card_selected(character_id: String) -> void:
print("[CharacterList] Character selected: %s" % character_id)
state_manager.select_character(character_id)
character_selected.emit(character_id)
# TODO: Navigate to main game scene
# get_tree().change_scene_to_file("res://scenes/game/main_game.tscn")
func _on_character_delete_requested(character_id: String) -> void:
print("[CharacterList] Delete requested for: %s" % character_id)
# TODO: Show confirmation dialog, then call DELETE endpoint
# For now, directly delete
_delete_character(character_id)
func _delete_character(character_id: String) -> void:
"""Delete a character via API."""
http_client.http_delete(
"/api/v1/characters/%s" % character_id,
func(response: APIResponse):
if response.is_success():
print("[CharacterList] Character deleted: %s" % character_id)
load_characters() # Refresh list
else:
_show_error(response.get_error_message()),
func(response: APIResponse):
_show_error(response.get_error_message())
)
func _on_create_button_pressed() -> void:
print("[CharacterList] Create button pressed")
create_requested.emit()
# TODO: Navigate to character creation wizard
# get_tree().change_scene_to_file("res://scenes/character/character_create.tscn")
func _on_refresh_button_pressed() -> void:
print("[CharacterList] Refresh button pressed")
refresh()
#endregion

View File

@@ -0,0 +1 @@
uid://x4mt6jwbywsl

View File

@@ -0,0 +1,202 @@
extends PanelContainer
class_name Card
## Card Component
##
## Styled container for content with optional header and footer.
## Mimics the card design from the web UI.
##
## Structure:
## Card (PanelContainer)
## └─ VBoxContainer
## ├─ Header (optional)
## ├─ Body (content)
## └─ Footer (optional)
##
## Usage:
## var card = Card.new()
## card.set_header("Character Details")
## card.add_content(my_content_node)
## card.set_footer_buttons(["Save", "Cancel"])
signal header_action_pressed(action: String)
signal footer_button_pressed(button_text: String)
# Export variables
@export var header_text: String = ""
@export var show_header: bool = false
@export var show_footer: bool = false
@export var card_style: StyleVariant = StyleVariant.DEFAULT
# Style variants
enum StyleVariant {
DEFAULT, # Standard card
HIGHLIGHTED, # Gold border highlight
SUBTLE # Minimal border
}
# Internal nodes
var _container: VBoxContainer = null
var _header_container: HBoxContainer = null
var _header_label: Label = null
var _body_container: MarginContainer = null
var _body_content: VBoxContainer = null
var _footer_container: HBoxContainer = null
func _ready() -> void:
_setup_structure()
_apply_style()
if not header_text.is_empty():
set_header(header_text)
## Setup card structure
func _setup_structure() -> void:
# Main container
_container = VBoxContainer.new()
add_child(_container)
# Header
_header_container = HBoxContainer.new()
_header_container.visible = show_header
_header_label = Label.new()
_header_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# TODO: Set heading font when theme is set up
_header_container.add_child(_header_label)
_container.add_child(_header_container)
# Add separator after header
var separator = HSeparator.new()
separator.visible = show_header
_container.add_child(separator)
# Body
_body_container = MarginContainer.new()
_body_container.add_theme_constant_override("margin_left", 16)
_body_container.add_theme_constant_override("margin_right", 16)
_body_container.add_theme_constant_override("margin_top", 16)
_body_container.add_theme_constant_override("margin_bottom", 16)
_body_content = VBoxContainer.new()
_body_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_body_content.size_flags_vertical = Control.SIZE_EXPAND_FILL
_body_container.add_child(_body_content)
_container.add_child(_body_container)
# Footer
var footer_separator = HSeparator.new()
footer_separator.visible = show_footer
_container.add_child(footer_separator)
_footer_container = HBoxContainer.new()
_footer_container.visible = show_footer
_footer_container.alignment = BoxContainer.ALIGNMENT_END
_container.add_child(_footer_container)
## Apply card styling
func _apply_style() -> void:
var style = StyleBoxFlat.new()
match card_style:
StyleVariant.DEFAULT:
style.bg_color = ThemeColors.BACKGROUND_CARD
style.border_width_all = 1
style.border_color = ThemeColors.BORDER_DEFAULT
style.corner_radius_all = 8
StyleVariant.HIGHLIGHTED:
style.bg_color = ThemeColors.BACKGROUND_CARD
style.border_width_all = 2
style.border_color = ThemeColors.GOLD_ACCENT
style.corner_radius_all = 8
style.shadow_color = ThemeColors.SHADOW
style.shadow_size = 4
StyleVariant.SUBTLE:
style.bg_color = ThemeColors.BACKGROUND_CARD
style.border_width_all = 1
style.border_color = ThemeColors.DIVIDER
style.corner_radius_all = 4
add_theme_stylebox_override("panel", style)
## Set header text and show header
func set_header(text: String) -> void:
header_text = text
_header_label.text = text
_header_container.visible = true
show_header = true
# Show separator
if _container.get_child_count() > 1:
_container.get_child(1).visible = true
## Hide header
func hide_header() -> void:
_header_container.visible = false
show_header = false
# Hide separator
if _container.get_child_count() > 1:
_container.get_child(1).visible = false
## Add content to card body
func add_content(node: Node) -> void:
_body_content.add_child(node)
## Clear all content from body
func clear_content() -> void:
for child in _body_content.get_children():
child.queue_free()
## Set footer buttons
func set_footer_buttons(button_labels: Array[String]) -> void:
# Clear existing buttons
for child in _footer_container.get_children():
child.queue_free()
# Add new buttons
for label in button_labels:
var btn = Button.new()
btn.text = label
btn.pressed.connect(_on_footer_button_pressed.bind(label))
_footer_container.add_child(btn)
# Show footer
_footer_container.visible = true
show_footer = true
# Show separator
if _container.get_child_count() > 2:
_container.get_child(_container.get_child_count() - 2).visible = true
## Hide footer
func hide_footer() -> void:
_footer_container.visible = false
show_footer = false
# Hide separator
if _container.get_child_count() > 2:
_container.get_child(_container.get_child_count() - 2).visible = false
## Set card style variant
func set_style_variant(variant: StyleVariant) -> void:
card_style = variant
_apply_style()
## Internal: Handle footer button press
func _on_footer_button_pressed(button_text: String) -> void:
footer_button_pressed.emit(button_text)

View File

@@ -0,0 +1 @@
uid://dq4fplw7kw5yu

View File

@@ -0,0 +1,280 @@
extends VBoxContainer
class_name FormField
## Form Field Component
##
## Combines label, input field, and error message display.
## Supports validation and different input types.
##
## Structure:
## VBoxContainer
## ├─ Label (field label)
## ├─ LineEdit (input field)
## └─ Label (error message, hidden by default)
##
## Usage:
## var field = FormField.new()
## field.label_text = "Email"
## field.placeholder = "Enter your email"
## field.input_type = FormField.InputType.EMAIL
## field.value_changed.connect(_on_email_changed)
signal value_changed(new_value: String)
signal validation_changed(is_valid: bool)
# Input types
enum InputType {
TEXT, # Plain text
EMAIL, # Email validation
PASSWORD, # Password (hidden)
NUMBER, # Numeric only
PHONE, # Phone number
}
# Export variables
@export var label_text: String = ""
@export var placeholder: String = ""
@export var input_type: InputType = InputType.TEXT
@export var required: bool = false
@export var min_length: int = 0
@export var max_length: int = 0
# Internal nodes
var _label: Label = null
var _input: LineEdit = null
var _error_label: Label = null
# State
var _is_valid: bool = true
var _error_message: String = ""
func _ready() -> void:
_setup_structure()
_apply_styling()
## Setup form field structure
func _setup_structure() -> void:
# Label
_label = Label.new()
_label.text = label_text
add_child(_label)
# Spacing
var spacer1 = Control.new()
spacer1.custom_minimum_size = Vector2(0, 4)
add_child(spacer1)
# Input field
_input = LineEdit.new()
_input.placeholder_text = placeholder
_input.text_changed.connect(_on_text_changed)
_input.text_submitted.connect(_on_text_submitted)
# Set input type
match input_type:
InputType.PASSWORD:
_input.secret = true
InputType.NUMBER:
# TODO: Add number-only filter
pass
_:
pass
# Set max length if specified
if max_length > 0:
_input.max_length = max_length
add_child(_input)
# Spacing
var spacer2 = Control.new()
spacer2.custom_minimum_size = Vector2(0, 2)
add_child(spacer2)
# Error label
_error_label = Label.new()
_error_label.visible = false
add_child(_error_label)
## Apply styling
func _apply_styling() -> void:
# Label styling
_label.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
# Input styling
var normal_style = StyleBoxFlat.new()
normal_style.bg_color = ThemeColors.BACKGROUND_SECONDARY
normal_style.border_width_all = 2
normal_style.border_color = ThemeColors.BORDER_DEFAULT
normal_style.corner_radius_all = 4
normal_style.content_margin_left = 12
normal_style.content_margin_right = 12
normal_style.content_margin_top = 8
normal_style.content_margin_bottom = 8
_input.add_theme_stylebox_override("normal", normal_style)
var focus_style = normal_style.duplicate()
focus_style.border_color = ThemeColors.GOLD_ACCENT
_input.add_theme_stylebox_override("focus", focus_style)
_input.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
_input.add_theme_color_override("font_placeholder_color", ThemeColors.TEXT_SECONDARY)
_input.add_theme_color_override("caret_color", ThemeColors.GOLD_ACCENT)
# Error label styling
_error_label.add_theme_color_override("font_color", ThemeColors.ERROR)
# TODO: Add smaller font size when theme is set up
## Get current value
func get_value() -> String:
return _input.text if _input else ""
## Set value
func set_value(value: String) -> void:
if _input:
_input.text = value
_validate()
## Set label text
func set_label(text: String) -> void:
label_text = text
if _label:
_label.text = text
## Set placeholder text
func set_placeholder(text: String) -> void:
placeholder = text
if _input:
_input.placeholder_text = text
## Show error message
func show_error(message: String) -> void:
_error_message = message
_is_valid = false
if _error_label:
_error_label.text = message
_error_label.visible = true
# Change input border to error color
if _input:
var error_style = StyleBoxFlat.new()
error_style.bg_color = ThemeColors.BACKGROUND_SECONDARY
error_style.border_width_all = 2
error_style.border_color = ThemeColors.ERROR
error_style.corner_radius_all = 4
error_style.content_margin_left = 12
error_style.content_margin_right = 12
error_style.content_margin_top = 8
error_style.content_margin_bottom = 8
_input.add_theme_stylebox_override("normal", error_style)
validation_changed.emit(false)
## Clear error
func clear_error() -> void:
_error_message = ""
_is_valid = true
if _error_label:
_error_label.visible = false
# Restore normal input styling
_apply_styling()
validation_changed.emit(true)
## Check if field is valid
func is_valid() -> bool:
return _is_valid
## Focus the input field
func focus_input() -> void:
if _input:
_input.grab_focus()
## Validate the field
func validate() -> bool:
return _validate()
## Internal: Validate input
func _validate() -> bool:
var value = get_value()
# Required check
if required and value.is_empty():
show_error("This field is required")
return false
# Min length check
if min_length > 0 and value.length() < min_length:
show_error("Must be at least %d characters" % min_length)
return false
# Max length check (redundant with LineEdit.max_length, but kept for consistency)
if max_length > 0 and value.length() > max_length:
show_error("Must be no more than %d characters" % max_length)
return false
# Type-specific validation
match input_type:
InputType.EMAIL:
if not _is_valid_email(value) and not value.is_empty():
show_error("Invalid email address")
return false
InputType.NUMBER:
if not value.is_valid_int() and not value.is_empty():
show_error("Must be a number")
return false
InputType.PHONE:
if not _is_valid_phone(value) and not value.is_empty():
show_error("Invalid phone number")
return false
# All validations passed
clear_error()
return true
## Internal: Email validation
func _is_valid_email(email: String) -> bool:
# Basic email regex validation
var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
return regex.search(email) != null
## Internal: Phone validation
func _is_valid_phone(phone: String) -> bool:
# Basic phone validation (digits, spaces, dashes, parentheses)
var regex = RegEx.new()
regex.compile("^[0-9\\s\\-\\(\\)\\+]+$")
return regex.search(phone) != null and phone.length() >= 10
## Internal: Handle text change
func _on_text_changed(new_text: String) -> void:
# Clear error when user starts typing
if not _error_message.is_empty():
clear_error()
value_changed.emit(new_text)
## Internal: Handle text submission (Enter key)
func _on_text_submitted(new_text: String) -> void:
_validate()

View File

@@ -0,0 +1 @@
uid://dvx6m2ahutlgm

View File

@@ -0,0 +1,171 @@
extends Control
## Main Scene
##
## Entry point for the application.
## Displays loading screen with logged in player info.
## Provides "Play Now" and "Logout" buttons for authenticated users.
# Scene paths
const SCENE_LOGIN = "res://scenes/auth/login.tscn"
const SCENE_CHARACTER_LIST = "res://scenes/character/character_list.tscn"
# UI node references (from scene)
@onready var background_panel: Panel = $BackgroundPanel
@onready var title_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/TitleLabel
@onready var welcome_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/WelcomeLabel
@onready var status_label: Label = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/StatusLabel
@onready var button_container: HBoxContainer = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/ButtonContainer
@onready var play_now_button: Button = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/ButtonContainer/PlayNowButton
@onready var logout_button: Button = $CenterContainer/PanelContainer/MarginContainer/VBoxContainer/ButtonContainer/LogoutButton
func _ready() -> void:
print("=== Code of Conquest Starting ===")
print("Godot Version: ", Engine.get_version_info())
print("Platform: ", OS.get_name())
# Connect button signals
play_now_button.pressed.connect(_on_play_now_pressed)
logout_button.pressed.connect(_on_logout_pressed)
# Initially hide buttons (will show after auth check)
button_container.visible = false
# Wait a frame for services to initialize
await get_tree().process_frame
# Check authentication status
_check_authentication()
## Check if user is authenticated
func _check_authentication() -> void:
if StateManager.is_authenticated():
print("[Main] User is authenticated")
# Get user info and display
var user_data = StateManager.get_current_user()
var username = user_data.get("email", user_data.get("name", "Player"))
welcome_label.text = "Welcome, %s" % username
status_label.text = "Validating session..."
# Validate token with backend
_validate_token()
else:
print("[Main] User not authenticated")
welcome_label.text = "Not authenticated"
status_label.text = "Please log in to continue"
# Wait a moment then navigate to login
await get_tree().create_timer(1.5).timeout
_navigate_to_login()
## Validate auth token with backend
func _validate_token() -> void:
# TODO: Replace with actual validation endpoint
# For now, assume token is valid and show buttons
print("[Main] Token validation not yet implemented")
print("[Main] Assuming valid, showing play options...")
# Uncomment when endpoint exists:
# HTTPClient.http_get("/api/v1/auth/validate", _on_token_valid, _on_token_invalid)
# For now, just show buttons (remove this when validation is implemented)
await get_tree().create_timer(0.5).timeout
_show_play_options()
## Show play/logout buttons
func _show_play_options() -> void:
status_label.text = "Ready to play!"
button_container.visible = true
## Handle valid token
func _on_token_valid(response: APIResponse) -> void:
if response.is_success():
print("[Main] Token is valid")
_show_play_options()
else:
print("[Main] Token validation failed: ", response.get_error_message())
_handle_invalid_token()
## Handle invalid token
#func _on_token_invalid(response: APIResponse) -> void:
#print("[Main] Token invalid or network error")
#_handle_invalid_token()
## Clear session and go to login
func _handle_invalid_token() -> void:
status_label.text = "Session expired"
welcome_label.text = "Please log in again"
button_container.visible = false
StateManager.clear_user_session()
await get_tree().create_timer(1.5).timeout
_navigate_to_login()
## Handle "Play Now" button press
func _on_play_now_pressed() -> void:
print("[Main] Play Now button pressed")
# Disable buttons during navigation
button_container.visible = false
status_label.text = "Loading characters..."
# Navigate to character list
_navigate_to_character_list()
## Handle "Logout" button press
func _on_logout_pressed() -> void:
print("[Main] Logout button pressed")
# Disable buttons
button_container.visible = false
status_label.text = "Logging out..."
welcome_label.text = "Goodbye!"
# Clear session
StateManager.clear_user_session()
# Wait a moment then navigate to login
await get_tree().create_timer(1.0).timeout
_navigate_to_login()
## Navigate to login scene
func _navigate_to_login() -> void:
print("[Main] Navigating to login...")
# Check if scene exists
if not FileAccess.file_exists(SCENE_LOGIN):
print("[Main] ERROR: Login scene not found at ", SCENE_LOGIN)
status_label.text = "Login scene not yet created.\nSee Phase 2 in GETTING_STARTED.md"
return
# Navigate
get_tree().change_scene_to_file(SCENE_LOGIN)
## Navigate to character list
func _navigate_to_character_list() -> void:
print("[Main] Navigating to character list...")
# Check if scene exists
if not FileAccess.file_exists(SCENE_CHARACTER_LIST):
print("[Main] ERROR: Character list scene not found at ", SCENE_CHARACTER_LIST)
status_label.text = "Character list not yet created.\nSee Phase 3 in GETTING_STARTED.md"
button_container.visible = true # Re-show buttons
return
# Navigate
get_tree().change_scene_to_file(SCENE_CHARACTER_LIST)

View File

@@ -0,0 +1 @@
uid://d100mupmyal5

View File

@@ -0,0 +1,56 @@
extends RefCounted
class_name APIResponse
## API Response Model
##
## Represents a response from the Flask backend API.
## Matches the backend response format:
## {
## "app": "Code of Conquest",
## "version": "0.1.0",
## "status": 200,
## "timestamp": "2025-11-16T...",
## "result": {...},
## "error": {...},
## "meta": {...}
## }
var app: String
var version: String
var status: int
var timestamp: String
var result: Variant
var error: Dictionary
var meta: Dictionary
var raw_response: String
func _init(json_data: Dictionary) -> void:
app = json_data.get("app", "")
version = json_data.get("version", "")
status = json_data.get("status", 0)
timestamp = json_data.get("timestamp", "")
result = json_data.get("result", null)
# Handle Dictionary fields that might be null
var error_data = json_data.get("error", null)
error = error_data if error_data != null else {}
var meta_data = json_data.get("meta", null)
meta = meta_data if meta_data != null else {}
## Check if the request was successful (2xx status)
func is_success() -> bool:
return status >= 200 and status < 300
## Check if there's an error
func has_error() -> bool:
return not error.is_empty() or status >= 400
## Get error message if present
func get_error_message() -> String:
if error.has("message"):
return error["message"]
return ""

View File

@@ -0,0 +1 @@
uid://cwwx1jcnpafur

View File

@@ -0,0 +1,364 @@
extends Node
## HTTPClient Service
##
## Singleton service for all HTTP communication with the Flask backend.
## Handles authentication, JSON parsing, error handling, and provides
## a convenient interface for making API requests.
##
## Usage:
## HTTPClient.http_get("/api/v1/characters", _on_characters_loaded)
## HTTPClient.http_post("/api/v1/auth/login", {"email": "...", "password": "..."}, _on_login_complete)
# API Configuration
const API_TIMEOUT := 30.0 # Seconds
# HTTP Methods
enum Method {
GET,
POST,
PUT,
DELETE,
PATCH
}
# Internal state
var _auth_token: String = ""
var _session_cookie: String = "" # Session cookie for authentication
#var _request_queue: Array[Dictionary] = []
var _active_requests: Dictionary = {} # request_id -> HTTPRequest node
func _ready() -> void:
print("[HTTPClient] Service initialized. API Base URL: %s" % Settings.get_api_url())
## Make a GET request
##
## @param endpoint: API endpoint (e.g., "/api/v1/characters")
## @param callback: Function to call with APIResponse when complete
## @param error_callback: Optional function to call on error
func http_get(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void:
_make_request(Method.GET, endpoint, {}, callback, error_callback)
## Make a POST request
##
## @param endpoint: API endpoint
## @param data: Dictionary to send as JSON body
## @param callback: Function to call with APIResponse when complete
## @param error_callback: Optional function to call on error
func http_post(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
_make_request(Method.POST, endpoint, data, callback, error_callback)
## Make a PUT request
##
## @param endpoint: API endpoint
## @param data: Dictionary to send as JSON body
## @param callback: Function to call with APIResponse when complete
## @param error_callback: Optional function to call on error
func http_put(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
_make_request(Method.PUT, endpoint, data, callback, error_callback)
## Make a DELETE request
##
## @param endpoint: API endpoint
## @param callback: Function to call with APIResponse when complete
## @param error_callback: Optional function to call on error
func http_delete(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void:
_make_request(Method.DELETE, endpoint, {}, callback, error_callback)
## Make a PATCH request
##
## @param endpoint: API endpoint
## @param data: Dictionary to send as JSON body
## @param callback: Function to call with APIResponse when complete
## @param error_callback: Optional function to call on error
func http_patch(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void:
_make_request(Method.PATCH, endpoint, data, callback, error_callback)
## Set the authentication token for subsequent requests
##
## @param token: JWT token from login/registration
func set_auth_token(token: String) -> void:
_auth_token = token
print("[HTTPClient] Auth token set")
## Clear the authentication token (logout)
func clear_auth_token() -> void:
_auth_token = ""
print("[HTTPClient] Auth token cleared")
## Get current auth token
func get_auth_token() -> String:
return _auth_token
## Set the session cookie for subsequent requests
##
## @param cookie: Session cookie value (e.g., "coc_session=xxx")
func set_session_cookie(cookie: String) -> void:
_session_cookie = cookie
print("[HTTPClient] Session cookie set")
## Clear the session cookie (logout)
func clear_session_cookie() -> void:
_session_cookie = ""
print("[HTTPClient] Session cookie cleared")
## Get current session cookie
func get_session_cookie() -> String:
return _session_cookie
## Check if authenticated
func is_authenticated() -> bool:
return not _auth_token.is_empty() or not _session_cookie.is_empty()
## Internal: Make HTTP request
func _make_request(
method: Method,
endpoint: String,
data: Dictionary,
callback: Callable,
error_callback: Callable
) -> void:
# Create HTTPRequest node
var http_request := HTTPRequest.new()
add_child(http_request)
# Generate request ID for tracking
var request_id := "%s_%d" % [endpoint.get_file(), Time.get_ticks_msec()]
_active_requests[request_id] = http_request
# Build full URL
var url := Settings.get_api_url() + endpoint
# Build headers
var headers := _build_headers()
# Connect completion signal
http_request.request_completed.connect(
_on_request_completed.bind(request_id, callback, error_callback)
)
# Set timeout
http_request.timeout = API_TIMEOUT
# Make request based on method
var method_int := _method_to_int(method)
var body := ""
if method in [Method.POST, Method.PUT, Method.PATCH]:
body = JSON.stringify(data)
print("[HTTPClient] %s %s" % [_method_to_string(method), url])
if not body.is_empty():
print("[HTTPClient] Body: %s" % body)
var error := http_request.request(url, headers, method_int, body)
if error != OK:
push_error("[HTTPClient] Failed to initiate request: %s" % error)
_cleanup_request(request_id)
if error_callback.is_valid():
error_callback.call(_create_error_response("Failed to initiate request", 0))
## Internal: Build request headers
func _build_headers() -> PackedStringArray:
var headers := PackedStringArray([
"Content-Type: application/json",
"Accept: application/json"
])
# Add auth token if present (for JWT auth)
if not _auth_token.is_empty():
headers.append("Authorization: Bearer %s" % _auth_token)
# Add session cookie if present (for cookie-based auth)
if not _session_cookie.is_empty():
headers.append("Cookie: %s" % _session_cookie)
return headers
## Internal: Handle request completion
func _on_request_completed(
result: int,
response_code: int,
headers: PackedStringArray,
body: PackedByteArray,
request_id: String,
callback: Callable,
error_callback: Callable
) -> void:
print("[HTTPClient] Request completed: %s (status=%d)" % [request_id, response_code])
# Extract Set-Cookie header if present
_extract_session_cookie(headers)
# Parse response body
var body_string := body.get_string_from_utf8()
# Handle network errors
if result != HTTPRequest.RESULT_SUCCESS:
push_error("[HTTPClient] Network error: %s" % _result_to_string(result))
_cleanup_request(request_id)
if error_callback.is_valid():
error_callback.call(_create_error_response(_result_to_string(result), response_code))
return
# Parse JSON response
var json := JSON.new()
var parse_error := json.parse(body_string)
if parse_error != OK:
push_error("[HTTPClient] Failed to parse JSON: %s" % body_string)
_cleanup_request(request_id)
if error_callback.is_valid():
error_callback.call(_create_error_response("Failed to parse JSON response", response_code))
return
# Create APIResponse
var api_response: APIResponse = APIResponse.new(json.data)
api_response.raw_response = body_string
# Check for errors
if api_response.has_error():
print("[HTTPClient] API error: %s" % api_response.get_error_message())
if error_callback.is_valid():
error_callback.call(api_response)
elif callback.is_valid():
# Call regular callback even with error if no error callback
callback.call(api_response)
else:
print("[HTTPClient] Success: %s" % request_id)
if callback.is_valid():
callback.call(api_response)
_cleanup_request(request_id)
## Internal: Extract session cookie from response headers
func _extract_session_cookie(headers: PackedStringArray) -> void:
for header in headers:
# Look for Set-Cookie header
if header.begins_with("Set-Cookie:") or header.begins_with("set-cookie:"):
# Extract cookie value
var cookie_string := header.substr(11).strip_edges() # Remove "Set-Cookie:"
# Look for coc_session cookie
if cookie_string.begins_with("coc_session="):
# Extract just the cookie name=value part (before semicolon)
var cookie_parts := cookie_string.split(";")
if cookie_parts.size() > 0:
_session_cookie = cookie_parts[0].strip_edges()
print("[HTTPClient] Session cookie extracted: %s" % _session_cookie)
return
## Internal: Cleanup request resources
func _cleanup_request(request_id: String) -> void:
if _active_requests.has(request_id):
var http_request: HTTPRequest = _active_requests[request_id]
http_request.queue_free()
_active_requests.erase(request_id)
## Internal: Create error response
func _create_error_response(message: String, status_code: int) -> APIResponse:
var error_data := {
"app": "Code of Conquest",
"version": "0.1.0",
"status": status_code if status_code > 0 else 500,
"timestamp": Time.get_datetime_string_from_system(),
"result": null,
"error": {
"message": message,
"code": "NETWORK_ERROR"
},
"meta": {}
}
return APIResponse.new(error_data)
## Internal: Convert Method enum to HTTPClient constant
func _method_to_int(method: Method) -> int:
match method:
Method.GET:
return HTTPClient.METHOD_GET
Method.POST:
return HTTPClient.METHOD_POST
Method.PUT:
return HTTPClient.METHOD_PUT
Method.DELETE:
return HTTPClient.METHOD_DELETE
Method.PATCH:
return HTTPClient.METHOD_PATCH
_:
return HTTPClient.METHOD_GET
## Internal: Convert Method enum to string
func _method_to_string(method: Method) -> String:
match method:
Method.GET:
return "GET"
Method.POST:
return "POST"
Method.PUT:
return "PUT"
Method.DELETE:
return "DELETE"
Method.PATCH:
return "PATCH"
_:
return "UNKNOWN"
## Internal: Convert HTTPRequest result to string
func _result_to_string(result: int) -> String:
match result:
HTTPRequest.RESULT_SUCCESS:
return "Success"
HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH:
return "Chunked body size mismatch"
HTTPRequest.RESULT_CANT_CONNECT:
return "Can't connect to server"
HTTPRequest.RESULT_CANT_RESOLVE:
return "Can't resolve hostname"
HTTPRequest.RESULT_CONNECTION_ERROR:
return "Connection error"
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
return "TLS handshake error"
HTTPRequest.RESULT_NO_RESPONSE:
return "No response from server"
HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
return "Body size limit exceeded"
HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED:
return "Body decompression failed"
HTTPRequest.RESULT_REQUEST_FAILED:
return "Request failed"
HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN:
return "Can't open download file"
HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR:
return "Download file write error"
HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED:
return "Redirect limit reached"
HTTPRequest.RESULT_TIMEOUT:
return "Request timeout"
_:
return "Unknown error (%d)" % result

View File

@@ -0,0 +1 @@
uid://du1woo6w2kr3b

View File

@@ -0,0 +1,204 @@
extends Node
## Settings Service
##
## Singleton service for application configuration.
## Stores URLs, feature flags, and other runtime settings.
##
## Usage:
## Settings.get_api_url()
## Settings.get_web_url()
## Settings.set_environment("production")
# Environment types
enum Env {
DEVELOPMENT,
STAGING,
PRODUCTION
}
# Current environment
var _current_environment: int = Env.DEVELOPMENT
# URL Configuration
var _api_urls := {
Env.DEVELOPMENT: "http://localhost:5000",
Env.STAGING: "https://staging-api.codeofconquest.com",
Env.PRODUCTION: "https://api.codeofconquest.com"
}
var _web_urls := {
Env.DEVELOPMENT: "http://localhost:8000", # Flask serves web pages in dev
Env.STAGING: "https://staging.codeofconquest.com",
Env.PRODUCTION: "https://www.codeofconquest.com"
}
# Feature flags
var _features := {
"enable_debug_logging": true,
"enable_analytics": false,
"enable_multiplayer": false,
"max_characters_per_user": 5
}
# User preferences (persisted)
var _preferences := {
"remember_login": true,
"auto_save": true,
"sound_enabled": true,
"music_enabled": true,
"sound_volume": 0.8,
"music_volume": 0.6
}
# Save file configuration
const SETTINGS_FILE_PATH := "user://settings.save"
const SETTINGS_VERSION := 1
func _ready() -> void:
_load_settings()
_detect_environment()
print("[Settings] Service initialized")
print("[Settings] Environment: %s" % _environment_to_string(_current_environment))
print("[Settings] API URL: %s" % get_api_url())
print("[Settings] Web URL: %s" % get_web_url())
## Get current API base URL
func get_api_url() -> String:
return _api_urls.get(_current_environment, _api_urls[Env.DEVELOPMENT])
## Get current web frontend base URL
func get_web_url() -> String:
return _web_urls.get(_current_environment, _web_urls[Env.DEVELOPMENT])
## Get current environment
func get_environment() -> int:
return _current_environment
## Set environment (for testing/switching)
func set_environment(env: int) -> void:
_current_environment = env
print("[Settings] Environment changed to: %s" % _environment_to_string(env))
print("[Settings] API URL: %s" % get_api_url())
print("[Settings] Web URL: %s" % get_web_url())
## Check if a feature is enabled
func is_feature_enabled(feature_name: String) -> bool:
return _features.get(feature_name, false)
## Get feature value
func get_feature(feature_name: String, default: Variant = null) -> Variant:
return _features.get(feature_name, default)
## Set feature flag (for testing/debugging)
func set_feature(feature_name: String, value: Variant) -> void:
_features[feature_name] = value
print("[Settings] Feature updated: %s = %s" % [feature_name, value])
## Get user preference
func get_preference(key: String, default: Variant = null) -> Variant:
return _preferences.get(key, default)
## Set user preference
func set_preference(key: String, value: Variant) -> void:
_preferences[key] = value
print("[Settings] Preference updated: %s = %s" % [key, value])
_save_settings()
## Get all preferences
func get_all_preferences() -> Dictionary:
return _preferences.duplicate()
## Auto-detect environment based on OS and build flags
func _detect_environment() -> void:
# Check for --production command line argument
var args := OS.get_cmdline_args()
if "--production" in args:
_current_environment = Env.PRODUCTION
return
if "--staging" in args:
_current_environment = Env.STAGING
return
# Default to development
_current_environment = Env.DEVELOPMENT
## Save settings to disk
func _save_settings() -> void:
var save_data := {
"version": SETTINGS_VERSION,
"preferences": _preferences,
"timestamp": Time.get_unix_time_from_system()
}
var file := FileAccess.open(SETTINGS_FILE_PATH, FileAccess.WRITE)
if file == null:
push_error("[Settings] Failed to open settings file for writing: %s" % FileAccess.get_open_error())
return
file.store_string(JSON.stringify(save_data))
file.close()
print("[Settings] Settings saved to %s" % SETTINGS_FILE_PATH)
## Load settings from disk
func _load_settings() -> void:
if not FileAccess.file_exists(SETTINGS_FILE_PATH):
print("[Settings] No settings file found, using defaults")
return
var file := FileAccess.open(SETTINGS_FILE_PATH, FileAccess.READ)
if file == null:
push_error("[Settings] Failed to open settings file for reading: %s" % FileAccess.get_open_error())
return
var json_string := file.get_as_text()
file.close()
var json := JSON.new()
var parse_error := json.parse(json_string)
if parse_error != OK:
push_error("[Settings] Failed to parse settings file")
return
var save_data: Dictionary = json.data
# Check version
if save_data.get("version", 0) != SETTINGS_VERSION:
print("[Settings] Settings file version mismatch, using defaults")
return
# Restore preferences
var saved_prefs = save_data.get("preferences", {})
for key in saved_prefs:
_preferences[key] = saved_prefs[key]
print("[Settings] Settings loaded from %s" % SETTINGS_FILE_PATH)
## Convert environment enum to string
func _environment_to_string(env: int) -> String:
match env:
Env.DEVELOPMENT:
return "Development"
Env.STAGING:
return "Staging"
Env.PRODUCTION:
return "Production"
_:
return "Unknown"

View File

@@ -0,0 +1 @@
uid://3s8i3b6v5mde

View File

@@ -0,0 +1,422 @@
extends Node
## StateManager Service
##
## Singleton service for managing global application state.
## Handles user session, character data, wizard state, navigation, and persistence.
##
## Usage:
## StateManager.set_user_session(user_data)
## var user = StateManager.get_current_user()
## StateManager.save_state() # Persist to local storage
# Signals for state changes
signal user_logged_in(user_data: Dictionary)
signal user_logged_out()
signal character_selected(character_id: String)
signal character_created(character_data: Dictionary)
signal character_deleted(character_id: String)
signal characters_updated(characters: Array)
# Save file configuration
const SAVE_FILE_PATH := "user://coc_state.save"
const SAVE_VERSION := 1
# User session state
var _current_user: Dictionary = {}
var _auth_token: String = ""
var _is_authenticated: bool = false
# Character state
var _characters: Array[Dictionary] = []
var _selected_character_id: String = ""
var _character_limits: Dictionary = {}
# Character creation wizard state
var _wizard_state: Dictionary = {
"step": 0, # Current step (0-3)
"selected_origin": null, # Selected origin data
"selected_class": null, # Selected class data
"character_name": "",
"character_customization": {}
}
# Navigation state
var _current_scene: String = ""
var _scene_history: Array[String] = []
# Settings (deprecated - use Settings service for preferences)
# Kept for backward compatibility with save files
var _settings: Dictionary = {
"remember_login": true,
"auto_save": true
}
# Reference to HTTPClient singleton (available after _ready)
@onready var http_client: Node = get_node("/root/HTTPClient")
func _ready() -> void:
print("[StateManager] Service initialized")
_load_state()
## Set user session data (after login/registration)
func set_user_session(user_data: Dictionary, token: String = "") -> void:
_current_user = user_data
_auth_token = token
_is_authenticated = true
# Update HTTPClient with token (if using JWT auth)
if not token.is_empty():
http_client.set_auth_token(token)
print("[StateManager] User session set: %s" % user_data.get("email", "unknown"))
# Emit signal
user_logged_in.emit(user_data)
# Auto-save if enabled
if _settings.get("auto_save", true):
save_state()
## Clear user session (logout)
func clear_user_session() -> void:
_current_user = {}
_auth_token = ""
_is_authenticated = false
_characters = []
_selected_character_id = ""
# Clear HTTPClient token and cookie
http_client.clear_auth_token()
http_client.clear_session_cookie()
print("[StateManager] User session cleared")
# Emit signal
user_logged_out.emit()
# Clear saved state
_clear_save_file()
## Get current user data
func get_current_user() -> Dictionary:
return _current_user
## Get current auth token
func get_auth_token() -> String:
return _auth_token
## Check if user is authenticated
func is_authenticated() -> bool:
# Check both token and cookie-based auth
return _is_authenticated and (not _auth_token.is_empty() or http_client.is_authenticated())
## Set characters list
func set_characters(characters: Array) -> void:
_characters.clear()
for character in characters:
_characters.append(character)
print("[StateManager] Characters updated: %d total" % _characters.size())
# Emit signal
characters_updated.emit(_characters)
# Auto-save if enabled
if _settings.get("auto_save", true):
save_state()
## Get all characters
func get_characters() -> Array[Dictionary]:
return _characters
## Add character to list
func add_character(character_data: Dictionary) -> void:
_characters.append(character_data)
print("[StateManager] Character added: %s" % character_data.get("name", "unknown"))
# Emit signal
character_created.emit(character_data)
# Auto-save if enabled
if _settings.get("auto_save", true):
save_state()
## Remove character from list
func remove_character(character_id: String) -> void:
for i in range(_characters.size()):
if _characters[i].get("id") == character_id:
_characters.remove_at(i)
print("[StateManager] Character removed: %s" % character_id)
# Clear selection if this was selected
if _selected_character_id == character_id:
_selected_character_id = ""
# Emit signal
character_deleted.emit(character_id)
# Auto-save if enabled
if _settings.get("auto_save", true):
save_state()
return
## Get character by ID
func get_character(character_id: String) -> Dictionary:
for character in _characters:
if character.get("id") == character_id:
return character
return {}
## Set selected character
func select_character(character_id: String) -> void:
_selected_character_id = character_id
print("[StateManager] Character selected: %s" % character_id)
# Emit signal
character_selected.emit(character_id)
## Get selected character
func get_selected_character() -> Dictionary:
return get_character(_selected_character_id)
## Get selected character ID
func get_selected_character_id() -> String:
return _selected_character_id
## Set character limits
func set_character_limits(limits: Dictionary) -> void:
_character_limits = limits
## Get character limits
func get_character_limits() -> Dictionary:
return _character_limits
## Character Creation Wizard State Management
## Reset wizard state
func reset_wizard() -> void:
_wizard_state = {
"step": 0,
"selected_origin": null,
"selected_class": null,
"character_name": "",
"character_customization": {}
}
print("[StateManager] Wizard state reset")
## Set wizard step
func set_wizard_step(step: int) -> void:
_wizard_state["step"] = step
## Get current wizard step
func get_wizard_step() -> int:
return _wizard_state.get("step", 0)
## Set selected origin
func set_wizard_origin(origin_data: Dictionary) -> void:
_wizard_state["selected_origin"] = origin_data
## Get selected origin
func get_wizard_origin() -> Dictionary:
return _wizard_state.get("selected_origin", {})
## Set selected class
func set_wizard_class(class_data: Dictionary) -> void:
_wizard_state["selected_class"] = class_data
## Get selected class
func get_wizard_class() -> Dictionary:
return _wizard_state.get("selected_class", {})
## Set character name
func set_wizard_name(char_name: String) -> void:
_wizard_state["character_name"] = char_name
## Get character name
func get_wizard_name() -> String:
return _wizard_state.get("character_name", "")
## Set character customization
func set_wizard_customization(customization: Dictionary) -> void:
_wizard_state["character_customization"] = customization
## Get character customization
func get_wizard_customization() -> Dictionary:
return _wizard_state.get("character_customization", {})
## Get complete wizard state
func get_wizard_state() -> Dictionary:
return _wizard_state
## Navigation State Management
## Set current scene
func set_current_scene(scene_path: String) -> void:
if not _current_scene.is_empty():
_scene_history.append(_current_scene)
_current_scene = scene_path
print("[StateManager] Scene changed: %s" % scene_path)
## Get current scene
func get_current_scene() -> String:
return _current_scene
## Navigate back
func navigate_back() -> String:
if _scene_history.is_empty():
return ""
var previous_scene: String = _scene_history.pop_back()
_current_scene = previous_scene
return previous_scene
## Can navigate back
func can_navigate_back() -> bool:
return not _scene_history.is_empty()
## Settings Management
## Get setting value
func get_setting(key: String, default: Variant = null) -> Variant:
return _settings.get(key, default)
## Set setting value
func set_setting(key: String, value: Variant) -> void:
_settings[key] = value
print("[StateManager] Setting updated: %s = %s" % [key, value])
# Auto-save if enabled
if _settings.get("auto_save", true):
save_state()
## Get all settings
func get_settings() -> Dictionary:
return _settings
## Persistence (Save/Load)
## Save state to disk
func save_state() -> void:
var save_data := {
"version": SAVE_VERSION,
"user": _current_user,
"auth_token": _auth_token if _settings.get("remember_login", true) else "",
"session_cookie": http_client.get_session_cookie() if _settings.get("remember_login", true) else "",
"is_authenticated": _is_authenticated if _settings.get("remember_login", true) else false,
"characters": _characters,
"selected_character_id": _selected_character_id,
"character_limits": _character_limits,
"settings": _settings,
"timestamp": Time.get_unix_time_from_system()
}
var file := FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if file == null:
push_error("[StateManager] Failed to open save file for writing: %s" % FileAccess.get_open_error())
return
file.store_string(JSON.stringify(save_data))
file.close()
print("[StateManager] State saved to %s" % SAVE_FILE_PATH)
## Load state from disk
func _load_state() -> void:
if not FileAccess.file_exists(SAVE_FILE_PATH):
print("[StateManager] No save file found, starting fresh")
return
var file := FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
if file == null:
push_error("[StateManager] Failed to open save file for reading: %s" % FileAccess.get_open_error())
return
var json_string := file.get_as_text()
file.close()
var json := JSON.new()
var parse_error := json.parse(json_string)
if parse_error != OK:
push_error("[StateManager] Failed to parse save file")
return
var save_data: Dictionary = json.data
# Check version
if save_data.get("version", 0) != SAVE_VERSION:
print("[StateManager] Save file version mismatch, ignoring")
return
# Restore state
_current_user = save_data.get("user", {})
_auth_token = save_data.get("auth_token", "")
_is_authenticated = save_data.get("is_authenticated", false)
_characters = []
# Convert characters array
var chars_data = save_data.get("characters", [])
for character in chars_data:
_characters.append(character)
_selected_character_id = save_data.get("selected_character_id", "")
_character_limits = save_data.get("character_limits", {})
_settings = save_data.get("settings", _settings)
# Update HTTPClient with token if authenticated
if _is_authenticated and not _auth_token.is_empty():
http_client.set_auth_token(_auth_token)
# Restore session cookie if present
var session_cookie = save_data.get("session_cookie", "")
if _is_authenticated and not session_cookie.is_empty():
http_client.set_session_cookie(session_cookie)
print("[StateManager] State loaded from %s" % SAVE_FILE_PATH)
print("[StateManager] Authenticated: %s, Characters: %d" % [_is_authenticated, _characters.size()])
## Clear save file
func _clear_save_file() -> void:
if FileAccess.file_exists(SAVE_FILE_PATH):
DirAccess.remove_absolute(SAVE_FILE_PATH)
print("[StateManager] Save file cleared")

View File

@@ -0,0 +1 @@
uid://b28q3ngeah5sf

View File

@@ -0,0 +1,25 @@
extends Button
@onready var test_button = $"."
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())

View File

@@ -0,0 +1 @@
uid://c1uv0pqjtun0r

View File

@@ -0,0 +1,121 @@
extends Object
class_name ThemeColors
## Theme Colors
##
## Defines the color palette for Code of Conquest.
## Based on the original web UI color scheme:
## - Dark slate gray backgrounds
## - Gold accents and highlights
## - RPG/fantasy aesthetic
##
## Usage:
## var bg_color = ThemeColors.BACKGROUND_PRIMARY
## button.add_theme_color_override("font_color", ThemeColors.GOLD_ACCENT)
# Primary Background Colors
const BACKGROUND_PRIMARY := Color("#1a1a2e") # Very dark blue-gray
const BACKGROUND_SECONDARY := Color("#16213e") # Slightly lighter blue-gray
const BACKGROUND_TERTIARY := Color("#0f3460") # Accent background
const BACKGROUND_CARD := Color("#1e1e2f") # Card backgrounds
# Text Colors
const TEXT_PRIMARY := Color("#e4e4e7") # Light gray (main text)
const TEXT_SECONDARY := Color("#a1a1aa") # Medium gray (secondary text)
const TEXT_DISABLED := Color("#71717a") # Dark gray (disabled)
const TEXT_INVERTED := Color("#1a1a2e") # Dark text on light bg
# Accent Colors
const GOLD_ACCENT := Color("#d4af37") # Primary gold accent
const GOLD_LIGHT := Color("#f4d03f") # Lighter gold
const GOLD_DARK := Color("#b8930a") # Darker gold
# Status Colors
const SUCCESS := Color("#10b981") # Green (success messages)
const ERROR := Color("#ef4444") # Red (errors)
const WARNING := Color("#f59e0b") # Orange (warnings)
const INFO := Color("#3b82f6") # Blue (info messages)
# HP/Mana/Resource Colors
const HP_COLOR := Color("#dc2626") # Red for HP bars
const MANA_COLOR := Color("#3b82f6") # Blue for mana bars
const STAMINA_COLOR := Color("#10b981") # Green for stamina
# Rarity Colors (for items, etc.)
const RARITY_COMMON := Color("#9ca3af") # Gray
const RARITY_UNCOMMON := Color("#10b981") # Green
const RARITY_RARE := Color("#3b82f6") # Blue
const RARITY_EPIC := Color("#a855f7") # Purple
const RARITY_LEGENDARY := Color("#f59e0b") # Orange/gold
# Border and Divider Colors
const BORDER_DEFAULT := Color("#3f3f46") # Default borders
const BORDER_ACCENT := Color("#d4af37") # Gold borders
const DIVIDER := Color("#27272a") # Subtle dividers
# Interactive States
const HOVER := Color("#d4af37", 0.1) # Gold overlay on hover
const PRESSED := Color("#d4af37", 0.2) # Gold overlay on press
const FOCUSED := Color("#d4af37", 0.15) # Gold overlay on focus
const SELECTED := Color("#d4af37", 0.25) # Gold overlay on selection
# Overlay Colors
const OVERLAY_DARK := Color("#000000", 0.5) # Dark overlay
const OVERLAY_LIGHT := Color("#ffffff", 0.1) # Light overlay
# Shadow Colors
const SHADOW := Color("#000000", 0.3) # Default shadow
const SHADOW_STRONG := Color("#000000", 0.5) # Strong shadow
## Get a color with modified alpha
static func with_alpha(color: Color, alpha: float) -> Color:
var new_color := color
new_color.a = alpha
return new_color
## Lighten a color by a percentage
static func lighten(color: Color, amount: float) -> Color:
return color.lightened(amount)
## Darken a color by a percentage
static func darken(color: Color, amount: float) -> Color:
return color.darkened(amount)
## Mix two colors
static func mix(color1: Color, color2: Color, weight: float = 0.5) -> Color:
return color1.lerp(color2, weight)
## Get rarity color by tier
static func get_rarity_color(tier: int) -> Color:
match tier:
1:
return RARITY_COMMON
2:
return RARITY_UNCOMMON
3:
return RARITY_RARE
4:
return RARITY_EPIC
5:
return RARITY_LEGENDARY
_:
return RARITY_COMMON
## Get status color by type
static func get_status_color(status: String) -> Color:
match status.to_lower():
"success", "ok", "complete":
return SUCCESS
"error", "fail", "failed":
return ERROR
"warning", "warn":
return WARNING
"info", "information":
return INFO
_:
return TEXT_SECONDARY

View File

@@ -0,0 +1 @@
uid://bbbe8eaq2ltgm