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,209 @@
extends Control
## Login Screen Script
##
## Handles user authentication via the backend API.
## Validates input, displays errors, and navigates to character list on success.
# Node references
@onready var email_input: LineEdit = $CenterContainer/LoginCard/MainContainer/ContentVBox/EmailInput
@onready var password_input: LineEdit = $CenterContainer/LoginCard/MainContainer/ContentVBox/PasswordInput
@onready var login_button: Button = $CenterContainer/LoginCard/MainContainer/ContentVBox/LoginButton
@onready var error_label: Label = $CenterContainer/LoginCard/MainContainer/ContentVBox/ErrorLabel
@onready var remember_checkbox: CheckBox = $CenterContainer/LoginCard/MainContainer/ContentVBox/RememberCheckBox
@onready var register_link: Button = $CenterContainer/LoginCard/MainContainer/ContentVBox/BottomLinksVBox/RegisterLink
@onready var forgot_password_link: Button = $CenterContainer/LoginCard/MainContainer/ContentVBox/BottomLinksVBox/ForgotPasswordLink
# Service references
@onready var http_client: Node = get_node("/root/HTTPClient")
@onready var state_manager: Node = get_node("/root/StateManager")
# Internal state
var _is_loading: bool = false
func _ready() -> void:
print("[LoginScreen] Initialized")
# Connect button signals
login_button.pressed.connect(_on_login_button_pressed)
register_link.pressed.connect(_on_register_link_pressed)
forgot_password_link.pressed.connect(_on_forgot_password_link_pressed)
# Connect Enter key to submit
email_input.text_submitted.connect(_on_input_submitted)
password_input.text_submitted.connect(_on_input_submitted)
# Hide error label by default
error_label.visible = false
# Load saved remember setting
remember_checkbox.button_pressed = state_manager.get_setting("remember_login", true)
## Handle login button press
func _on_login_button_pressed() -> void:
# Prevent double-click
if _is_loading:
return
# Get input values
var email := email_input.text.strip_edges()
var password := password_input.text
# Validate inputs
var validation_error := _validate_inputs(email, password)
if not validation_error.is_empty():
_show_error(validation_error)
return
# Start login process
_start_login(email, password)
## Handle Enter key press in input fields
func _on_input_submitted(_text: String = "") -> void:
_on_login_button_pressed()
## Validate email and password
func _validate_inputs(email: String, password: String) -> String:
# Check if email is empty
if email.is_empty():
return "Please enter your email address"
# Check if password is empty
if password.is_empty():
return "Please enter your password"
# Basic email format validation (contains @ and .)
if not email.contains("@") or not email.contains("."):
return "Please enter a valid email address"
# Check minimum password length
if password.length() < 6:
return "Password must be at least 6 characters"
return ""
## Start login process
func _start_login(email: String, password: String) -> void:
_is_loading = true
_hide_error()
# Disable button and show loading state
login_button.disabled = true
login_button.text = "LOGGING IN..."
# Build request payload
var payload := {
"email": email,
"password": password,
"remember_me": remember_checkbox.button_pressed
}
print("[LoginScreen] Attempting login for: %s (remember_me=%s)" % [email, remember_checkbox.button_pressed])
# Make API request
http_client.http_post(
"/api/v1/auth/login",
payload,
_on_login_success,
_on_login_error
)
## Handle successful login response
func _on_login_success(response: APIResponse) -> void:
_is_loading = false
# Re-enable button
login_button.disabled = false
login_button.text = "LOGIN"
# Check if response is actually successful
if not response.is_success():
_on_login_error(response)
return
print("[LoginScreen] Login successful")
# Extract user data from response
# Note: Authentication is cookie-based, so no token in response
var result: Dictionary = response.result if response.result is Dictionary else {}
var user_data: Dictionary = result.get("user", {})
if user_data.is_empty():
_show_error("Invalid response from server")
return
# Update remember setting
state_manager.set_setting("remember_login", remember_checkbox.button_pressed)
# Save session to StateManager (cookie is already set in HTTPClient)
state_manager.set_user_session(user_data)
print("[LoginScreen] User authenticated: %s" % user_data.get("email", "unknown"))
print("[LoginScreen] Session cookie stored, ready for authenticated requests")
# Navigate back to main screen (which will show authenticated UI)
print("[LoginScreen] Navigating to main screen...")
get_tree().change_scene_to_file("res://scenes/main.tscn")
## Handle login error
func _on_login_error(response: APIResponse) -> void:
_is_loading = false
# Re-enable button
login_button.disabled = false
login_button.text = "LOGIN"
# Get error message
var error_message := response.get_error_message()
# Show user-friendly error
if error_message.is_empty():
error_message = "Login failed. Please try again."
# Handle specific error cases
if response.status == 401:
error_message = "Invalid email or password"
elif response.status == 0 or response.status >= 500:
error_message = "Cannot connect to server. Please check your connection."
print("[LoginScreen] Login error: %s (status=%d)" % [error_message, response.status])
_show_error(error_message)
## Show error message
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
## Hide error message
func _hide_error() -> void:
error_label.visible = false
## Handle register link press
func _on_register_link_pressed() -> void:
print("[LoginScreen] Opening registration page in browser")
var register_url := Settings.get_web_url() + "/auth/register"
var error := OS.shell_open(register_url)
if error != OK:
push_error("[LoginScreen] Failed to open browser: %s" % error)
_show_error("Could not open registration page")
## Handle forgot password link press
func _on_forgot_password_link_pressed() -> void:
print("[LoginScreen] Opening forgot password page in browser")
var forgot_url := Settings.get_web_url() + "/auth/forgot-password"
var error := OS.shell_open(forgot_url)
if error != OK:
push_error("[LoginScreen] Failed to open browser: %s" % error)
_show_error("Could not open password reset page")

View File

@@ -0,0 +1 @@
uid://260dpbodarqy

View File

@@ -0,0 +1,241 @@
[gd_scene load_steps=10 format=3 uid="uid://pon554b5gdnu"]
[ext_resource type="Theme" uid="uid://bviqieumdiccr" path="res://assets/themes/main_theme.tres" id="1_8wugm"]
[ext_resource type="Script" uid="uid://260dpbodarqy" path="res://scenes/auth/login.gd" id="1_lg6fp"]
[ext_resource type="Texture2D" uid="uid://cja8kui47qb3d" path="res://assets/ui/main_menu.png" id="3_c4dse"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_card"]
bg_color = Color(0, 0, 0, 0.9882353)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.247059, 0.247059, 0.278431, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input_normal"]
content_margin_left = 12.0
content_margin_top = 8.0
content_margin_right = 12.0
content_margin_bottom = 8.0
bg_color = Color(0.0862745, 0.129412, 0.243137, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.247059, 0.247059, 0.278431, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input_focus"]
content_margin_left = 12.0
content_margin_top = 8.0
content_margin_right = 12.0
content_margin_bottom = 8.0
bg_color = Color(0.0862745, 0.129412, 0.243137, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.831373, 0.686275, 0.215686, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_normal"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.831373, 0.686275, 0.215686, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.721569, 0.576471, 0.0392157, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_pressed"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.721569, 0.576471, 0.0392157, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.721569, 0.576471, 0.0392157, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.956863, 0.815686, 0.247059, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.721569, 0.576471, 0.0392157, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[node name="Login" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_8wugm")
script = ExtResource("1_lg6fp")
[node name="TextureRect" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("3_c4dse")
expand_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_8wugm")
[node name="LoginCard" type="PanelContainer" parent="CenterContainer"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
[node name="MainContainer" type="MarginContainer" parent="CenterContainer/LoginCard"]
layout_mode = 2
theme_override_constants/margin_left = 32
theme_override_constants/margin_top = 32
theme_override_constants/margin_right = 32
theme_override_constants/margin_bottom = 32
[node name="ContentVBox" type="VBoxContainer" parent="CenterContainer/LoginCard/MainContainer"]
layout_mode = 2
theme_override_constants/separation = 16
[node name="TitleLabel" type="Label" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.831373, 0.686275, 0.215686, 1)
theme_override_font_sizes/font_size = 24
text = "Welcome Back"
horizontal_alignment = 1
[node name="Spacer1" type="Control" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="EmailLabel" type="Label" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.894118, 0.894118, 0.905882, 1)
text = "Email"
[node name="EmailInput" type="LineEdit" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
theme_override_colors/font_color = Color(0.894118, 0.894118, 0.905882, 1)
theme_override_colors/font_placeholder_color = Color(0.631373, 0.631373, 0.666667, 1)
theme_override_colors/caret_color = Color(0.831373, 0.686275, 0.215686, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_input_normal")
theme_override_styles/focus = SubResource("StyleBoxFlat_input_focus")
placeholder_text = "Enter your email"
[node name="Spacer2" type="Control" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 12)
layout_mode = 2
[node name="PasswordLabel" type="Label" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.894118, 0.894118, 0.905882, 1)
text = "Password"
[node name="PasswordInput" type="LineEdit" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
theme_override_colors/font_color = Color(0.894118, 0.894118, 0.905882, 1)
theme_override_colors/font_placeholder_color = Color(0.631373, 0.631373, 0.666667, 1)
theme_override_colors/caret_color = Color(0.831373, 0.686275, 0.215686, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_input_normal")
theme_override_styles/focus = SubResource("StyleBoxFlat_input_focus")
placeholder_text = "Enter your password"
secret = true
[node name="Spacer3" type="Control" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 12)
layout_mode = 2
[node name="RememberCheckBox" type="CheckBox" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.894118, 0.894118, 0.905882, 1)
text = "Remember me"
[node name="Spacer4" type="Control" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="ErrorLabel" type="Label" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
visible = false
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
theme_override_colors/font_color = Color(0.937255, 0.266667, 0.266667, 1)
horizontal_alignment = 1
autowrap_mode = 3
[node name="LoginButton" type="Button" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
theme_override_colors/font_color = Color(0.101961, 0.101961, 0.180392, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
text = "LOGIN"
[node name="Spacer5" type="Control" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
[node name="BottomLinksVBox" type="VBoxContainer" parent="CenterContainer/LoginCard/MainContainer/ContentVBox"]
layout_mode = 2
theme_override_constants/separation = 8
alignment = 1
[node name="RegisterLink" type="Button" parent="CenterContainer/LoginCard/MainContainer/ContentVBox/BottomLinksVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.631373, 0.631373, 0.666667, 1)
theme_override_colors/font_hover_color = Color(0.831373, 0.686275, 0.215686, 1)
text = "Don't have an account? Register"
flat = true
[node name="ForgotPasswordLink" type="Button" parent="CenterContainer/LoginCard/MainContainer/ContentVBox/BottomLinksVBox"]
layout_mode = 2
theme_override_colors/font_color = Color(0.631373, 0.631373, 0.666667, 1)
theme_override_colors/font_hover_color = Color(0.831373, 0.686275, 0.215686, 1)
text = "Forgot password?"
flat = true

View File

@@ -0,0 +1,65 @@
[gd_scene load_steps=3 format=3 uid="uid://dqx8k3h5nwc2r"]
[ext_resource type="Theme" uid="uid://bviqieumdiccr" path="res://assets/themes/main_theme.tres" id="1_80ja4"]
[ext_resource type="Script" uid="uid://sxgrib8ck0wx" path="res://scripts/character/character_card.gd" id="1_card"]
[node name="CharacterCard" type="PanelContainer"]
custom_minimum_size = Vector2(0, 120)
size_flags_horizontal = 3
theme = ExtResource("1_80ja4")
script = ExtResource("1_card")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 12
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
layout_mode = 2
[node name="InfoVBox" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 4
[node name="NameLabel" type="Label" parent="MarginContainer/HBoxContainer/InfoVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "Character Name"
[node name="ClassLabel" type="Label" parent="MarginContainer/HBoxContainer/InfoVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Class Name"
[node name="LevelLabel" type="Label" parent="MarginContainer/HBoxContainer/InfoVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 12
text = "Level 1"
[node name="GoldLabel" type="Label" parent="MarginContainer/HBoxContainer/InfoVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 12
text = "0 Gold"
[node name="LocationLabel" type="Label" parent="MarginContainer/HBoxContainer/InfoVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 12
text = "Unknown Location"
[node name="ActionVBox" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 8
alignment = 1
[node name="SelectButton" type="Button" parent="MarginContainer/HBoxContainer/ActionVBox"]
custom_minimum_size = Vector2(100, 36)
layout_mode = 2
text = "Select"
[node name="DeleteButton" type="Button" parent="MarginContainer/HBoxContainer/ActionVBox"]
custom_minimum_size = Vector2(100, 36)
layout_mode = 2
text = "Delete"

View File

@@ -0,0 +1,156 @@
[gd_scene load_steps=5 format=3 uid="uid://csqelun8tcd7y"]
[ext_resource type="Script" uid="uid://x4mt6jwbywsl" path="res://scripts/character/character_list.gd" id="1_list"]
[ext_resource type="Texture2D" uid="uid://cja8kui47qb3d" path="res://assets/ui/main_menu.png" id="2_arrgh"]
[ext_resource type="Theme" uid="uid://bviqieumdiccr" path="res://assets/themes/main_theme.tres" id="3_pfk5o"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mvluj"]
bg_color = Color(0, 0, 0, 0.69411767)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.83137256, 0.6862745, 0.21568628, 1)
[node name="CharacterList" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_list")
[node name="TextureRect" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_arrgh")
expand_mode = 3
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 16
[node name="Header" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="TitleLabel" type="Label" parent="VBoxContainer/Header"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 24
text = "Your Characters"
[node name="TierLabel" type="Label" parent="VBoxContainer/Header"]
layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "Free: 0/1"
horizontal_alignment = 2
[node name="LoadingIndicator" type="CenterContainer" parent="VBoxContainer"]
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/LoadingIndicator"]
layout_mode = 2
[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/LoadingIndicator/VBoxContainer"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
max_value = 0.0
indeterminate = true
editor_preview_indeterminate = false
[node name="Label" type="Label" parent="VBoxContainer/LoadingIndicator/VBoxContainer"]
layout_mode = 2
text = "Loading characters..."
horizontal_alignment = 1
[node name="CharacterScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="CharacterVBox" type="VBoxContainer" parent="VBoxContainer/CharacterScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 12
[node name="ErrorContainer" type="MarginContainer" parent="VBoxContainer"]
visible = false
layout_mode = 2
theme = ExtResource("3_pfk5o")
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 8
[node name="ErrorLabel" type="Label" parent="VBoxContainer/ErrorContainer"]
custom_minimum_size = Vector2(200, 20)
layout_mode = 2
theme_override_colors/font_color = Color(0.9, 0.3, 0.3, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_mvluj")
text = "Error message here"
horizontal_alignment = 1
autowrap_mode = 2
[node name="ActionContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 16
alignment = 1
[node name="CreateButton" type="Button" parent="VBoxContainer/ActionContainer"]
custom_minimum_size = Vector2(180, 44)
layout_mode = 2
text = "Create Character"
[node name="RefreshButton" type="Button" parent="VBoxContainer/ActionContainer"]
custom_minimum_size = Vector2(100, 44)
layout_mode = 2
text = "Refresh"
[node name="EmptyState" type="CenterContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="EmptyState"]
layout_mode = 2
theme_override_constants/separation = 16
[node name="IconLabel" type="Label" parent="EmptyState/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 48
text = "⚔"
horizontal_alignment = 1
[node name="MessageLabel" type="Label" parent="EmptyState/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "No characters yet"
horizontal_alignment = 1
[node name="SubMessageLabel" type="Label" parent="EmptyState/VBoxContainer"]
layout_mode = 2
text = "Create your first character to begin your adventure!"
horizontal_alignment = 1
autowrap_mode = 2
[node name="CreateFirstButton" type="Button" parent="EmptyState/VBoxContainer"]
custom_minimum_size = Vector2(180, 44)
layout_mode = 2
size_flags_horizontal = 4
text = "Create Character"

View File

@@ -0,0 +1,335 @@
# Reusable UI Components
This directory contains reusable UI components for Code of Conquest.
## Available Components
### CustomButton
**Script**: `scripts/components/custom_button.gd`
Enhanced button with multiple visual variants and features.
**Features**:
- Multiple variants (Primary, Secondary, Danger, Success, Ghost)
- Icon support (left or right position)
- Loading state
- Hover effects
- Themed styling
**Usage**:
```gdscript
var btn = CustomButton.new()
btn.text = "Login"
btn.set_variant(CustomButton.Variant.PRIMARY)
btn.button_clicked.connect(_on_login_clicked)
add_child(btn)
```
**Variants**:
- `PRIMARY` - Gold accent button for main actions
- `SECONDARY` - Standard button
- `DANGER` - Red button for destructive actions (delete, etc.)
- `SUCCESS` - Green button for positive actions
- `GHOST` - Transparent/subtle button
### Card
**Script**: `scripts/components/card.gd`
Container with optional header and footer, styled like the web UI cards.
**Features**:
- Optional header with text
- Body content area
- Optional footer with buttons
- Multiple style variants
- Automatic sizing
**Usage**:
```gdscript
var card = Card.new()
card.set_header("Character Details")
var content = Label.new()
content.text = "Character information goes here"
card.add_content(content)
card.set_footer_buttons(["Save", "Cancel"])
card.footer_button_pressed.connect(_on_card_button_pressed)
add_child(card)
```
**Style Variants**:
- `DEFAULT` - Standard card with subtle border
- `HIGHLIGHTED` - Gold border with shadow
- `SUBTLE` - Minimal border
### FormField
**Script**: `scripts/components/form_field.gd`
Form input field with label, validation, and error display.
**Features**:
- Label + input + error message
- Multiple input types (text, email, password, number, phone)
- Built-in validation
- Required field support
- Min/max length validation
- Error state styling
**Usage**:
```gdscript
var email_field = FormField.new()
email_field.set_label("Email")
email_field.set_placeholder("Enter your email")
email_field.input_type = FormField.InputType.EMAIL
email_field.required = true
email_field.value_changed.connect(_on_email_changed)
add_child(email_field)
# Later, validate
if email_field.validate():
var email = email_field.get_value()
# Process email
```
**Input Types**:
- `TEXT` - Plain text
- `EMAIL` - Email validation
- `PASSWORD` - Hidden password
- `NUMBER` - Numeric only
- `PHONE` - Phone number format
## Creating New Components
### 1. Create Script
Create a new GDScript file in `scripts/components/`:
```gdscript
extends Control # or appropriate base class
class_name MyComponent
## MyComponent
##
## Brief description of what this component does
##
## Usage:
## var comp = MyComponent.new()
## comp.some_property = "value"
signal some_signal(data: String)
@export var some_property: String = ""
func _ready() -> void:
_setup_structure()
_apply_styling()
func _setup_structure() -> void:
# Build node hierarchy
pass
func _apply_styling() -> void:
# Apply theme colors and styleboxes
pass
```
### 2. Use Theme Colors
Always use `ThemeColors` constants:
```gdscript
var style = StyleBoxFlat.new()
style.bg_color = ThemeColors.BACKGROUND_CARD
style.border_color = ThemeColors.BORDER_DEFAULT
```
### 3. Make It Configurable
Use `@export` variables for Godot Inspector:
```gdscript
@export var title: String = ""
@export_enum("Small", "Medium", "Large") var size: String = "Medium"
@export var show_icon: bool = true
```
### 4. Emit Signals
For user interactions:
```gdscript
signal item_selected(item_id: String)
signal value_changed(new_value: Variant)
func _on_internal_action():
item_selected.emit("some_id")
```
### 5. Document Usage
Include usage examples in docstring:
```gdscript
## MyComponent
##
## Detailed description of the component.
##
## Usage:
## var comp = MyComponent.new()
## comp.title = "My Title"
## comp.item_selected.connect(_on_item_selected)
## add_child(comp)
```
## Component Patterns
### Separation of Structure and Style
```gdscript
func _ready() -> void:
_setup_structure() # Build node hierarchy
_apply_styling() # Apply theme
func _setup_structure() -> void:
# Create child nodes
# Set up hierarchy
# Connect signals
pass
func _apply_styling() -> void:
# Apply StyleBoxes
# Set colors
# Set fonts
pass
```
### Responsive Design
```gdscript
func _apply_styling() -> void:
# Check platform
var is_mobile = OS.get_name() in ["Android", "iOS"]
if is_mobile:
# Larger touch targets
custom_minimum_size = Vector2(60, 60)
else:
# Desktop sizing
custom_minimum_size = Vector2(40, 40)
```
### Validation Pattern
```gdscript
func validate() -> bool:
var is_valid = true
# Check conditions
if some_condition:
show_error("Error message")
is_valid = false
if is_valid:
clear_error()
return is_valid
```
## Testing Components
Create a test scene `test_components.tscn`:
1. Add each component
2. Test all variants
3. Test all states (normal, hover, pressed, disabled)
4. Test on different screen sizes
5. Test on different platforms
Example test scene structure:
```
Control (root)
├─ VBoxContainer
│ ├─ Label ("Buttons")
│ ├─ HBoxContainer
│ │ ├─ CustomButton (Primary)
│ │ ├─ CustomButton (Secondary)
│ │ ├─ CustomButton (Danger)
│ │ └─ CustomButton (Ghost)
│ ├─ Label ("Cards")
│ ├─ HBoxContainer
│ │ ├─ Card (Default)
│ │ ├─ Card (Highlighted)
│ │ └─ Card (Subtle)
│ └─ Label ("Form Fields")
│ ├─ FormField (Text)
│ ├─ FormField (Email)
│ └─ FormField (Password)
```
## Best Practices
1. **Always use ThemeColors** - Never hardcode colors
2. **Make components reusable** - Avoid scene-specific logic
3. **Use signals for communication** - Don't couple components
4. **Document everything** - Docstrings + usage examples
5. **Test on all platforms** - Desktop, mobile, web
6. **Follow naming conventions** - PascalCase for class names
7. **Export important properties** - Make them editable in Inspector
8. **Validate inputs** - Check types and ranges
9. **Handle edge cases** - Empty strings, null values, etc.
10. **Keep it simple** - One component, one responsibility
## Common Pitfalls
### ❌ Don't hardcode colors
```gdscript
var style = StyleBoxFlat.new()
style.bg_color = Color("#1a1a2e") # BAD
```
### ✅ Use ThemeColors
```gdscript
var style = StyleBoxFlat.new()
style.bg_color = ThemeColors.BACKGROUND_PRIMARY # GOOD
```
### ❌ Don't create nodes in every frame
```gdscript
func _process(delta):
var button = Button.new() # BAD - leaks memory
add_child(button)
```
### ✅ Create once in _ready
```gdscript
var button: Button
func _ready():
button = Button.new()
add_child(button)
```
### ❌ Don't couple components
```gdscript
# In a button component
func _on_pressed():
get_parent().get_node("SomeOtherNode").do_something() # BAD
```
### ✅ Use signals
```gdscript
signal action_requested
func _on_pressed():
action_requested.emit() # GOOD
```
## Resources
- [Godot UI Documentation](https://docs.godotengine.org/en/stable/tutorials/ui/index.html)
- [Control Nodes](https://docs.godotengine.org/en/stable/classes/class_control.html)
- [Themes](https://docs.godotengine.org/en/stable/tutorials/ui/gui_using_theme_editor.html)
- [StyleBox](https://docs.godotengine.org/en/stable/classes/class_stylebox.html)

View File

@@ -0,0 +1,183 @@
[gd_scene load_steps=5 format=3 uid="uid://b8qxqvw3n2k4r"]
[ext_resource type="Script" uid="uid://d100mupmyal5" path="res://scripts/main.gd" id="1_main"]
[ext_resource type="Theme" uid="uid://bviqieumdiccr" path="res://assets/themes/main_theme.tres" id="1_sugp2"]
[ext_resource type="Texture2D" uid="uid://cja8kui47qb3d" path="res://assets/ui/main_menu.png" id="3_sugp2"]
[sub_resource type="GDScript" id="GDScript_0wfyh"]
script/source = "extends Button
class_name CustomButton
## Custom Button Component
##
## Enhanced button with icon support, loading state, and themed styling.
##
## Features:
## - Optional icon (left or right)
## - Loading spinner state
## - Hover effects
## - Disabled state management
## - Multiple visual variants
##
## Usage:
## var btn = CustomButton.new()
## btn.text = \"Login\"
## btn.variant = CustomButton.Variant.PRIMARY
## btn.set_loading(true)
signal button_clicked()
# Button variants
enum Variant {
PRIMARY, # Gold accent button (main actions)
SECONDARY, # Standard button
DANGER, # Red button (destructive actions)
SUCCESS, # Green button (positive actions)
GHOST # Transparent button (subtle actions)
}
# Icon position
enum IconPosition {
LEFT,
RIGHT,
NONE
}
# Export variables (editable in Godot Inspector)
@export var variant: Variant = Variant.SECONDARY
@export var icon_texture: Texture2D = null
@export var icon_position: IconPosition = IconPosition.LEFT
@export var show_loading: bool = false
# Internal nodes (set up in _ready)
#var _icon: TextureRect = null
#var _label: Label = null
#var _spinner: TextureRect = null
func _ready() -> void:
# Apply theme based on variant
# Connect press signal
pressed.connect(_on_button_pressed)
## Set loading state
func set_loading(loading: bool) -> void:
show_loading = loading
disabled = loading
_update_loading_state()
## Internal: Update loading spinner
func _update_loading_state() -> void:
# TODO: Implement loading spinner when scene is set up
if show_loading:
text = \"Loading...\"
else:
# Restore original text
pass
## Internal: Handle button press
func _on_button_pressed() -> void:
if not disabled and not show_loading:
button_clicked.emit()
"
[node name="Main" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_sugp2")
script = ExtResource("1_main")
[node name="BackgroundPanel" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="TextureRect" type="TextureRect" parent="BackgroundPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("3_sugp2")
expand_mode = 4
stretch_mode = 5
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/PanelContainer"]
layout_mode = 2
theme = ExtResource("1_sugp2")
theme_override_constants/margin_left = 15
theme_override_constants/margin_top = 15
theme_override_constants/margin_right = 15
theme_override_constants/margin_bottom = 15
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="TitleLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Code of Conquest"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Spacer1" type="Control" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="WelcomeLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Welcome, Player"
horizontal_alignment = 1
vertical_alignment = 1
[node name="StatusLabel" type="Label" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Loading..."
horizontal_alignment = 1
vertical_alignment = 1
[node name="Spacer2" type="Control" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 16
alignment = 1
[node name="PlayNowButton" type="Button" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Play Now"
script = SubResource("GDScript_0wfyh")
[node name="LogoutButton" type="Button" parent="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Logout"
script = SubResource("GDScript_0wfyh")

View File

@@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3 uid="uid://b1gbhqqbu5aij"]
[ext_resource type="Script" uid="uid://c1uv0pqjtun0r" path="res://scripts/test_services.gd" id="1_slw0k"]
[node name="Control" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 0
offset_right = 55.0
offset_bottom = 40.0
[node name="Button" type="Button" parent="PanelContainer"]
layout_mode = 2
text = "Test API"
script = ExtResource("1_slw0k")