From 3175c98ea32d069ade8a4e9bdf65907f8a915a5c Mon Sep 17 00:00:00 2001 From: moyin <2443444649@qq.com> Date: Fri, 2 Jan 2026 00:58:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor=EF=BC=9A=E5=AE=9E=E7=8E=B0=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 _Core/components/ 和 _Core/utils/ 目录 - 重新组织 scenes/ 目录结构,按功能分类 - 迁移 StringUtils.gd 到新的 _Core/utils/ 位置 - 迁移 AuthScene.gd 到新的 scenes/ui/ 位置 - 添加 AI 文档支持目录 docs/AI_docs/ - 添加开发参考文档 claude.md --- _Core/components/.gitkeep | 2 + _Core/utils/.gitkeep | 2 + _Core/utils/StringUtils.gd | 199 ++++ _Core/utils/StringUtils.gd.uid | 1 + claude.md | 96 ++ docs/AI_docs/.gitkeep | 0 .../quick_reference/troubleshooting.md | 238 ++++ docs/AI_docs/templates/managers.md | 617 ++++++++++ docs/AI_docs/workflows/feature_development.md | 363 ++++++ scenes/Maps/.gitkeep | 2 + scenes/characters/.gitkeep | 2 + scenes/effects/.gitkeep | 2 + scenes/prefabs/.gitkeep | 2 + scenes/prefabs/characters/.gitkeep | 2 + scenes/prefabs/effects/.gitkeep | 2 + scenes/prefabs/items/.gitkeep | 2 + scenes/prefabs/ui/.gitkeep | 2 + scenes/ui/.gitkeep | 2 + scenes/ui/AuthScene.gd | 1032 +++++++++++++++++ scenes/ui/AuthScene.gd.uid | 1 + scenes/ui/LoginWindow.tscn | 561 +++++++++ 21 files changed, 3130 insertions(+) create mode 100644 _Core/components/.gitkeep create mode 100644 _Core/utils/.gitkeep create mode 100644 _Core/utils/StringUtils.gd create mode 100644 _Core/utils/StringUtils.gd.uid create mode 100644 claude.md create mode 100644 docs/AI_docs/.gitkeep create mode 100644 docs/AI_docs/quick_reference/troubleshooting.md create mode 100644 docs/AI_docs/templates/managers.md create mode 100644 docs/AI_docs/workflows/feature_development.md create mode 100644 scenes/Maps/.gitkeep create mode 100644 scenes/characters/.gitkeep create mode 100644 scenes/effects/.gitkeep create mode 100644 scenes/prefabs/.gitkeep create mode 100644 scenes/prefabs/characters/.gitkeep create mode 100644 scenes/prefabs/effects/.gitkeep create mode 100644 scenes/prefabs/items/.gitkeep create mode 100644 scenes/prefabs/ui/.gitkeep create mode 100644 scenes/ui/.gitkeep create mode 100644 scenes/ui/AuthScene.gd create mode 100644 scenes/ui/AuthScene.gd.uid create mode 100644 scenes/ui/LoginWindow.tscn diff --git a/_Core/components/.gitkeep b/_Core/components/.gitkeep new file mode 100644 index 0000000..f1f9fb0 --- /dev/null +++ b/_Core/components/.gitkeep @@ -0,0 +1,2 @@ +# 基础组件实现目录 +# 存放项目的基础组件类 \ No newline at end of file diff --git a/_Core/utils/.gitkeep b/_Core/utils/.gitkeep new file mode 100644 index 0000000..6ccaea4 --- /dev/null +++ b/_Core/utils/.gitkeep @@ -0,0 +1,2 @@ +# 核心工具类目录 +# 存放字符串处理、数学计算等工具类 \ No newline at end of file diff --git a/_Core/utils/StringUtils.gd b/_Core/utils/StringUtils.gd new file mode 100644 index 0000000..b866af6 --- /dev/null +++ b/_Core/utils/StringUtils.gd @@ -0,0 +1,199 @@ +class_name StringUtils + +# 字符串工具类 - 提供常用的字符串处理功能 + +# 验证邮箱格式 +static func is_valid_email(email: String) -> bool: + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + return regex.search(email) != null + +# 验证用户名格式(字母、数字、下划线) +static func is_valid_username(username: String) -> bool: + if username.is_empty() or username.length() > 50: + return false + + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9_]+$") + return regex.search(username) != null + +# 验证密码强度 +static func validate_password_strength(password: String) -> Dictionary: + var result = {"valid": false, "message": "", "strength": 0} + + if password.length() < 8: + result.message = "密码长度至少8位" + return result + + if password.length() > 128: + result.message = "密码长度不能超过128位" + return result + + var has_letter = false + var has_digit = false + var has_special = false + + for i in range(password.length()): + var character = password[i] + if character >= 'a' and character <= 'z' or character >= 'A' and character <= 'Z': + has_letter = true + elif character >= '0' and character <= '9': + has_digit = true + elif character in "!@#$%^&*()_+-=[]{}|;:,.<>?": + has_special = true + + var strength = 0 + if has_letter: + strength += 1 + if has_digit: + strength += 1 + if has_special: + strength += 1 + if password.length() >= 12: + strength += 1 + + result.strength = strength + + if not (has_letter and has_digit): + result.message = "密码必须包含字母和数字" + return result + + result.valid = true + result.message = "密码强度: " + ["弱", "中", "强", "很强"][min(strength - 1, 3)] + return result + +# 截断字符串 +static func truncate(text: String, max_length: int, suffix: String = "...") -> String: + if text.length() <= max_length: + return text + return text.substr(0, max_length - suffix.length()) + suffix + +# 首字母大写 +static func capitalize_first(text: String) -> String: + if text.is_empty(): + return text + return text[0].to_upper() + text.substr(1) + +# 转换为标题格式(每个单词首字母大写) +static func to_title_case(text: String) -> String: + var words = text.split(" ") + var result = [] + for word in words: + if not word.is_empty(): + result.append(capitalize_first(word.to_lower())) + return " ".join(result) + +# 移除HTML标签 +static func strip_html_tags(html: String) -> String: + var regex = RegEx.new() + regex.compile("<[^>]*>") + return regex.sub(html, "", true) + +# 格式化文件大小 +static func format_file_size(bytes: int) -> String: + var units = ["B", "KB", "MB", "GB", "TB"] + var size = float(bytes) + var unit_index = 0 + + while size >= 1024.0 and unit_index < units.size() - 1: + size /= 1024.0 + unit_index += 1 + + if unit_index == 0: + return str(int(size)) + " " + units[unit_index] + else: + return "%.1f %s" % [size, units[unit_index]] + +# 将UTC时间字符串转换为本地时间显示 +static func format_utc_to_local_time(utc_time_str: String) -> String: + # 解析UTC时间字符串 (格式: 2025-12-25T11:23:52.175Z) + var regex = RegEx.new() + regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})") + var result = regex.search(utc_time_str) + + if result == null: + return utc_time_str # 如果解析失败,返回原字符串 + + # 提取时间组件 + var year = int(result.get_string(1)) + var month = int(result.get_string(2)) + var day = int(result.get_string(3)) + var hour = int(result.get_string(4)) + var minute = int(result.get_string(5)) + var second = int(result.get_string(6)) + + # 创建UTC时间字典 + var utc_dict = { + "year": year, + "month": month, + "day": day, + "hour": hour, + "minute": minute, + "second": second + } + + # 转换为Unix时间戳(UTC) + var utc_timestamp = Time.get_unix_time_from_datetime_dict(utc_dict) + + # 获取本地时间(Godot会自动处理时区转换) + var local_dict = Time.get_datetime_dict_from_unix_time(utc_timestamp) + + # 格式化为易读的本地时间 + return "%04d年%02d月%02d日 %02d:%02d:%02d" % [ + local_dict.year, + local_dict.month, + local_dict.day, + local_dict.hour, + local_dict.minute, + local_dict.second + ] + +# 获取相对时间描述(多少分钟后) +static func get_relative_time_until(utc_time_str: String) -> String: + # 解析UTC时间字符串 + var regex = RegEx.new() + regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})") + var result = regex.search(utc_time_str) + + if result == null: + return "时间格式错误" + + # 提取时间组件 + var year = int(result.get_string(1)) + var month = int(result.get_string(2)) + var day = int(result.get_string(3)) + var hour = int(result.get_string(4)) + var minute = int(result.get_string(5)) + var second = int(result.get_string(6)) + + # 创建UTC时间字典 + var utc_dict = { + "year": year, + "month": month, + "day": day, + "hour": hour, + "minute": minute, + "second": second + } + + # 转换为Unix时间戳 + var target_timestamp = Time.get_unix_time_from_datetime_dict(utc_dict) + var current_timestamp = Time.get_unix_time_from_system() + + # 计算时间差(秒) + var diff_seconds = target_timestamp - current_timestamp + + if diff_seconds <= 0: + return "现在可以重试" + elif diff_seconds < 60: + return "%d秒后" % diff_seconds + elif diff_seconds < 3600: + var minutes = int(diff_seconds / 60) + return "%d分钟后" % minutes + else: + var hours = int(diff_seconds / 3600) + var minutes = int((diff_seconds % 3600) / 60) + if minutes > 0: + return "%d小时%d分钟后" % [hours, minutes] + else: + return "%d小时后" % hours \ No newline at end of file diff --git a/_Core/utils/StringUtils.gd.uid b/_Core/utils/StringUtils.gd.uid new file mode 100644 index 0000000..f305e41 --- /dev/null +++ b/_Core/utils/StringUtils.gd.uid @@ -0,0 +1 @@ +uid://bu8onmk6q8wic diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..0b04993 --- /dev/null +++ b/claude.md @@ -0,0 +1,96 @@ +# 🎯 CLAUDE.md - WhaleTown Project Instructions + +## 1. Project Vision & Context +- **Project**: "WhaleTown" - A 2D top-down pixel art RPG. +- **Engine**: Godot 4.2+ (Strictly NO Godot 3.x syntax). +- **Architecture**: Strictly layered: `_Core` (Framework), `Scenes` (Gameplay), `UI` (Interface). +- **Core Principle**: "Signal Up, Call Down". High decoupling via `EventSystem`. + +## 2. 🛠 Command Reference & Setup +- **Input Map (Required Configuration)**: + - `move_left`, `move_right`, `move_up`, `move_down` (WASD / Arrows) + - `interact` (E Key / Space) + - `pause` (ESC) +- **Run Game**: `godot --path .` +- **Run Tests (GUT)**: `godot --headless -s addons/gut/gut_cmdline.gd -gdir=res://tests/ -ginclude_subdirs` +- **Init Structure**: `mkdir -p _Core/managers _Core/systems Scenes/Maps Scenes/Entities Scenes/Components UI/Windows UI/HUD Assets/Sprites tests/unit tests/integration` + +## 3. 📂 File Path Rules (STRICT LOWERCASE) +*Claude: Root folders MUST be lowercase. Scripts and Scenes MUST stay together.* +- **Core Managers**: `_Core/managers/[Name].gd` +- **Core Systems**: `_Core/systems/[Name].gd` +- **Entities**: `Scenes/Entities/[EntityName]/[EntityName].tscn` (Script `.gd` in same folder). +- **Maps**: `Scenes/Maps/[map_name].tscn` +- **Components**: `Scenes/Components/[ComponentName].gd` (Reusable logic nodes). +- **UI Windows**: `UI/Windows/[WindowName].tscn` +- **Tests**: `tests/[unit|integration]/test_[name].gd` (Folder is lowercase `tests`). + +## 4. 📋 Coding Standards (The Law) +- **Type Safety**: ALWAYS use strict static typing: `var speed: float = 100.0`, `func _ready() -> void`. +- **Naming Conventions**: + - `class_name PascalCase` at the top of every script. + - Variables/Functions: `snake_case`. Constants: `SCREAMING_SNAKE_CASE`. + - Private members: Prefix with underscore `_` (e.g., `var _health: int`). +- **Node Access**: Use `%UniqueName` for UI and internal scene components. +- **Signals**: Use "Signal Up, Call Down". Parent calls child methods; Child emits signals. +- **Forbidden Patterns**: + - ❌ NO `yield()` -> Use `await`. + - ❌ NO `get_node()` in `_process` -> Cache with `@onready`. + - ❌ NO Linear Filter -> All Sprite2D/TileMap resources MUST use **Nearest** filter. + +## 5. 🏛 Architecture & Communication +- **EventSystem**: Use `_Core/systems/EventSystem.gd` for cross-module messaging. +- **Event Registry**: Use `class_name EventNames` in `_Core/EventNames.gd`. + ```gdscript + class_name EventNames + const PLAYER_MOVED = "player_moved" + const INTERACT_PRESSED = "interact_pressed" + const NPC_TALKED = "npc_talked" +Singletons: Only GameManager, SceneManager, EventSystem allowed as Autoloads. +Decoupling: Low-level entities MUST NOT reference GameManager. Use events. +6. 🏗 Implementation Details +Player: CharacterBody2D. Must include Camera2D with position_smoothing_enabled = true. +NPC/Interactables: Use Area2D named InteractionArea. Trigger via EventSystem. +TileMap Layers: +Layer 0: Ground (No collision). +Layer 1: Obstacles (Physics Layer enabled). +Layer 2: Decoration (Y-Sort enabled). +Camera: Must auto-calculate limits via TileMap.get_used_rect(). +7. 🧪 Testing Requirements (MANDATORY) +Coverage: Every Manager/System in _Core/ MUST have a GUT test. +Naming: Test files must start with test_ and extend GutTest. +Example: +code +Gdscript +extends GutTest +func test_event_emission(): + var sender = Node.new() + watch_signals(EventSystem) + EventSystem.emit_event(EventNames.PLAYER_MOVED, {}) + assert_signal_emitted(EventSystem, "event_raised") +8. 🧘 The Zen of Development +Juice or Death: Every interaction (UI popup, NPC talk) MUST have a Tween placeholder. +Zero Magic Numbers: All speeds/timers MUST be @export or defined in Config/. +Simplicity: If a function does two things, split it. +Back of the Fence: Hidden logic (like ResponseHandler.gd) must be as clean as the HUD. +9. 📝 Code Template (Entity Pattern) +code +Gdscript +extends CharacterBody2D +class_name Player + +# 1. Exports & Constants +@export var move_speed: float = 200.0 + +# 2. Node References +@onready var sprite: Sprite2D = %Sprite2D + +# 3. Lifecycle +func _physics_process(delta: float) -> void: + _move(delta) + +# 4. Private Methods +func _move(_delta: float) -> void: + var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down") + velocity = dir * move_speed + move_and_slide() \ No newline at end of file diff --git a/docs/AI_docs/.gitkeep b/docs/AI_docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/AI_docs/quick_reference/troubleshooting.md b/docs/AI_docs/quick_reference/troubleshooting.md new file mode 100644 index 0000000..a8a6fc8 --- /dev/null +++ b/docs/AI_docs/quick_reference/troubleshooting.md @@ -0,0 +1,238 @@ +# 🔧 故障排除指南 + +> AI编程助手专用:常见问题的快速解决方案 + +## 🚨 常见错误及解决方案 + +### 1. UID无效警告 + +**错误信息**: +``` +WARNING: scene/resources/resource_format_text.cpp:444 - res://path/to/file.tscn:X - ext_resource, invalid UID: uid://xxxxx - using text path instead: res://path/to/script.gd +``` + +**原因**: 文件移动后,Godot的UID缓存没有更新,导致UID引用失效。 + +**解决方案**: +```gdscript +# 方法1: 移除无效的UID,使用文本路径 +# 将这行: +[ext_resource type="Script" uid="uid://invalid_uid" path="res://path/to/script.gd" id="1_script"] + +# 改为: +[ext_resource type="Script" path="res://path/to/script.gd" id="1_script"] +``` + +**预防措施**: +- 移动文件时使用Godot编辑器的文件系统面板 +- 避免直接在文件系统中移动.gd和.tscn文件 +- 移动文件后重新导入项目 + +### 2. 脚本路径错误 + +**错误信息**: +``` +ERROR: Failed to load script "res://old/path/Script.gd" with error "File not found". +``` + +**解决方案**: +```gdscript +# 检查并更新所有.tscn文件中的脚本路径 +# 使用搜索替换功能批量更新: + +# 旧路径 → 新路径 +"res://UI/Windows/" → "res://scenes/ui/" +"res://Utils/" → "res://_Core/utils/" +"res://Scenes/Maps/" → "res://scenes/maps/" +``` + +### 3. AutoLoad路径错误 + +**错误信息**: +``` +ERROR: Cannot load autoload: res://old/path/Manager.gd +``` + +**解决方案**: +```ini +# 在 project.godot 中更新 AutoLoad 路径 +[autoload] +GameManager="*res://_Core/managers/GameManager.gd" +SceneManager="*res://_Core/managers/SceneManager.gd" +EventSystem="*res://_Core/systems/EventSystem.gd" +``` + +### 4. 资源加载失败 + +**错误信息**: +``` +ERROR: Failed to load resource "res://old/path/resource.png". +``` + +**解决方案**: +```gdscript +# 检查资源路径是否正确 +# 使用 ResourceLoader.exists() 验证路径 +func load_resource_safely(path: String): + if not ResourceLoader.exists(path): + push_error("Resource not found: %s" % path) + return null + + return load(path) +``` + +## 🔍 调试技巧 + +### 1. 路径验证 +```gdscript +# 验证文件是否存在 +func verify_file_exists(file_path: String) -> bool: + return FileAccess.file_exists(file_path) + +# 验证资源是否存在 +func verify_resource_exists(resource_path: String) -> bool: + return ResourceLoader.exists(resource_path) + +# 打印当前工作目录 +func print_current_directory(): + print("Current directory: ", OS.get_executable_path().get_base_dir()) +``` + +### 2. 场景加载调试 +```gdscript +# 安全的场景加载 +func load_scene_safely(scene_path: String) -> PackedScene: + if not ResourceLoader.exists(scene_path): + push_error("Scene file not found: %s" % scene_path) + return null + + var scene = load(scene_path) as PackedScene + if scene == null: + push_error("Failed to load scene: %s" % scene_path) + return null + + return scene +``` + +### 3. 节点引用调试 +```gdscript +# 安全的节点获取 +func get_node_safely(node_path: String) -> Node: + var node = get_node_or_null(node_path) + if node == null: + push_warning("Node not found: %s" % node_path) + return node + +# 检查@onready变量是否正确初始化 +func _ready(): + # 验证所有@onready节点 + if not some_button: + push_error("some_button not found - check node path") + if not some_label: + push_error("some_label not found - check node path") +``` + +## 🛠️ 项目结构问题 + +### 1. 文件移动后的检查清单 +- [ ] 更新.tscn文件中的脚本路径 +- [ ] 更新project.godot中的AutoLoad路径 +- [ ] 更新代码中的硬编码路径 +- [ ] 清理Godot缓存文件 +- [ ] 重新导入项目 + +### 2. 缓存清理命令 +```bash +# Windows PowerShell +Remove-Item -Recurse -Force ".godot\uid_cache.bin" +Remove-Item -Recurse -Force ".godot\global_script_class_cache.cfg" + +# Linux/macOS +rm -rf .godot/uid_cache.bin +rm -rf .godot/global_script_class_cache.cfg +``` + +### 3. 路径常量管理 +```gdscript +# 在 _Core/ProjectPaths.gd 中定义所有路径 +class_name ProjectPaths + +# 核心路径 +const CORE_ROOT = "res://_Core/" +const MANAGERS_PATH = CORE_ROOT + "managers/" +const SYSTEMS_PATH = CORE_ROOT + "systems/" +const UTILS_PATH = CORE_ROOT + "utils/" + +# 场景路径 +const SCENES_ROOT = "res://scenes/" +const UI_PATH = SCENES_ROOT + "ui/" +const MAPS_PATH = SCENES_ROOT + "maps/" + +# 使用示例 +var scene_path = ProjectPaths.UI_PATH + "LoginWindow.tscn" +``` + +## 🎯 性能问题 + +### 1. 内存泄漏检测 +```gdscript +# 监控节点数量 +func _ready(): + print("Initial node count: ", get_tree().get_node_count()) + +func _exit_tree(): + print("Final node count: ", get_tree().get_node_count()) + +# 检查未释放的资源 +func check_resource_leaks(): + print("Resource count: ", ResourceLoader.get_resource_list().size()) +``` + +### 2. 帧率监控 +```gdscript +# 在 _Core/managers/PerformanceManager.gd +extends Node + +var frame_count: int = 0 +var fps_timer: float = 0.0 + +func _process(delta: float): + frame_count += 1 + fps_timer += delta + + if fps_timer >= 1.0: + var fps = frame_count / fps_timer + if fps < 30: + push_warning("Low FPS detected: %.1f" % fps) + + frame_count = 0 + fps_timer = 0.0 +``` + +## 🔧 开发工具问题 + +### 1. Godot编辑器崩溃 +**解决方案**: +1. 备份项目文件 +2. 删除.godot文件夹 +3. 重新打开项目 +4. 重新导入所有资源 + +### 2. 脚本编辑器问题 +**解决方案**: +```gdscript +# 检查脚本语法 +# 使用 Godot 内置的语法检查器 +# 或者在命令行中运行: +# godot --check-only script.gd +``` + +### 3. 场景编辑器问题 +**解决方案**: +- 检查场景文件是否损坏 +- 重新创建有问题的场景 +- 使用版本控制恢复到工作版本 + +--- + +**💡 提示**: 遇到问题时,首先检查Godot的输出面板和调试器,它们通常会提供详细的错误信息和解决建议。 \ No newline at end of file diff --git a/docs/AI_docs/templates/managers.md b/docs/AI_docs/templates/managers.md new file mode 100644 index 0000000..6edd9ca --- /dev/null +++ b/docs/AI_docs/templates/managers.md @@ -0,0 +1,617 @@ +# 🎯 管理器模板集合 + +> AI编程助手专用:各类管理器的标准化代码模板 + +## 🎮 游戏管理器模板 + +### 基础游戏管理器 +```gdscript +# _Core/managers/GameManager.gd +extends Node + +## 游戏状态管理器 +## 负责管理游戏的全局状态、玩家数据和游戏流程 + +# 信号定义 +signal game_state_changed(old_state: GameState, new_state: GameState) +signal player_data_updated(data: Dictionary) +signal game_paused() +signal game_resumed() + +# 游戏状态枚举 +enum GameState { + LOADING, + MENU, + PLAYING, + PAUSED, + GAME_OVER, + SETTINGS +} + +# 常量定义 +const SAVE_FILE_PATH: String = "user://game_save.dat" +const CONFIG_FILE_PATH: String = "res://Config/game_config.json" + +# 导出变量 +@export var debug_mode: bool = false +@export var auto_save_interval: float = 30.0 + +# 公共变量 +var current_state: GameState = GameState.LOADING +var is_paused: bool = false +var game_time: float = 0.0 + +# 玩家数据 +var player_data: Dictionary = { + "level": 1, + "experience": 0, + "coins": 100, + "health": 100, + "max_health": 100, + "energy": 100, + "max_energy": 100 +} + +# 私有变量 +var _auto_save_timer: Timer +var _game_config: Dictionary = {} + +func _ready() -> void: + _initialize_manager() + _setup_auto_save() + _load_game_config() + change_state(GameState.MENU) + +func _process(delta: float) -> void: + if current_state == GameState.PLAYING and not is_paused: + game_time += delta + +## 改变游戏状态 +func change_state(new_state: GameState) -> void: + if current_state == new_state: + return + + var old_state = current_state + _exit_state(old_state) + current_state = new_state + _enter_state(new_state) + + game_state_changed.emit(old_state, new_state) + + if debug_mode: + print("[GameManager] State changed: %s -> %s" % [old_state, new_state]) + +## 暂停游戏 +func pause_game() -> void: + if current_state != GameState.PLAYING: + return + + is_paused = true + get_tree().paused = true + game_paused.emit() + +## 恢复游戏 +func resume_game() -> void: + if not is_paused: + return + + is_paused = false + get_tree().paused = false + game_resumed.emit() + +## 更新玩家数据 +func update_player_data(key: String, value) -> void: + if not player_data.has(key): + push_warning("Unknown player data key: %s" % key) + return + + player_data[key] = value + player_data_updated.emit(player_data) + + if debug_mode: + print("[GameManager] Player data updated: %s = %s" % [key, value]) + +## 获取玩家数据 +func get_player_data(key: String = ""): + if key.is_empty(): + return player_data.duplicate() + + return player_data.get(key, null) + +## 保存游戏数据 +func save_game() -> bool: + var save_data = { + "player_data": player_data, + "game_time": game_time, + "current_state": current_state, + "timestamp": Time.get_unix_time_from_system() + } + + var file = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) + if file == null: + push_error("Failed to open save file for writing") + return false + + file.store_string(JSON.stringify(save_data)) + file.close() + + if debug_mode: + print("[GameManager] Game saved successfully") + + return true + +## 加载游戏数据 +func load_game() -> bool: + if not FileAccess.file_exists(SAVE_FILE_PATH): + if debug_mode: + print("[GameManager] No save file found") + return false + + var file = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) + if file == null: + push_error("Failed to open save file for reading") + return false + + var json_text = file.get_as_text() + file.close() + + var json = JSON.new() + var parse_result = json.parse(json_text) + if parse_result != OK: + push_error("Failed to parse save file JSON") + return false + + var save_data = json.data + + # 恢复玩家数据 + if save_data.has("player_data"): + player_data = save_data.player_data + player_data_updated.emit(player_data) + + # 恢复游戏时间 + if save_data.has("game_time"): + game_time = save_data.game_time + + if debug_mode: + print("[GameManager] Game loaded successfully") + + return true + +# 私有方法 +func _initialize_manager() -> void: + # 设置进程模式为总是处理(即使暂停时也能工作) + process_mode = Node.PROCESS_MODE_ALWAYS + +func _setup_auto_save() -> void: + _auto_save_timer = Timer.new() + add_child(_auto_save_timer) + _auto_save_timer.wait_time = auto_save_interval + _auto_save_timer.timeout.connect(_on_auto_save_timeout) + _auto_save_timer.start() + +func _load_game_config() -> void: + if not FileAccess.file_exists(CONFIG_FILE_PATH): + push_warning("Game config file not found") + return + + var file = FileAccess.open(CONFIG_FILE_PATH, FileAccess.READ) + if file == null: + push_error("Failed to open game config file") + return + + var json_text = file.get_as_text() + file.close() + + var json = JSON.new() + var parse_result = json.parse(json_text) + if parse_result != OK: + push_error("Failed to parse game config JSON") + return + + _game_config = json.data + +func _enter_state(state: GameState) -> void: + match state: + GameState.LOADING: + # 加载游戏资源 + pass + GameState.MENU: + # 显示主菜单 + pass + GameState.PLAYING: + # 开始游戏逻辑 + _auto_save_timer.start() + GameState.PAUSED: + # 暂停游戏 + _auto_save_timer.stop() + GameState.GAME_OVER: + # 游戏结束处理 + save_game() + GameState.SETTINGS: + # 显示设置界面 + pass + +func _exit_state(state: GameState) -> void: + match state: + GameState.PLAYING: + # 退出游戏时自动保存 + save_game() + +func _on_auto_save_timeout() -> void: + if current_state == GameState.PLAYING: + save_game() +``` + +## 🌐 网络管理器模板 + +### HTTP请求管理器 +```gdscript +# _Core/managers/NetworkManager.gd +extends Node + +## 网络请求管理器 +## 负责处理所有HTTP请求和网络通信 + +# 信号定义 +signal request_completed(request_id: String, success: bool, data: Dictionary) +signal connection_status_changed(is_connected: bool) + +# 常量定义 +const BASE_URL: String = "https://api.example.com" +const TIMEOUT_DURATION: float = 10.0 +const MAX_RETRIES: int = 3 + +# 请求状态枚举 +enum RequestStatus { + PENDING, + SUCCESS, + FAILED, + TIMEOUT, + CANCELLED +} + +# 公共变量 +var is_connected: bool = false +var active_requests: Dictionary = {} + +# 私有变量 +var _http_client: HTTPClient +var _request_counter: int = 0 + +func _ready() -> void: + _initialize_network() + _check_connection() + +## 发送GET请求 +func send_get_request(endpoint: String, headers: Dictionary = {}) -> String: + return _send_request(HTTPClient.METHOD_GET, endpoint, "", headers) + +## 发送POST请求 +func send_post_request(endpoint: String, data: Dictionary, headers: Dictionary = {}) -> String: + var json_data = JSON.stringify(data) + return _send_request(HTTPClient.METHOD_POST, endpoint, json_data, headers) + +## 发送PUT请求 +func send_put_request(endpoint: String, data: Dictionary, headers: Dictionary = {}) -> String: + var json_data = JSON.stringify(data) + return _send_request(HTTPClient.METHOD_PUT, endpoint, json_data, headers) + +## 发送DELETE请求 +func send_delete_request(endpoint: String, headers: Dictionary = {}) -> String: + return _send_request(HTTPClient.METHOD_DELETE, endpoint, "", headers) + +## 取消请求 +func cancel_request(request_id: String) -> bool: + if not active_requests.has(request_id): + return false + + var request_data = active_requests[request_id] + if request_data.http_request: + request_data.http_request.cancel_request() + + _cleanup_request(request_id, RequestStatus.CANCELLED) + return true + +## 检查网络连接状态 +func check_connection() -> void: + _check_connection() + +# 私有方法 +func _initialize_network() -> void: + _http_client = HTTPClient.new() + +func _send_request(method: HTTPClient.Method, endpoint: String, data: String, headers: Dictionary) -> String: + var request_id = _generate_request_id() + var full_url = BASE_URL + endpoint + + # 创建HTTP请求节点 + var http_request = HTTPRequest.new() + add_child(http_request) + + # 设置请求超时 + http_request.timeout = TIMEOUT_DURATION + + # 连接完成信号 + http_request.request_completed.connect(_on_request_completed.bind(request_id)) + + # 准备请求头 + var request_headers = ["Content-Type: application/json"] + for key in headers: + request_headers.append("%s: %s" % [key, headers[key]]) + + # 存储请求信息 + active_requests[request_id] = { + "http_request": http_request, + "method": method, + "url": full_url, + "status": RequestStatus.PENDING, + "retry_count": 0, + "start_time": Time.get_time_dict_from_system() + } + + # 发送请求 + var error = http_request.request(full_url, request_headers, method, data) + if error != OK: + push_error("Failed to send HTTP request: %d" % error) + _cleanup_request(request_id, RequestStatus.FAILED) + return "" + + return request_id + +func _generate_request_id() -> String: + _request_counter += 1 + return "req_%d_%d" % [Time.get_time_dict_from_system().hour * 3600 + Time.get_time_dict_from_system().minute * 60 + Time.get_time_dict_from_system().second, _request_counter] + +func _on_request_completed(request_id: String, result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + if not active_requests.has(request_id): + return + + var request_data = active_requests[request_id] + var success = false + var response_data = {} + + # 解析响应 + if response_code >= 200 and response_code < 300: + success = true + var body_text = body.get_string_from_utf8() + + if not body_text.is_empty(): + var json = JSON.new() + var parse_result = json.parse(body_text) + if parse_result == OK: + response_data = json.data + else: + response_data = {"raw_response": body_text} + else: + # 处理错误响应 + var body_text = body.get_string_from_utf8() + response_data = { + "error": "HTTP Error %d" % response_code, + "response_code": response_code, + "raw_response": body_text + } + + # 发送完成信号 + request_completed.emit(request_id, success, response_data) + + # 清理请求 + _cleanup_request(request_id, RequestStatus.SUCCESS if success else RequestStatus.FAILED) + +func _cleanup_request(request_id: String, status: RequestStatus) -> void: + if not active_requests.has(request_id): + return + + var request_data = active_requests[request_id] + + # 移除HTTP请求节点 + if request_data.http_request: + request_data.http_request.queue_free() + + # 从活动请求中移除 + active_requests.erase(request_id) + +func _check_connection() -> void: + # 简单的连接检查(可以改为ping服务器) + var old_status = is_connected + is_connected = true # 这里可以实现实际的连接检查逻辑 + + if old_status != is_connected: + connection_status_changed.emit(is_connected) +``` + +## 🎵 音频管理器模板 + +### 音频系统管理器 +```gdscript +# _Core/managers/AudioManager.gd +extends Node + +## 音频管理器 +## 负责管理游戏中的音乐和音效播放 + +# 信号定义 +signal music_finished() +signal sound_effect_finished(sound_name: String) + +# 音频类型枚举 +enum AudioType { + MUSIC, + SOUND_EFFECT, + UI_SOUND, + VOICE +} + +# 常量定义 +const MUSIC_PATH: String = "res://assets/audio/music/" +const SOUND_PATH: String = "res://assets/audio/sounds/" +const VOICE_PATH: String = "res://assets/audio/voice/" + +# 导出变量 +@export var master_volume: float = 1.0 +@export var music_volume: float = 0.7 +@export var sfx_volume: float = 0.8 +@export var voice_volume: float = 0.9 + +# 音频播放器 +var music_player: AudioStreamPlayer +var sfx_players: Array[AudioStreamPlayer] = [] +var voice_player: AudioStreamPlayer + +# 当前播放状态 +var current_music: String = "" +var is_music_playing: bool = false +var active_sounds: Dictionary = {} + +func _ready() -> void: + _initialize_audio_players() + _load_audio_settings() + +## 播放背景音乐 +func play_music(music_name: String, fade_in: bool = true) -> void: + var music_path = MUSIC_PATH + music_name + ".ogg" + + if not ResourceLoader.exists(music_path): + push_warning("Music file not found: %s" % music_path) + return + + # 停止当前音乐 + if is_music_playing: + stop_music(fade_in) + await get_tree().create_timer(0.5).timeout + + # 加载并播放新音乐 + var audio_stream = load(music_path) + music_player.stream = audio_stream + music_player.volume_db = linear_to_db(music_volume * master_volume) + + if fade_in: + _fade_in_music() + else: + music_player.play() + + current_music = music_name + is_music_playing = true + +## 停止背景音乐 +func stop_music(fade_out: bool = true) -> void: + if not is_music_playing: + return + + if fade_out: + _fade_out_music() + else: + music_player.stop() + is_music_playing = false + current_music = "" + +## 播放音效 +func play_sound_effect(sound_name: String, volume_override: float = -1.0) -> void: + var sound_path = SOUND_PATH + sound_name + ".ogg" + + if not ResourceLoader.exists(sound_path): + push_warning("Sound file not found: %s" % sound_path) + return + + # 获取可用的音效播放器 + var player = _get_available_sfx_player() + if player == null: + push_warning("No available sound effect player") + return + + # 加载并播放音效 + var audio_stream = load(sound_path) + player.stream = audio_stream + + var final_volume = volume_override if volume_override > 0 else sfx_volume + player.volume_db = linear_to_db(final_volume * master_volume) + + player.play() + active_sounds[sound_name] = player + +## 设置主音量 +func set_master_volume(volume: float) -> void: + master_volume = clamp(volume, 0.0, 1.0) + _update_all_volumes() + +## 设置音乐音量 +func set_music_volume(volume: float) -> void: + music_volume = clamp(volume, 0.0, 1.0) + if is_music_playing: + music_player.volume_db = linear_to_db(music_volume * master_volume) + +## 设置音效音量 +func set_sfx_volume(volume: float) -> void: + sfx_volume = clamp(volume, 0.0, 1.0) + _update_sfx_volumes() + +# 私有方法 +func _initialize_audio_players() -> void: + # 创建音乐播放器 + music_player = AudioStreamPlayer.new() + add_child(music_player) + music_player.finished.connect(_on_music_finished) + + # 创建多个音效播放器(支持同时播放多个音效) + for i in range(8): + var sfx_player = AudioStreamPlayer.new() + add_child(sfx_player) + sfx_players.append(sfx_player) + + # 创建语音播放器 + voice_player = AudioStreamPlayer.new() + add_child(voice_player) + +func _get_available_sfx_player() -> AudioStreamPlayer: + for player in sfx_players: + if not player.playing: + return player + return null + +func _fade_in_music() -> void: + music_player.volume_db = linear_to_db(0.01) + music_player.play() + + var tween = create_tween() + tween.tween_method(_set_music_volume_db, 0.01, music_volume * master_volume, 1.0) + +func _fade_out_music() -> void: + var tween = create_tween() + tween.tween_method(_set_music_volume_db, music_volume * master_volume, 0.01, 1.0) + tween.tween_callback(_stop_music_after_fade) + +func _set_music_volume_db(volume: float) -> void: + music_player.volume_db = linear_to_db(volume) + +func _stop_music_after_fade() -> void: + music_player.stop() + is_music_playing = false + current_music = "" + +func _update_all_volumes() -> void: + if is_music_playing: + music_player.volume_db = linear_to_db(music_volume * master_volume) + _update_sfx_volumes() + +func _update_sfx_volumes() -> void: + for player in sfx_players: + if player.playing: + player.volume_db = linear_to_db(sfx_volume * master_volume) + +func _load_audio_settings() -> void: + # 从配置文件加载音频设置 + pass + +func _on_music_finished() -> void: + is_music_playing = false + current_music = "" + music_finished.emit() +``` + +--- + +**🎯 使用说明**: +1. 选择合适的管理器模板 +2. 根据具体需求修改类名和功能 +3. 确保在project.godot中配置为AutoLoad +4. 遵循项目的命名规范和代码风格 +5. 添加必要的错误处理和日志记录 \ No newline at end of file diff --git a/docs/AI_docs/workflows/feature_development.md b/docs/AI_docs/workflows/feature_development.md new file mode 100644 index 0000000..bd81490 --- /dev/null +++ b/docs/AI_docs/workflows/feature_development.md @@ -0,0 +1,363 @@ +# 🚀 功能开发流程 + +> AI编程助手专用:新功能开发的标准化工作流程 + +## 🎯 开发流程概览 + +### 阶段1: 需求分析 → 阶段2: 架构设计 → 阶段3: 代码实现 → 阶段4: 测试验证 → 阶段5: 文档更新 + +--- + +## 📋 阶段1: 需求分析 + +### 1.1 理解需求 +```markdown +**必须明确的问题:** +- 功能的具体作用是什么? +- 涉及哪些用户交互? +- 需要哪些数据和状态管理? +- 与现有功能的关系如何? +``` + +### 1.2 需求分类 +```gdscript +# 功能类型分类 +enum FeatureType { + CORE_SYSTEM, # 核心系统功能 → 放在 _Core/ + GAME_SCENE, # 游戏场景功能 → 放在 scenes/ + UI_COMPONENT, # UI组件功能 → 放在 scenes/ui/ + ASSET_RELATED, # 资源相关功能 → 涉及 assets/ + CONFIG_DRIVEN # 配置驱动功能 → 涉及 Config/ +} +``` + +### 1.3 依赖分析 +- 需要哪些现有管理器? +- 需要创建新的管理器吗? +- 需要新的事件定义吗? +- 需要新的配置文件吗? + +--- + +## 🏗️ 阶段2: 架构设计 + +### 2.1 确定文件位置 +```gdscript +# 根据功能类型确定文件位置 +match feature_type: + FeatureType.CORE_SYSTEM: + # _Core/managers/ 或 _Core/systems/ + var file_path = "_Core/managers/YourManager.gd" + + FeatureType.GAME_SCENE: + # scenes/maps/, scenes/characters/, scenes/effects/ + var file_path = "scenes/characters/YourCharacter.gd" + + FeatureType.UI_COMPONENT: + # scenes/ui/ + var file_path = "scenes/ui/YourWindow.gd" +``` + +### 2.2 设计接口 +```gdscript +# 定义公共接口 +class_name YourFeature + +# 信号定义(对外通信) +signal feature_initialized() +signal feature_state_changed(new_state: String) + +# 公共方法(供其他模块调用) +func initialize(config: Dictionary) -> bool +func get_state() -> String +func cleanup() -> void +``` + +### 2.3 事件设计 +```gdscript +# 在 _Core/EventNames.gd 中添加新事件 +const YOUR_FEATURE_STARTED: String = "your_feature_started" +const YOUR_FEATURE_COMPLETED: String = "your_feature_completed" +const YOUR_FEATURE_ERROR: String = "your_feature_error" +``` + +--- + +## 💻 阶段3: 代码实现 + +### 3.1 创建基础结构 +```gdscript +# 使用标准模板创建文件 +# 参考: docs/AI_docs/templates/components.md + +extends Node # 或其他合适的基类 + +## [功能描述] +## 负责[具体职责] + +# 信号定义 +signal feature_ready() + +# 枚举定义 +enum FeatureState { + UNINITIALIZED, + INITIALIZING, + READY, + ERROR +} + +# 常量定义 +const CONFIG_PATH: String = "res://Config/your_feature_config.json" + +# 导出变量 +@export var debug_mode: bool = false + +# 公共变量 +var current_state: FeatureState = FeatureState.UNINITIALIZED + +# 私有变量 +var _config_data: Dictionary = {} + +func _ready() -> void: + initialize() +``` + +### 3.2 实现核心逻辑 +```gdscript +## 初始化功能 +func initialize() -> bool: + if current_state != FeatureState.UNINITIALIZED: + push_warning("Feature already initialized") + return false + + current_state = FeatureState.INITIALIZING + + # 加载配置 + if not _load_config(): + current_state = FeatureState.ERROR + return false + + # 连接事件 + _connect_events() + + # 执行初始化逻辑 + _perform_initialization() + + current_state = FeatureState.READY + feature_ready.emit() + return true + +func _load_config() -> bool: + # 配置加载逻辑 + return true + +func _connect_events() -> void: + # 事件连接逻辑 + EventSystem.connect_event("related_event", _on_related_event) + +func _perform_initialization() -> void: + # 具体初始化逻辑 + pass +``` + +### 3.3 错误处理 +```gdscript +func _handle_error(error_message: String) -> void: + push_error("[YourFeature] %s" % error_message) + current_state = FeatureState.ERROR + + # 发送错误事件 + EventSystem.emit_event(EventNames.YOUR_FEATURE_ERROR, { + "message": error_message, + "timestamp": Time.get_unix_time_from_system() + }) +``` + +--- + +## 🧪 阶段4: 测试验证 + +### 4.1 创建测试文件 +```gdscript +# tests/unit/test_your_feature.gd +extends "res://addons/gut/test.gd" + +## YourFeature 单元测试 + +var your_feature: YourFeature + +func before_each(): + your_feature = preload("res://_Core/managers/YourFeature.gd").new() + add_child(your_feature) + +func after_each(): + your_feature.queue_free() + +func test_initialization(): + # 测试初始化 + var result = your_feature.initialize() + assert_true(result, "Feature should initialize successfully") + assert_eq(your_feature.current_state, YourFeature.FeatureState.READY) + +func test_error_handling(): + # 测试错误处理 + # 模拟错误条件 + pass +``` + +### 4.2 集成测试 +```gdscript +# tests/integration/test_your_feature_integration.gd +extends "res://addons/gut/test.gd" + +## YourFeature 集成测试 + +func test_feature_with_event_system(): + # 测试与事件系统的集成 + var event_received = false + + EventSystem.connect_event("your_feature_started", func(data): event_received = true) + + # 触发功能 + # 验证事件是否正确发送 + assert_true(event_received, "Event should be emitted") +``` + +### 4.3 性能测试 +```gdscript +# tests/performance/test_your_feature_performance.gd +extends "res://addons/gut/test.gd" + +## YourFeature 性能测试 + +func test_initialization_performance(): + var start_time = Time.get_time_dict_from_system() + + # 执行功能 + your_feature.initialize() + + var end_time = Time.get_time_dict_from_system() + var duration = _calculate_duration(start_time, end_time) + + # 验证性能要求(例如:初始化应在100ms内完成) + assert_lt(duration, 0.1, "Initialization should complete within 100ms") +``` + +--- + +## 📚 阶段5: 文档更新 + +### 5.1 更新API文档 +```markdown +# 在 docs/AI_docs/quick_reference/api_reference.md 中添加 + +## YourFeature API + +### 初始化 +```gdscript +var feature = YourFeature.new() +feature.initialize(config_dict) +``` + +### 主要方法 +- `initialize(config: Dictionary) -> bool` - 初始化功能 +- `get_state() -> FeatureState` - 获取当前状态 +- `cleanup() -> void` - 清理资源 + +### 事件 +- `feature_ready` - 功能准备就绪 +- `feature_state_changed(new_state)` - 状态改变 +``` + +### 5.2 更新使用示例 +```gdscript +# 在 docs/AI_docs/quick_reference/code_snippets.md 中添加 + +## YourFeature 使用示例 + +### 基本使用 +```gdscript +# 创建和初始化 +var feature = YourFeature.new() +add_child(feature) + +# 连接信号 +feature.feature_ready.connect(_on_feature_ready) + +# 初始化 +var config = {"setting1": "value1"} +feature.initialize(config) + +func _on_feature_ready(): + print("Feature is ready to use") +``` +``` + +### 5.3 更新架构文档 +```markdown +# 在 docs/AI_docs/architecture_guide.md 中更新 + +## 新增功能: YourFeature + +### 位置 +- 文件路径: `_Core/managers/YourFeature.gd` +- AutoLoad: 是/否 +- 依赖: EventSystem, ConfigManager + +### 职责 +- 负责[具体职责描述] +- 管理[相关数据/状态] +- 提供[对外接口] +``` + +--- + +## ✅ 开发检查清单 + +### 代码质量检查 +- [ ] 遵循命名规范(PascalCase类名,snake_case变量名) +- [ ] 所有变量和函数都有类型注解 +- [ ] 添加了适当的注释和文档字符串 +- [ ] 实现了错误处理和边界检查 +- [ ] 使用事件系统进行模块间通信 + +### 架构一致性检查 +- [ ] 文件放在正确的目录中 +- [ ] 如果是管理器,已配置AutoLoad +- [ ] 事件名称已添加到EventNames.gd +- [ ] 配置文件已放在Config/目录 +- [ ] 遵循项目的架构原则 + +### 测试覆盖检查 +- [ ] 编写了单元测试 +- [ ] 编写了集成测试(如果需要) +- [ ] 编写了性能测试(如果是核心功能) +- [ ] 所有测试都能通过 +- [ ] 测试覆盖了主要功能和边界情况 + +### 文档更新检查 +- [ ] 更新了API参考文档 +- [ ] 添加了使用示例 +- [ ] 更新了架构文档 +- [ ] 更新了相关的工作流程文档 + +--- + +## 🔄 迭代优化 + +### 代码审查要点 +1. **功能完整性**: 是否满足所有需求? +2. **性能表现**: 是否存在性能瓶颈? +3. **错误处理**: 是否处理了所有可能的错误情况? +4. **代码可读性**: 代码是否清晰易懂? +5. **测试覆盖**: 测试是否充分? + +### 持续改进 +- 收集用户反馈 +- 监控性能指标 +- 定期重构优化 +- 更新文档和示例 + +--- + +**🎯 记住**: 这个流程确保了功能开发的质量和一致性。严格遵循每个阶段的要求,将大大提高开发效率和代码质量。 \ No newline at end of file diff --git a/scenes/Maps/.gitkeep b/scenes/Maps/.gitkeep new file mode 100644 index 0000000..c8f3ac4 --- /dev/null +++ b/scenes/Maps/.gitkeep @@ -0,0 +1,2 @@ +# 地图场景目录 +# 存放游戏关卡、世界地图等地图场景 \ No newline at end of file diff --git a/scenes/characters/.gitkeep b/scenes/characters/.gitkeep new file mode 100644 index 0000000..c6ef796 --- /dev/null +++ b/scenes/characters/.gitkeep @@ -0,0 +1,2 @@ +# 人物场景目录 +# 存放角色、NPC、敌人等人物相关场景 \ No newline at end of file diff --git a/scenes/effects/.gitkeep b/scenes/effects/.gitkeep new file mode 100644 index 0000000..48501db --- /dev/null +++ b/scenes/effects/.gitkeep @@ -0,0 +1,2 @@ +# 特效场景目录 +# 存放粒子效果、动画等特效场景 \ No newline at end of file diff --git a/scenes/prefabs/.gitkeep b/scenes/prefabs/.gitkeep new file mode 100644 index 0000000..afb3d03 --- /dev/null +++ b/scenes/prefabs/.gitkeep @@ -0,0 +1,2 @@ +# 预制体组件目录 +# 存放可复用的预制体组件 \ No newline at end of file diff --git a/scenes/prefabs/characters/.gitkeep b/scenes/prefabs/characters/.gitkeep new file mode 100644 index 0000000..03176b7 --- /dev/null +++ b/scenes/prefabs/characters/.gitkeep @@ -0,0 +1,2 @@ +# 角色预制体目录 +# 存放可复用的角色组件预制体 \ No newline at end of file diff --git a/scenes/prefabs/effects/.gitkeep b/scenes/prefabs/effects/.gitkeep new file mode 100644 index 0000000..2d4709c --- /dev/null +++ b/scenes/prefabs/effects/.gitkeep @@ -0,0 +1,2 @@ +# 特效预制体目录 +# 存放可复用的特效组件预制体 \ No newline at end of file diff --git a/scenes/prefabs/items/.gitkeep b/scenes/prefabs/items/.gitkeep new file mode 100644 index 0000000..55f90aa --- /dev/null +++ b/scenes/prefabs/items/.gitkeep @@ -0,0 +1,2 @@ +# 物品预制体目录 +# 存放可复用的物品组件预制体 \ No newline at end of file diff --git a/scenes/prefabs/ui/.gitkeep b/scenes/prefabs/ui/.gitkeep new file mode 100644 index 0000000..9371238 --- /dev/null +++ b/scenes/prefabs/ui/.gitkeep @@ -0,0 +1,2 @@ +# UI预制体目录 +# 存放可复用的UI组件预制体 \ No newline at end of file diff --git a/scenes/ui/.gitkeep b/scenes/ui/.gitkeep new file mode 100644 index 0000000..6f9d732 --- /dev/null +++ b/scenes/ui/.gitkeep @@ -0,0 +1,2 @@ +# UI界面场景目录 +# 存放菜单、HUD、对话框等UI界面场景 \ No newline at end of file diff --git a/scenes/ui/AuthScene.gd b/scenes/ui/AuthScene.gd new file mode 100644 index 0000000..485da13 --- /dev/null +++ b/scenes/ui/AuthScene.gd @@ -0,0 +1,1032 @@ +extends Control + +# 信号定义 +signal login_success(username: String) + +# UI节点引用 +@onready var background_image: TextureRect = $BackgroundImage +@onready var login_panel: Panel = $CenterContainer/LoginPanel +@onready var register_panel: Panel = $CenterContainer/RegisterPanel +@onready var title_label: Label = $CenterContainer/LoginPanel/VBoxContainer/TitleLabel +@onready var subtitle_label: Label = $CenterContainer/LoginPanel/VBoxContainer/SubtitleLabel +@onready var whale_frame: TextureRect = $WhaleFrame + +# 登录表单 +@onready var login_username: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameInput +@onready var login_password: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordInput +@onready var login_verification: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationInputContainer/VerificationInput +@onready var login_username_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var login_password_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var login_verification_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationLabelContainer/VerificationError +@onready var password_container: VBoxContainer = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer +@onready var verification_container: VBoxContainer = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer +@onready var get_code_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationInputContainer/GetCodeBtn +@onready var main_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/MainButton +@onready var login_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/ButtonContainer/LoginBtn +@onready var forgot_password_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/ForgotPassword +@onready var register_link_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/RegisterLink + +# 注册表单 +@onready var register_username: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameInput +@onready var register_email: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailInput +@onready var register_password: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordInput +@onready var register_confirm: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmInput +@onready var verification_input: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/VerificationInput +@onready var send_code_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/SendCodeBtn +@onready var register_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/RegisterBtn +@onready var to_login_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/ToLoginBtn + +# 错误提示标签 +@onready var register_username_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var register_email_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer/EmailError +@onready var register_password_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var register_confirm_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer/ConfirmError +@onready var verification_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer/VerificationError + +# Toast消息节点 +@onready var toast_container: Control = $ToastContainer + +# Toast管理 +var active_toasts: Array = [] +var toast_counter: int = 0 + +# 验证码状态 +var verification_codes_sent: Dictionary = {} +var code_cooldown: float = 60.0 +var cooldown_timer: Timer = null +var current_email: String = "" + +# 登录模式枚举 +enum LoginMode { + PASSWORD, # 密码登录模式 + VERIFICATION # 验证码登录模式 +} + +# 当前登录模式 +var current_login_mode: LoginMode = LoginMode.PASSWORD + +# 网络请求管理 +var active_request_ids: Array = [] + +func _ready(): + connect_signals() + show_login_panel() + update_login_mode_ui() # 初始化登录模式UI + await get_tree().process_frame + print("认证系统已加载") + test_network_connection() + +func test_network_connection(): + print("=== 测试网络连接 ===") + + var request_id = NetworkManager.get_app_status(_on_network_test_response) + if request_id != "": + active_request_ids.append(request_id) + print("网络测试请求已发送,ID: ", request_id) + else: + print("网络连接测试失败") +func connect_signals(): + # 主要按钮 + main_btn.pressed.connect(_on_main_button_pressed) + + # 登录界面按钮 + login_btn.pressed.connect(_on_login_pressed) + forgot_password_btn.pressed.connect(_on_forgot_password_pressed) + register_link_btn.pressed.connect(_on_register_link_pressed) + get_code_btn.pressed.connect(_on_get_login_code_pressed) + + # 注册界面按钮 + register_btn.pressed.connect(_on_register_pressed) + to_login_btn.pressed.connect(_on_to_login_pressed) + send_code_btn.pressed.connect(_on_send_code_pressed) + + # 回车键登录 + login_password.text_submitted.connect(_on_login_enter) + + # 登录表单失焦验证 + login_username.focus_exited.connect(_on_login_username_focus_exited) + login_password.focus_exited.connect(_on_login_password_focus_exited) + login_verification.focus_exited.connect(_on_login_verification_focus_exited) + + # 注册表单失焦验证 + register_username.focus_exited.connect(_on_register_username_focus_exited) + register_email.focus_exited.connect(_on_register_email_focus_exited) + register_password.focus_exited.connect(_on_register_password_focus_exited) + register_confirm.focus_exited.connect(_on_register_confirm_focus_exited) + verification_input.focus_exited.connect(_on_verification_focus_exited) + + # 实时输入验证 + register_username.text_changed.connect(_on_register_username_text_changed) + register_email.text_changed.connect(_on_register_email_text_changed) + register_password.text_changed.connect(_on_register_password_text_changed) + register_confirm.text_changed.connect(_on_register_confirm_text_changed) + verification_input.text_changed.connect(_on_verification_text_changed) + +func show_login_panel(): + login_panel.visible = true + register_panel.visible = false + login_username.grab_focus() + +func show_register_panel(): + login_panel.visible = false + register_panel.visible = true + register_username.grab_focus() + +# 更新登录模式UI +func update_login_mode_ui(): + if current_login_mode == LoginMode.PASSWORD: + # 密码登录模式 + login_btn.text = "验证码登录" + forgot_password_btn.text = "忘记密码" + + # 显示密码输入框,隐藏验证码输入框 + password_container.visible = true + verification_container.visible = false + + # 清空验证码输入框和错误提示 + login_verification.text = "" + hide_field_error(login_verification_error) + + else: # VERIFICATION mode + # 验证码登录模式 + login_btn.text = "密码登录" + forgot_password_btn.text = "获取验证码" + + # 隐藏密码输入框,显示验证码输入框 + password_container.visible = false + verification_container.visible = true + + # 清空密码输入框和错误提示 + login_password.text = "" + hide_field_error(login_password_error) + # 这里需要根据实际UI结构调整 + +# 切换登录模式 +func toggle_login_mode(): + if current_login_mode == LoginMode.PASSWORD: + current_login_mode = LoginMode.VERIFICATION + else: + current_login_mode = LoginMode.PASSWORD + + update_login_mode_ui() + + # 清空输入框 + login_username.text = "" + login_password.text = "" + hide_field_error(login_username_error) + hide_field_error(login_password_error) + +# ============ 按钮事件处理 ============ + +func _on_main_button_pressed(): + # 根据当前登录模式执行不同的登录逻辑 + if current_login_mode == LoginMode.PASSWORD: + _execute_password_login() + else: + _execute_verification_login() + +func _execute_password_login(): + if not validate_login_form(): + return + + var username = login_username.text.strip_edges() + var password = login_password.text + + show_loading(main_btn, "登录中...") + show_toast('正在验证登录信息...', true) + + var request_id = NetworkManager.login(username, password, _on_login_response) + if request_id != "": + active_request_ids.append(request_id) + else: + restore_button(main_btn, "进入小镇") + show_toast('网络请求失败', false) + +func _execute_verification_login(): + var identifier = login_username.text.strip_edges() + var verification_code = login_verification.text.strip_edges() + + if identifier.is_empty(): + show_field_error(login_username_error, "请输入用户名/手机/邮箱") + login_username.grab_focus() + return + + if verification_code.is_empty(): + show_field_error(login_verification_error, "请输入验证码") + login_verification.grab_focus() + return + + show_loading(main_btn, "登录中...") + show_toast('正在验证验证码...', true) + + var request_id = NetworkManager.verification_code_login(identifier, verification_code, _on_verification_login_response) + if request_id != "": + active_request_ids.append(request_id) + else: + restore_button(main_btn, "进入小镇") + show_toast('网络请求失败', false) + +func _on_login_pressed(): + # 现在这个按钮用于切换登录模式 + toggle_login_mode() + +func _on_register_pressed(): + print("注册按钮被点击") + + if not validate_register_form(): + print("注册表单验证失败") + show_toast('请检查并完善注册信息', false) + return + + print("注册表单验证通过,开始注册流程") + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var verification_code = verification_input.text.strip_edges() + + show_loading(register_btn, "注册中...") + show_toast('正在创建账户...', true) + + # 直接调用注册接口,让服务器端处理验证码验证 + send_register_request(username, email, password, verification_code) + +func _on_send_code_pressed(): + var email = register_email.text.strip_edges() + + var email_validation = validate_email(email) + if not email_validation.valid: + show_toast(email_validation.message, false) + register_email.grab_focus() + return + + hide_field_error(register_email_error) + + # 检查冷却时间 + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + + if verification_codes_sent.has(email): + var email_data = verification_codes_sent[email] + if email_data.sent and (current_timestamp - email_data.time) < code_cooldown: + var remaining = code_cooldown - (current_timestamp - email_data.time) + show_toast('该邮箱请等待 %d 秒后再次发送' % remaining, false) + return + + if current_email != email: + stop_current_cooldown() + current_email = email + + if not verification_codes_sent.has(email): + verification_codes_sent[email] = {} + + verification_codes_sent[email].sent = true + verification_codes_sent[email].time = current_timestamp + start_cooldown_timer(email) + + var request_id = NetworkManager.send_email_verification(email, _on_send_code_response) + if request_id != "": + active_request_ids.append(request_id) + else: + show_toast('网络请求失败', false) + reset_verification_button() + +func _on_register_link_pressed(): + show_register_panel() + +func _on_get_login_code_pressed(): + var identifier = login_username.text.strip_edges() + + if identifier.is_empty(): + show_field_error(login_username_error, "请先输入用户名/手机/邮箱") + login_username.grab_focus() + return + + show_loading(get_code_btn, "发送中...") + show_toast('正在发送登录验证码...', true) + + var request_id = NetworkManager.send_login_verification_code(identifier, _on_send_login_code_response) + if request_id != "": + active_request_ids.append(request_id) + else: + restore_button(get_code_btn, "获取验证码") + show_toast('网络请求失败', false) + +func _on_to_login_pressed(): + show_login_panel() + +func _on_login_enter(_text: String): + _on_login_pressed() + +func _on_forgot_password_pressed(): + var identifier = login_username.text.strip_edges() + + if identifier.is_empty(): + show_toast('请先输入邮箱或手机号', false) + login_username.grab_focus() + return + + if not is_valid_email(identifier) and not is_valid_phone(identifier): + show_toast('请输入有效的邮箱或手机号', false) + login_username.grab_focus() + return + + if current_login_mode == LoginMode.PASSWORD: + # 密码登录模式:发送密码重置验证码 + show_loading(forgot_password_btn, "发送中...") + show_toast('正在发送密码重置验证码...', true) + + var request_id = NetworkManager.forgot_password(identifier, _on_forgot_password_response) + if request_id != "": + active_request_ids.append(request_id) + else: + restore_button(forgot_password_btn, "忘记密码") + show_toast('网络请求失败', false) + else: + # 验证码登录模式:发送登录验证码 + show_loading(forgot_password_btn, "发送中...") + show_toast('正在发送登录验证码...', true) + + var request_id = NetworkManager.send_login_verification_code(identifier, _on_send_login_code_response) + if request_id != "": + active_request_ids.append(request_id) + else: + restore_button(forgot_password_btn, "获取验证码") + show_toast('网络请求失败', false) +# ============ 网络响应处理 ============ + +func send_register_request(username: String, email: String, password: String, verification_code: String = ""): + var request_id = NetworkManager.register(username, password, username, email, verification_code, _on_register_response) + if request_id != "": + active_request_ids.append(request_id) + else: + show_toast('网络请求失败', false) + restore_button(register_btn, "注册") + +func _on_network_test_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 网络测试响应处理 ===") + print("成功: ", success) + print("数据: ", data) + + var result = ResponseHandler.handle_network_test_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + +func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 登录响应处理 ===") + print("成功: ", success) + print("数据: ", data) + print("错误信息: ", error_info) + + restore_button(main_btn, "进入小镇") + + var result = ResponseHandler.handle_login_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + + if result.success: + var username = login_username.text.strip_edges() + if data.has("data") and data.data.has("user") and data.data.user.has("username"): + username = data.data.user.username + + login_username.text = "" + login_password.text = "" + hide_field_error(login_username_error) + hide_field_error(login_password_error) + + await get_tree().create_timer(1.0).timeout + login_success.emit(username) + +func _on_send_code_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 发送验证码响应处理 ===") + print("成功: ", success) + print("数据: ", data) + + var result = ResponseHandler.handle_send_verification_code_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + + if not result.success: + reset_verification_button() + +func _on_send_login_code_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 发送登录验证码响应处理 ===") + print("成功: ", success) + print("数据: ", data) + + # 恢复按钮状态 + if current_login_mode == LoginMode.PASSWORD: + restore_button(forgot_password_btn, "忘记密码") + else: + restore_button(forgot_password_btn, "获取验证码") + restore_button(get_code_btn, "获取验证码") + + var result = ResponseHandler.handle_send_login_code_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + +func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 验证码登录响应处理 ===") + print("成功: ", success) + print("数据: ", data) + print("错误信息: ", error_info) + + restore_button(main_btn, "进入小镇") + + var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + + if result.success: + var username = login_username.text.strip_edges() + if data.has("data") and data.data.has("user") and data.data.user.has("username"): + username = data.data.user.username + + login_username.text = "" + hide_field_error(login_username_error) + + await get_tree().create_timer(1.0).timeout + login_success.emit(username) + +func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 忘记密码响应处理 ===") + print("成功: ", success) + print("数据: ", data) + + restore_button(forgot_password_btn, "忘记密码") + + # 使用通用的发送验证码响应处理 + var result = ResponseHandler.handle_send_login_code_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + + if result.success: + show_toast("密码重置验证码已发送,请查收邮件", true) + +func _on_register_response(success: bool, data: Dictionary, error_info: Dictionary): + print("=== 注册响应处理 ===") + print("成功: ", success) + print("数据: ", data) + + restore_button(register_btn, "注册") + + var result = ResponseHandler.handle_register_response(success, data, error_info) + + if result.should_show_toast: + show_toast(result.message, result.success) + + if result.success: + clear_register_form() + show_login_panel() + login_username.text = register_username.text.strip_edges() # 使用注册时的用户名 +# ============ 验证码冷却管理 ============ + +func start_cooldown_timer(email: String): + if cooldown_timer != null: + cooldown_timer.queue_free() + + current_email = email + + send_code_btn.disabled = true + send_code_btn.text = "重新发送(60)" + + cooldown_timer = Timer.new() + add_child(cooldown_timer) + cooldown_timer.wait_time = 1.0 + cooldown_timer.timeout.connect(_on_cooldown_timer_timeout) + cooldown_timer.start() + +func _on_cooldown_timer_timeout(): + var input_email = register_email.text.strip_edges() + + if input_email != current_email: + stop_current_cooldown() + return + + if verification_codes_sent.has(current_email): + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var email_data = verification_codes_sent[current_email] + var remaining = code_cooldown - (current_timestamp - email_data.time) + + if remaining > 0: + send_code_btn.text = "重新发送(%d)" % remaining + else: + send_code_btn.text = "重新发送" + send_code_btn.disabled = false + + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + current_email = "" + +func stop_current_cooldown(): + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + + send_code_btn.disabled = false + send_code_btn.text = "发送验证码" + current_email = "" + +func reset_verification_button(): + if current_email != "" and verification_codes_sent.has(current_email): + verification_codes_sent[current_email].sent = false + + stop_current_cooldown() + +func clear_register_form(): + register_username.text = "" + register_email.text = "" + register_password.text = "" + register_confirm.text = "" + verification_input.text = "" + + stop_current_cooldown() + verification_codes_sent.clear() + + hide_field_error(register_username_error) + hide_field_error(register_email_error) + hide_field_error(register_password_error) + hide_field_error(register_confirm_error) + hide_field_error(verification_error) + +# ============ Toast消息系统 ============ + +# 检测文本是否包含中文字符 +func contains_chinese(text: String) -> bool: + for i in range(text.length()): + var char_code = text.unicode_at(i) + # 中文字符的Unicode范围 + if (char_code >= 0x4E00 and char_code <= 0x9FFF) or \ + (char_code >= 0x3400 and char_code <= 0x4DBF) or \ + (char_code >= 0x20000 and char_code <= 0x2A6DF): + return true + return false + +func show_toast(message: String, is_success: bool = true): + print("显示Toast消息: ", message, " 成功: ", is_success) + + if toast_container == null: + print("错误: toast_container 节点不存在") + return + + # 异步创建Toast实例 + create_toast_instance(message, is_success) + +func create_toast_instance(message: String, is_success: bool): + toast_counter += 1 + + # Web平台字体处理 + var is_web = OS.get_name() == "Web" + + # 1. 创建Toast Panel(方框UI) + var toast_panel = Panel.new() + toast_panel.name = "Toast_" + str(toast_counter) + + # 设置Toast样式 + var style = StyleBoxFlat.new() + if is_success: + style.bg_color = Color(0.15, 0.7, 0.15, 0.95) + style.border_color = Color(0.2, 0.9, 0.2, 0.9) + else: + style.bg_color = Color(0.7, 0.15, 0.15, 0.95) + style.border_color = Color(0.9, 0.2, 0.2, 0.9) + + style.border_width_left = 3 + style.border_width_top = 3 + style.border_width_right = 3 + style.border_width_bottom = 3 + style.corner_radius_top_left = 12 + style.corner_radius_top_right = 12 + style.corner_radius_bottom_left = 12 + style.corner_radius_bottom_right = 12 + style.shadow_color = Color(0, 0, 0, 0.3) + style.shadow_size = 4 + style.shadow_offset = Vector2(2, 2) + + toast_panel.add_theme_stylebox_override("panel", style) + + # 设置Toast基本尺寸 + var toast_width = 320 + toast_panel.size = Vector2(toast_width, 60) + + # 2. 创建VBoxContainer + var vbox = VBoxContainer.new() + vbox.add_theme_constant_override("separation", 0) + vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + vbox.alignment = BoxContainer.ALIGNMENT_CENTER + + # 3. 创建CenterContainer + var center_container = CenterContainer.new() + center_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + center_container.size_flags_vertical = Control.SIZE_SHRINK_CENTER + + # 4. 创建Label(文字控件) + var text_label = Label.new() + text_label.text = message + text_label.add_theme_color_override("font_color", Color(1, 1, 1, 1)) + text_label.add_theme_font_size_override("font_size", 14) + + # 平台特定的字体处理 + if is_web: + print("Web平台Toast字体处理") + # Web平台使用主题文件 + var chinese_theme = load("res://assets/ui/chinese_theme.tres") + if chinese_theme: + text_label.theme = chinese_theme + print("Web平台应用中文主题") + else: + print("Web平台中文主题加载失败") + else: + print("桌面平台Toast字体处理") + # 桌面平台直接加载中文字体 + var desktop_chinese_font = load("res://assets/fonts/msyh.ttc") + if desktop_chinese_font: + text_label.add_theme_font_override("font", desktop_chinese_font) + print("桌面平台使用中文字体") + + text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + text_label.custom_minimum_size = Vector2(280, 0) + text_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + text_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + + # 组装控件层级 + center_container.add_child(text_label) + vbox.add_child(center_container) + toast_panel.add_child(vbox) + + # 计算位置 + var margin = 20 + var start_x = get_viewport().get_visible_rect().size.x + var final_x = get_viewport().get_visible_rect().size.x - toast_width - margin + + # 计算Y位置 + var y_position = margin + for existing_toast in active_toasts: + if is_instance_valid(existing_toast): + y_position += existing_toast.size.y + 15 + + # 设置初始位置 + toast_panel.position = Vector2(start_x, y_position) + + # 添加到容器 + toast_container.add_child(toast_panel) + active_toasts.append(toast_panel) + + # 等待一帧让布局系统计算尺寸 + await get_tree().process_frame + + # 让Toast高度自适应内容 + var content_size = vbox.get_combined_minimum_size() + var final_height = max(60, content_size.y + 20) # 最小60,加20像素边距 + toast_panel.size.y = final_height + + # 重新排列所有Toast + rearrange_toasts() + + # 开始动画 + animate_toast_in(toast_panel, final_x) + +func animate_toast_in(toast_panel: Panel, final_x: float): + var tween = create_tween() + tween.set_ease(Tween.EASE_OUT) + tween.set_trans(Tween.TRANS_BACK) + + tween.parallel().tween_property(toast_panel, "position:x", final_x, 0.6) + tween.parallel().tween_property(toast_panel, "modulate:a", 1.0, 0.4) + + toast_panel.modulate.a = 0.0 + + await get_tree().create_timer(3.0).timeout + animate_toast_out(toast_panel) + +func animate_toast_out(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + var tween = create_tween() + tween.set_ease(Tween.EASE_IN) + tween.set_trans(Tween.TRANS_QUART) + + var end_x = get_viewport().get_visible_rect().size.x + 50 + tween.parallel().tween_property(toast_panel, "position:x", end_x, 0.4) + tween.parallel().tween_property(toast_panel, "modulate:a", 0.0, 0.3) + + await tween.finished + cleanup_toast(toast_panel) + +func cleanup_toast(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + active_toasts.erase(toast_panel) + rearrange_toasts() + toast_panel.queue_free() + +func rearrange_toasts(): + var margin = 20 + var current_y = margin + + for i in range(active_toasts.size()): + var toast = active_toasts[i] + if is_instance_valid(toast): + var tween = create_tween() + tween.tween_property(toast, "position:y", current_y, 0.2) + current_y += toast.size.y + 15 + +# ============ UI工具方法 ============ + +func show_loading(button: Button, loading_text: String): + button.disabled = true + button.text = loading_text + +func restore_button(button: Button, original_text: String): + button.disabled = false + button.text = original_text + +func show_field_error(error_label: Label, message: String): + error_label.text = message + error_label.visible = true + +func hide_field_error(error_label: Label): + error_label.visible = false + +func is_valid_email(email: String) -> bool: + return StringUtils.is_valid_email(email) + +func is_valid_phone(phone: String) -> bool: + var regex = RegEx.new() + regex.compile("^\\+?[1-9]\\d{1,14}$") + return regex.search(phone) != null + +# ============ 表单验证方法 ============ + +func validate_username(username: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if username.is_empty(): + result.message = "用户名不能为空" + return result + + if not StringUtils.is_valid_username(username): + if username.length() > 50: + result.message = "用户名长度不能超过50字符" + else: + result.message = "用户名只能包含字母、数字和下划线" + return result + + result.valid = true + return result + +func validate_email(email: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if email.is_empty(): + result.message = "邮箱不能为空" + return result + + if not StringUtils.is_valid_email(email): + result.message = "请输入有效的邮箱地址" + return result + + result.valid = true + return result + +func validate_password(password: String) -> Dictionary: + return StringUtils.validate_password_strength(password) + +func validate_confirm_password(password: String, confirm: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if confirm.is_empty(): + result.message = "确认密码不能为空" + return result + + if password != confirm: + result.message = "两次输入的密码不一致" + return result + + result.valid = true + return result + +func validate_verification_code(code: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if code.is_empty(): + result.message = "验证码不能为空" + return result + + if code.length() != 6: + result.message = "验证码必须是6位数字" + return result + + for i in range(code.length()): + var character = code[i] + if not (character >= '0' and character <= '9'): + result.message = "验证码必须是6位数字" + return result + + result.valid = true + return result + +# ============ 表单验证事件 ============ + +func _on_login_username_focus_exited(): + var username = login_username.text.strip_edges() + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + else: + hide_field_error(login_username_error) + +func _on_login_password_focus_exited(): + var password = login_password.text + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + else: + hide_field_error(login_password_error) + +func _on_login_verification_focus_exited(): + var verification_code = login_verification.text.strip_edges() + if verification_code.is_empty(): + show_field_error(login_verification_error, "验证码不能为空") + elif verification_code.length() != 6: + show_field_error(login_verification_error, "验证码必须是6位数字") + elif not verification_code.is_valid_int(): + show_field_error(login_verification_error, "验证码只能包含数字") + else: + hide_field_error(login_verification_error) + +func _on_register_username_focus_exited(): + var username = register_username.text.strip_edges() + var validation = validate_username(username) + if not validation.valid: + show_field_error(register_username_error, validation.message) + else: + hide_field_error(register_username_error) + +func _on_register_email_focus_exited(): + var email = register_email.text.strip_edges() + var validation = validate_email(email) + if not validation.valid: + show_field_error(register_email_error, validation.message) + else: + hide_field_error(register_email_error) + +func _on_register_password_focus_exited(): + var password = register_password.text + var validation = validate_password(password) + if not validation.valid: + show_field_error(register_password_error, validation.message) + else: + hide_field_error(register_password_error) + if not register_confirm.text.is_empty(): + _on_register_confirm_focus_exited() + +func _on_register_confirm_focus_exited(): + var password = register_password.text + var confirm = register_confirm.text + var validation = validate_confirm_password(password, confirm) + if not validation.valid: + show_field_error(register_confirm_error, validation.message) + else: + hide_field_error(register_confirm_error) + +func _on_verification_focus_exited(): + var code = verification_input.text.strip_edges() + var validation = validate_verification_code(code) + if not validation.valid: + show_field_error(verification_error, validation.message) + else: + hide_field_error(verification_error) + +# ============ 实时输入验证事件 ============ + +func _on_register_username_text_changed(new_text: String): + if register_username_error.visible and not new_text.is_empty(): + hide_field_error(register_username_error) + +func _on_register_email_text_changed(new_text: String): + if register_email_error.visible and not new_text.is_empty(): + hide_field_error(register_email_error) + +func _on_register_password_text_changed(new_text: String): + if register_password_error.visible and not new_text.is_empty(): + hide_field_error(register_password_error) + +func _on_register_confirm_text_changed(new_text: String): + if register_confirm_error.visible and not new_text.is_empty(): + hide_field_error(register_confirm_error) + +func _on_verification_text_changed(new_text: String): + if verification_error.visible and not new_text.is_empty(): + hide_field_error(verification_error) +# ============ 表单整体验证 ============ + +func validate_login_form() -> bool: + var is_valid = true + + var username = login_username.text.strip_edges() + var password = login_password.text + + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + is_valid = false + else: + hide_field_error(login_username_error) + + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + is_valid = false + else: + hide_field_error(login_password_error) + + return is_valid + +func validate_register_form() -> bool: + print("开始验证注册表单") + var is_valid = true + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var confirm = register_confirm.text + var verification_code = verification_input.text.strip_edges() + + print("表单数据: 用户名='%s', 邮箱='%s', 密码长度=%d, 确认密码长度=%d, 验证码='%s'" % [username, email, password.length(), confirm.length(), verification_code]) + + var username_validation = validate_username(username) + if not username_validation.valid: + print("用户名验证失败: ", username_validation.message) + show_field_error(register_username_error, username_validation.message) + is_valid = false + else: + hide_field_error(register_username_error) + + var email_validation = validate_email(email) + if not email_validation.valid: + print("邮箱验证失败: ", email_validation.message) + show_field_error(register_email_error, email_validation.message) + is_valid = false + else: + hide_field_error(register_email_error) + + var password_validation = validate_password(password) + if not password_validation.valid: + print("密码验证失败: ", password_validation.message) + show_field_error(register_password_error, password_validation.message) + is_valid = false + else: + hide_field_error(register_password_error) + + var confirm_validation = validate_confirm_password(password, confirm) + if not confirm_validation.valid: + print("确认密码验证失败: ", confirm_validation.message) + show_field_error(register_confirm_error, confirm_validation.message) + is_valid = false + else: + hide_field_error(register_confirm_error) + + var code_validation = validate_verification_code(verification_code) + if not code_validation.valid: + print("验证码格式验证失败: ", code_validation.message) + show_field_error(verification_error, code_validation.message) + is_valid = false + else: + hide_field_error(verification_error) + + var current_email_input = register_email.text.strip_edges() + var has_sent_code = false + + if verification_codes_sent.has(current_email_input): + var email_data = verification_codes_sent[current_email_input] + has_sent_code = email_data.get("sent", false) + + if not has_sent_code: + print("当前邮箱验证码未发送,email = ", current_email_input) + show_toast("请先获取邮箱验证码", false) + is_valid = false + + print("表单验证结果: ", is_valid) + return is_valid + +# ============ 资源清理 ============ + +func _exit_tree(): + for request_id in active_request_ids: + NetworkManager.cancel_request(request_id) + active_request_ids.clear() + + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + +func _input(event): + if event.is_action_pressed("ui_cancel"): + get_tree().quit() \ No newline at end of file diff --git a/scenes/ui/AuthScene.gd.uid b/scenes/ui/AuthScene.gd.uid new file mode 100644 index 0000000..97b8ff9 --- /dev/null +++ b/scenes/ui/AuthScene.gd.uid @@ -0,0 +1 @@ +uid://b514h2wuido0h diff --git a/scenes/ui/LoginWindow.tscn b/scenes/ui/LoginWindow.tscn new file mode 100644 index 0000000..3cf8c76 --- /dev/null +++ b/scenes/ui/LoginWindow.tscn @@ -0,0 +1,561 @@ +[gd_scene load_steps=10 format=3 uid="uid://by7m8snb4xllf"] + +[ext_resource type="Texture2D" uid="uid://bx17oy8lvaca4" path="res://assets/ui/auth/bg_auth_scene.png" id="1_background"] +[ext_resource type="Texture2D" uid="uid://de4q4s1gxivtf" path="res://assets/ui/auth/login_frame_smart_transparent.png" id="2_frame"] +[ext_resource type="Script" path="res://scenes/ui/AuthScene.gd" id="3_script"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hover"] +bg_color = Color(0.3, 0.6, 0.9, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.2, 0.5, 0.8, 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_normal"] +bg_color = Color(0.2, 0.5, 0.8, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.15, 0.4, 0.7, 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_pressed"] +bg_color = Color(0.4, 0.7, 1, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.3, 0.6, 0.9, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="Theme" id="Theme_main_button"] +Button/colors/font_color = Color(1, 1, 1, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/font_sizes/font_size = 18 +Button/styles/hover = SubResource("StyleBoxFlat_hover") +Button/styles/normal = SubResource("StyleBoxFlat_normal") +Button/styles/pressed = SubResource("StyleBoxFlat_pressed") + +[sub_resource type="Theme" id="Theme_button"] +Button/colors/font_color = Color(1, 1, 1, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/styles/hover = SubResource("StyleBoxFlat_hover") +Button/styles/normal = SubResource("StyleBoxFlat_normal") +Button/styles/pressed = SubResource("StyleBoxFlat_pressed") + +[node name="AuthScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_script") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="BackgroundImage" 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("1_background") +expand_mode = 1 +stretch_mode = 6 + +[node name="WhaleFrame" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -300.0 +offset_top = -300.0 +offset_right = 300.0 +offset_bottom = 300.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("2_frame") +expand_mode = 1 +stretch_mode = 5 + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -175.0 +offset_top = -184.0 +offset_right = 175.0 +offset_bottom = 236.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="LoginPanel" type="Panel" parent="CenterContainer"] +custom_minimum_size = Vector2(350, 400) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_1") + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 30.0 +offset_top = 30.0 +offset_right = -30.0 +offset_bottom = -30.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 24 +text = "Whaletown" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="SubtitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 14 +text = "开始你的小镇之旅!" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="LoginForm" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="UsernameContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="UsernameLabelContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer"] +layout_mode = 2 + +[node name="UsernameLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "用户名/手机/邮箱" + +[node name="RequiredStar" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UsernameError" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "用户名不能为空" +horizontal_alignment = 2 + +[node name="UsernameInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "用户名/手机/邮箱" + +[node name="PasswordContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="PasswordLabelContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer"] +layout_mode = 2 + +[node name="PasswordLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PasswordError" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "密码不能为空" +horizontal_alignment = 2 + +[node name="PasswordInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入密码" +secret = true + +[node name="VerificationContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +visible = false +layout_mode = 2 + +[node name="VerificationLabelContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "验证码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VerificationError" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "请输入验证码" +horizontal_alignment = 2 + +[node name="VerificationInputContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入6位验证码" +max_length = 6 + +[node name="GetCodeBtn" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +text = "获取验证码" + +[node name="CheckboxContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="RememberPassword" type="CheckBox" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/CheckboxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "记住密码" + +[node name="AutoLogin" type="CheckBox" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/CheckboxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "自动登录" + +[node name="HSeparator2" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="MainButton" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer"] +custom_minimum_size = Vector2(280, 50) +layout_mode = 2 +theme = SubResource("Theme_main_button") +text = "进入小镇" + +[node name="HSeparator3" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="LoginBtn" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(100, 35) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "密码登录" + +[node name="BottomLinks" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="ForgotPassword" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/BottomLinks"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "忘记密码?" +flat = true + +[node name="RegisterLink" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/BottomLinks"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "注册居民身份" +flat = true + +[node name="RegisterPanel" type="Panel" parent="CenterContainer"] +visible = false +custom_minimum_size = Vector2(400, 570) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_1") + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 30.0 +offset_top = 75.0 +offset_right = -30.0 +offset_bottom = -72.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="TitleLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 20 +text = "注册新居民" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 + +[node name="RegisterForm" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +alignment = 1 + +[node name="UsernameContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="UsernameLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer"] +layout_mode = 2 + +[node name="UsernameLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "用户名" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UsernameError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "用户名不能为空" +horizontal_alignment = 2 + +[node name="UsernameInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入用户名" + +[node name="EmailContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="EmailLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer"] +layout_mode = 2 + +[node name="EmailLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "邮箱" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="EmailError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "邮箱不能为空" +horizontal_alignment = 2 + +[node name="EmailInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入邮箱地址" + +[node name="PasswordContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="PasswordLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer"] +layout_mode = 2 + +[node name="PasswordLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PasswordError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "密码不能为空" +horizontal_alignment = 2 + +[node name="PasswordInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入密码(至少8位)" +secret = true + +[node name="ConfirmContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="ConfirmLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer"] +layout_mode = 2 + +[node name="ConfirmLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "确认密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ConfirmError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "确认密码不能为空" +horizontal_alignment = 2 + +[node name="ConfirmInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请再次输入密码" +secret = true + +[node name="VerificationContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="VerificationLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "邮箱验证码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VerificationError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "验证码不能为空" +horizontal_alignment = 2 + +[node name="VerificationInputContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入6位验证码" +max_length = 6 + +[node name="SendCodeBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +text = "发送验证码" + +[node name="HSeparator2" type="HSeparator" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="RegisterBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 45) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "注册" + +[node name="ToLoginBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 45) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "返回登录" + +[node name="ToastContainer" type="Control" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2