26 Commits

Author SHA1 Message Date
91c8b1b062 chore(assets): 添加建筑和围栏精灵资源
新增游戏资源文件:
- 12 个 Datawhale 房屋建筑精灵(多种样式)
- 2 个围栏瓦片集(木质和铁质)

所有文件名已规范为小写格式
2026-01-10 01:32:58 +08:00
e3c4d08021 Merge pull request '修正语法错误' (#11) from qbb0530/whale-town-front:main into main
Reviewed-on: #11
2026-01-05 11:28:13 +08:00
王浩
3bdda47191 修正语法错误 2026-01-04 17:17:34 +08:00
3d6c4e5356 Merge pull request 'feature/网格瓦片系统' (#9) from feature/网格瓦片系统 into main
Reviewed-on: #9
2026-01-03 22:40:55 +08:00
c621d70475 chore:清理空的占位文件
- 删除sprites目录下的空.gitkeep文件
- 删除tools目录下的空README.md文件
2026-01-03 22:37:04 +08:00
f527fa3c38 docs:添加网格瓦片系统功能文档
- 详细说明32x32网格系统的使用方法
- 包含核心组件介绍和API参考
- 提供编辑器和代码使用示例
2026-01-03 22:35:36 +08:00
e9fa21280e scene:创建广场地图场景并添加环境瓦片资源
- 新增square.tscn广场地图场景
- 添加多种环境瓦片纹理资源
- 包含草地、地板、路缘等瓦片素材
2026-01-03 22:35:13 +08:00
a3d384d39d style:统一代码文件末尾换行格式 2026-01-03 22:33:56 +08:00
ced69fd4b6 scene:创建草地瓦片预制体
- 实现GrassTile组件,支持32x32网格对齐
- 添加自动纹理验证和占位符生成
- 提供网格位置设置和世界坐标转换
- 包含位置变化信号和调试功能
2026-01-03 22:30:03 +08:00
7a6e5be4f8 config:更新核心配置支持网格系统
- EventNames添加网格相关事件定义
- ProjectPaths添加网格系统和地形资源路径
2026-01-03 22:28:29 +08:00
ba5b0daa13 feat:实现32x32网格系统核心功能
- 添加GridSystem类提供网格坐标转换
- 支持世界坐标与网格坐标互转
- 提供位置吸附和距离计算方法
- 包含网格区域和边界检查功能
2026-01-03 22:28:06 +08:00
83404d031e Merge pull request 'feature/auth-system-refactor' (#8) from feature/auth-system-refactor into main
Reviewed-on: #8
2026-01-02 21:25:30 +08:00
709242d223 docs:更新项目结构说明和路径配置
- 更新ProjectPaths.gd中的路径常量
- 更新项目结构说明文档
- 清理临时测试文件的uid引用

确保文档与实际项目结构保持一致
2026-01-02 21:20:31 +08:00
2f1ccbc2cd docs:为核心管理器添加详细中文注释
- GameManager.gd:游戏状态管理注释
- NetworkManager.gd:网络请求管理注释
- SceneManager.gd:场景切换管理注释
- StringUtils.gd:字符串工具函数注释

按照docs注释规范,添加文件头、函数说明、参数描述和使用示例
方便协同开发者快速理解和调用
2026-01-02 21:19:53 +08:00
aaaf2b31a8 fix:修复EventSystem中的GDScript语法错误
- 移除不支持的try/except语句
- 改为直接调用回调函数
- 确保EventSystem能正常编译运行
2026-01-02 21:19:25 +08:00
93baf1a5b5 refactor:统一AuthScene命名规范
- 将LoginWindow.tscn重命名为AuthScene.tscn
- 更新MainScene.tscn中的场景引用路径
- 实现命名一致性:
  - 场景文件:AuthScene.tscn
  - 脚本文件:AuthScene.gd
  - 节点名称:AuthScene
- AuthScene比LoginWindow更准确描述功能(登录+注册)
2026-01-02 21:19:04 +08:00
5f915c61b6 refactor:AuthScene解耦重构,实现视图与业务逻辑分离
- 创建AuthManager.gd:负责所有认证业务逻辑
  - 用户登录/注册逻辑
  - 表单验证逻辑
  - 验证码管理逻辑
  - 网络请求管理

- 创建ToastManager.gd:负责Toast消息管理
  - Toast创建和显示
  - 动画和生命周期管理
  - 支持成功/失败样式

- 重构AuthScene.gd:纯视图层实现
  - 只负责UI交互和显示
  - 通过信号与业务层通信
  - 移除所有业务逻辑代码

- 修复GDScript警告:
  - 未使用参数添加下划线前缀
  - 修复变量名与基类方法冲突
  - 修复EventSystem中的try语法错误
  - 修复AuthManager中的方法名不匹配错误

符合docs中的架构要求,实现完全解耦
2026-01-02 21:18:38 +08:00
d256249789 refactor:将MainScene移动到scenes根目录
- 将MainScene从scenes/Maps/移动到scenes/根目录
- 更新project.godot中的主场景路径配置
- 符合项目结构规范,MainScene作为图像显示入口文件
2026-01-02 21:17:56 +08:00
29c6740870 Merge pull request 'feature/project-structure-refactor' (#7) from feature/project-structure-refactor into main
Reviewed-on: #7
2026-01-02 01:03:07 +08:00
7b1affa360 Merge branch 'main' into feature/project-structure-refactor 2026-01-02 01:02:59 +08:00
fa360e1c78 docs:更新项目文档以反映新的结构变更
- 更新 README.md 中的项目结构说明
- 修订项目结构说明文档,反映最新的目录组织
- 确保文档与实际项目结构保持同步
2026-01-02 00:59:25 +08:00
d80feaa02b test:更新测试文件以适配新的项目结构
- 更新 auth_ui_test.tscn 中的场景引用路径
- 修复 enhanced_toast_test.gd 中的脚本路径引用
- 确保测试文件与重构后的项目结构保持一致
2026-01-02 00:59:05 +08:00
f1a60137e1 fix:修复代码警告和UID冲突问题
- 更新 ProjectPaths.gd 中的路径引用,适配新的目录结构
- 修复 SceneManager.gd 中的场景路径问题
- 更新 project.godot 配置,修复 AutoLoad 路径
- 修复 MainScene 相关文件的 UID 冲突
- 解决代码中的路径引用警告
2026-01-02 00:58:51 +08:00
3175c98ea3 refactor:实现新的项目结构组织
- 添加 _Core/components/ 和 _Core/utils/ 目录
- 重新组织 scenes/ 目录结构,按功能分类
- 迁移 StringUtils.gd 到新的 _Core/utils/ 位置
- 迁移 AuthScene.gd 到新的 scenes/ui/ 位置
- 添加 AI 文档支持目录 docs/AI_docs/
- 添加开发参考文档 claude.md
2026-01-02 00:58:34 +08:00
a18c7a54b1 refactor:重构项目结构,删除旧的文件组织
- 删除旧的 UI/Windows/ 目录下的认证相关文件
- 删除旧的 Utils/ 目录下的工具类文件
- 删除旧的 scenes/Components/ 目录结构
- 为新的目录结构让路
2026-01-02 00:58:16 +08:00
fca3eb79dd chore:清理临时文档文件
- 删除迁移完成标记文件 MIGRATION_COMPLETE.md
- 删除重构说明文件 REFACTORING.md
- 删除结构对比文件 STRUCTURE_COMPARISON.md
- 删除临时文档 cloude.md
2026-01-02 00:57:53 +08:00
104 changed files with 5241 additions and 2566 deletions

View File

@@ -1,235 +0,0 @@
# ✅ 项目结构重构完成报告
## 📅 完成时间
2025-12-31
## 🎉 重构成功
项目已成功从混乱的文件结构重构为清晰、模块化的架构!
---
## 📊 最终结构总览
```
whale-town-front/
├── _Core/ ✅ [框架层] 全局单例和系统
├── Scenes/ ✅ [玩法层] 游戏世界实体
├── UI/ ✅ [界面层] 所有UI界面
├── Assets/ ✅ [资源层] 美术资源
├── Config/ ✅ [配置层] 静态数据
├── Utils/ ✅ [工具层] 工具类
├── Tests/ ✅ [测试层] 测试脚本
└── docs/ 📄 项目文档
```
---
## ✅ 已完成的迁移
### 1⃣ 框架层 (_Core/)
-`GameManager.gd``_Core/managers/`
-`SceneManager.gd``_Core/managers/` (已更新路径)
-`NetworkManager.gd``_Core/managers/`
-`ResponseHandler.gd``_Core/managers/`
-`EventSystem.gd``_Core/systems/`
### 2⃣ 场景层 (Scenes/)
-`scenes/main_scene.tscn``Scenes/Maps/main_scene.tscn`
-`scripts/scenes/MainScene.gd``Scenes/Maps/MainScene.gd`
-`scenes/prefabs/``Scenes/Components/`
### 3⃣ 界面层 (UI/)
-`scenes/auth_scene.tscn``UI/Windows/LoginWindow.tscn`
-`scripts/scenes/AuthScene.gd``UI/Windows/AuthScene.gd`
-`assets/ui/chinese_theme.tres``UI/Theme/MainTheme.tres`
-`assets/fonts/``UI/Theme/Fonts/`
### 4⃣ 配置层 (Config/)
-`data/configs/game_config.json``Config/game_config.json`
-`data/localization/zh_CN.json``Config/zh_CN.json`
### 5⃣ 工具层 (Utils/)
-`core/utils/StringUtils.gd``Utils/StringUtils.gd`
### 6⃣ 资源层 (Assets/)
-`assets/sprites/``Assets/Sprites/`
-`assets/audio/``Assets/Audio/`
- ✅ 其他资源文件保留在 `assets/`(待后续整理)
### 7⃣ 构建脚本
-`scripts/build_web.sh``./build_web.sh`
-`scripts/serve_web.sh``./serve_web.sh`
---
## 🗑️ 已删除的旧目录
-`core/` - 已迁移到 `_Core/`
-`module/` - 空目录,未使用
-`scripts/` - 脚本已内联到场景目录
-`scenes/` - 已迁移到 `Scenes/``UI/`
-`data/` - 配置已移至 `Config/`
---
## 🔧 已更新的配置
### project.godot
```ini
✅ run/main_scene="res://Scenes/Maps/main_scene.tscn"
✅ GameManager="*res://_Core/managers/GameManager.gd"
✅ SceneManager="*res://_Core/managers/SceneManager.gd"
✅ EventSystem="*res://_Core/systems/EventSystem.gd"
✅ NetworkManager="*res://_Core/managers/NetworkManager.gd"
✅ ResponseHandler="*res://_Core/managers/ResponseHandler.gd"
```
### SceneManager.gd
```gdscript
"main": "res://Scenes/Maps/main_scene.tscn"
"auth": "res://UI/Windows/LoginWindow.tscn"
```
### 测试文件
```gdscript
tests/auth/enhanced_toast_test.gd -
tests/auth/auth_ui_test.tscn -
```
---
## 📚 创建的文档
1. **REFACTORING.md** - 详细的重构文档
- 迁移映射表
- 设计原则
- 注意事项
- 后续建议
2. **STRUCTURE_COMPARISON.md** - 结构对比分析
- 旧结构问题分析
- 新结构优势说明
- 对比表格
- 团队协作改进
---
## 🎯 关键改进
### 清晰的分层
- **_Core**: 框架代码,全局系统
- **Scenes**: 游戏世界,地图和实体
- **UI**: 所有界面HUD和弹窗
- **Config**: 静态数据,策划可编辑
- **Utils**: 通用工具函数库
### 组件化设计
```gdscript
Scenes/Components/ //
characters/ //
effects/ //
items/ //
ui/ // UI预制体
```
### 场景内聚
- 每个 `.tscn` 配套一个 `.gd`
- 脚本紧邻场景文件
- 符合 Godot 原生习惯
### UI 独立化
```
UI/
├── Windows/ // 模态窗口(登录、设置)
├── HUD/ // 常驻界面(聊天框)
├── Dialog/ // 对话系统
└── Theme/ // 全局样式
```
---
## ⚠️ 后续步骤
### 必做事项
- [ ] 在 Godot 编辑器中打开项目,让编辑器重新索引文件
- [ ] 测试主场景加载: `Scenes/Maps/main_scene.tscn`
- [ ] 验证登录窗口: `UI/Windows/LoginWindow.tscn`
- [ ] 测试所有自动加载脚本
- [ ] 运行完整测试套件
### 建议优化
- [ ] 补充 `Scenes/Components/` 下的可复用组件
- [ ] 完善 `UI/HUD/``UI/Dialog/`
- [ ] 添加 `Scenes/Entities/Player/` 玩家实体
- [ ] 将硬编码数值移至 `Config/`
### 代码审查
- [ ] 检查是否还有硬编码的旧路径
- [ ] 验证所有 `.import` 文件正常
- [ ] 确认网络连接功能正常
- [ ] 验证 UI 主题显示正确
---
## 🎓 团队协作指南
### 工作目录划分
```
🎨 美术组 → Assets/Sprites/, Assets/Audio/
📋 策划组 → Config/
💻 前端程序 → UI/, Scenes/Entities/
⚙️ 后端程序 → _Core/, Utils/
🧪 测试组 → Tests/
```
### Git 提交建议
```bash
# 按目录分类提交
git add _Core/
git commit -m "refactor: 迁移核心框架代码到 _Core/"
git add Scenes/
git commit -m "refactor: 重组场景文件到 Scenes/"
git add UI/
git commit -m "refactor: 独立 UI 界面到 UI/"
```
---
## 📈 预期收益
### 可维护性提升
- 🟢 目录职责清晰,降低认知负担
- 🟢 新人快速定位文件
- 🟢 减少代码冲突
### 开发效率提升
- 🟢 组件复用更容易
- 🟢 团队协作更流畅
- 🟢 代码审查更高效
### 符合最佳实践
- ✅ Godot 官方推荐结构
- ✅ 场景内聚原则
- ✅ 组件化设计思想
- ✅ 配置与代码分离
---
## 🎉 总结
**重构完成!** 项目现在拥有:
- ✅ 清晰的 6 层架构
- ✅ 符合 Godot 最佳实践
- ✅ 易于维护和扩展
- ✅ 团队协作友好
感谢您的耐心!如有问题,请查看详细文档:
- [REFACTORING.md](./REFACTORING.md) - 重构详情
- [STRUCTURE_COMPARISON.md](./STRUCTURE_COMPARISON.md) - 结构对比
---
**下一步:在 Godot 编辑器中打开项目并测试!** 🚀

View File

@@ -75,6 +75,7 @@ python tests/api/api_client_test.py
1. **[📖 项目入门总览](docs/01-项目入门/README.md)** - 5分钟了解项目
2. **[🏗️ 项目结构说明](docs/01-项目入门/项目结构说明.md)** - 理解架构设计
3. **[⚙️ 项目设置指南](docs/01-项目入门/项目设置指南.md)** - 配置开发环境
4. **[🤖 AI开发指南](docs/AI_docs/README.md)** - AI编程助手专用文档
### 🎯 第二步:学习规范
@@ -121,34 +122,65 @@ WhaleTown/ # 🐋 项目根目录
│ ├── 03-技术实现/ # 🔧 开发指导
│ ├── 04-高级开发/ # 🚀 进阶技巧
│ ├── 05-部署运维/ # 🌐 发布部署
── 06-功能模块/ # 🎮 功能文档
├── 🎬 scenes/ # 🎭 游戏场景
│ ├── Maps/ # 🗺 地图场景
── Components/ # 🧩 组件预制体
├── 🔧 _Core/ # ⚙️ 核心系统
│ ├── managers/ # 🎯 全局管理器
│ ├── systems/ # 🔄 系统组件
── 06-功能模块/ # 🎮 功能文档
│ └── AI_docs/ # 🤖 AI专用文档执行规范、代码模板
├── 🔧 _Core/ # 核心底层实现
── managers/ # 🎯 全局管理器(游戏状态、场景、网络等)
│ ├── systems/ # 🔄 系统组件(事件系统、输入系统等)
│ ├── components/ # 🧩 基础组件实现
│ ├── utils/ # <20> 核件心工具类(字符串处理、数学计算等)
│ ├── EventNames.gd # 📝 事件名称定义
│ └── ProjectPaths.gd # 📂 路径统一管理
├── 🎨 UI/ # 🖼️ 用户界面
── Windows/ # 🪟 窗口界面
├── 🔨 Utils/ # 🔨 工具类
├── 🎮 module/ # 🧩 功能模块
├── 🎨 assets/ # 🖼️ 游戏资源
├── ⚙️ Config/ # 📋 配置文
├── 🧪 tests/ # 🔬 测试文件
└── 🌐 web_assets/ # 🌍 Web部署资源
│ └── ProjectPaths.gd # <EFBFBD> 路径统一管理
├── 🎬 scenes/ # 🎭 场景与视觉呈现
── maps/ # <EFBFBD> 地图一场景(游戏关卡、世界地图)
│ ├── characters/ # 👤 人物场景角色、NPC、敌人
│ ├── ui/ # 🖼️ UI界面场景菜单、HUD、对话框
│ ├── effects/ # ✨ 特效场景(粒子效果、动画)
│ └── prefabs/ # 🧩 预制体组
├── 🎨 assets/ # 🖼️ 静态资源存储
│ ├── sprites/ # 🎨 精灵图片(角色、物品、环境)
│ ├── audio/ # 🎵 音频资源(音乐、音效)
│ ├── fonts/ # 🔤 字体文件
│ ├── materials/ # 🎭 材质资源
│ ├── shaders/ # 🌈 着色器文件
│ ├── ui/ # 🖼️ UI素材按钮、图标、背景
│ └── icon/ # 📱 应用图标
├── ⚙️ Config/ # 📋 配置文件管理
│ ├── game_config.json # 🎮 游戏配置(难度、设置等)
│ ├── zh_CN.json # 🌐 本地化配置
│ └── environment/ # 🔧 环境配置(开发、测试、生产)
├── 🧪 tests/ # 🔬 测试文件系统
│ ├── unit/ # 🔍 单元测试(组件功能测试)
│ ├── integration/ # 🔗 集成测试(系统交互测试)
│ ├── performance/ # ⚡ 性能测试(帧率、内存优化)
│ └── api/ # 🌐 API接口测试
└── 🌐 web_assets/ # 🌍 Web导出资源
├── html/ # 📄 HTML模板文件
├── css/ # 🎨 样式文件
└── js/ # 📜 JavaScript脚本
```
### 🔧 核心组件
### 🔧 核心架构说明
| 组件 | 作用 | 文档链接 |
| 目录 | 作用 | 详细说明 |
|------|------|----------|
| **EventSystem** | 全局事件通信 | [架构规范](docs/02-开发规范/架构与通信规范.md) |
| **GameManager** | 游戏状态管理 | [实现细节](docs/03-技术实现/实现细节规范.md) |
| **SceneManager** | 场景切换管理 | [场景设计](docs/04-高级开发/场景设计规范.md) |
| **NetworkManager** | 网络请求管理 | [网络管理器](docs/03-技术实现/网络管理器设置.md) |
| **ProjectPaths** | 路径统一管理 | [项目结构](docs/01-项目入门/项目结构说明.md) |
| **_Core** | 🔧 功能实现与组件实现 | 项目最基本的底层实现,包含所有核心系统和基础组件 |
| **scenes** | 🎭 场景与视觉呈现 | 包含地图场景、人物场景等一系列视觉呈现部分主要是UI的实现 |
| **assets** | 🎨 静态资源存储 | 所有静态资源的存储,包括图片、音乐、视频、贴图等素材 |
| **Config** | ⚙️ 配置文件管理 | 主要用来配置各类环境,包括游戏设置、本地化等配置 |
| **tests** | 🧪 测试文件系统 | 放置所有对应组件的测试代码,方便快速进行功能性与性能测试 |
| **web_assets** | 🌐 Web导出资源 | 专门用于Web平台导出的相关资源和配置文件 |
| **docs/AI_docs** | 🤖 AI专用文档 | 专门为AI编程助手准备的执行规范和代码模板提升vibe coding效率 |
### 🎮 核心组件
| 组件 | 位置 | 作用 | 文档链接 |
|------|------|------|----------|
| **EventSystem** | _Core/systems/ | 全局事件通信系统 | [架构规范](docs/02-开发规范/架构与通信规范.md) |
| **GameManager** | _Core/managers/ | 游戏状态管理器 | [实现细节](docs/03-技术实现/实现细节规范.md) |
| **SceneManager** | _Core/managers/ | 场景切换管理器 | [场景设计](docs/04-高级开发/场景设计规范.md) |
| **NetworkManager** | _Core/managers/ | 网络请求管理器 | [网络管理器](docs/03-技术实现/网络管理器设置.md) |
| **ProjectPaths** | _Core/ | 路径统一管理工具 | [项目结构](docs/01-项目入门/项目结构说明.md) |
---

View File

@@ -1,222 +0,0 @@
# 项目结构重构文档
## 📅 重构时间
2025-12-31
## 🎯 重构目标
将项目从混乱的文件结构重构为清晰、模块化的架构,采用 Godot 最佳实践。
## 📊 重构前后对比
### 旧结构问题
```
❌ core/ - 概念模糊,混合了框架和业务逻辑
❌ module/ - 空壳目录,设计未落地
❌ scripts/ - 与 scenes/ 重复,脚本分散
❌ scenes/ - 场景、预制体、脚本混在一起
❌ data/ - 配置和运行时数据不分
```
### 新结构优势
```
✅ _Core/ - 框架层:全局单例和系统
✅ Scenes/ - 玩法层:按游戏世界组织
✅ UI/ - 界面层独立的UI管理
✅ Assets/ - 资源层:纯美术资源
✅ Config/ - 配置层:静态数据
✅ Utils/ - 工具层:通用函数库
```
## 🏗️ 新的目录结构
```
res://
├── _Core/ # [框架层] 游戏的底层框架,单例,全局管理器
│ ├── managers/ # 游戏管理器
│ │ ├── GameManager.gd # 游戏状态管理
│ │ ├── SceneManager.gd # 场景管理(已更新路径)
│ │ ├── NetworkManager.gd # 网络通信
│ │ └── ResponseHandler.gd # API响应处理
│ ├── systems/ # 核心系统
│ │ └── EventSystem.gd # 事件系统
│ └── singletons/ # 其他单例(待扩展)
├── Scenes/ # [玩法层] 具体的游戏场景、实体、地图
│ ├── Maps/ # 地图场景
│ │ └── main_scene.tscn # 主游戏场景
│ ├── Entities/ # 游戏实体
│ │ ├── Player/ # 玩家实体
│ │ ├── NPC/ # NPC实体
│ │ └── Interactables/ # 交互物
│ └── Components/ # 可复用组件
├── UI/ # [界面层] 所有UI相关的预制体和逻辑
│ ├── HUD/ # 抬头显示(常驻)
│ ├── Windows/ # 模态窗口
│ │ └── LoginWindow.tscn # 登录窗口原auth_scene.tscn
│ ├── Dialog/ # 对话系统
│ └── Theme/ # 全局样式
│ ├── MainTheme.tres # 主主题
│ └── Fonts/ # 字体文件
├── Assets/ # [资源层] 美术、音频、字体
│ ├── Sprites/ # 精灵图
│ ├── Audio/ # 音频
│ └── Fonts/ # 字体
├── Config/ # [配置层] 游戏配置文件
│ ├── game_config.json # 游戏配置
│ └── zh_CN.json # 中文本地化
├── Utils/ # [工具层] 通用辅助脚本
│ └── StringUtils.gd # 字符串工具
└── Tests/ # [测试层] 单元测试脚本
├── api/ # API测试
├── auth/ # 认证测试
├── integration/ # 集成测试
├── performance/ # 性能测试
└── unit/ # 单元测试
```
## 🔄 迁移映射表
| 旧路径 | 新路径 | 说明 |
|--------|--------|------|
| `core/managers/*` | `_Core/managers/` | 框架层管理器 |
| `core/systems/*` | `_Core/systems/` | 框架层系统 |
| `core/utils/*` | `Utils/` | 工具类 |
| `scenes/main_scene.tscn` | `Scenes/Maps/main_scene.tscn` | 主游戏场景 |
| `scenes/auth_scene.tscn` | `UI/Windows/LoginWindow.tscn` | 登录窗口 |
| `data/configs/*.json` | `Config/` | 配置文件 |
| `data/localization/*.json` | `Config/` | 本地化配置 |
| `assets/ui/chinese_theme.tres` | `UI/Theme/MainTheme.tres` | UI主题 |
| `assets/fonts/*` | `UI/Theme/Fonts/` | 字体文件 |
## ✂️ 已删除的目录
-`core/` - 已迁移到 `_Core/`
-`module/` - 空目录,未使用
-`scripts/` - 脚本应内联到场景目录
-`scenes/` - 已迁移到 `Scenes/``UI/`
-`data/` - 配置已移至 `Config/`
## 🔧 已更新的配置文件
### project.godot
```ini
# 更新主场景路径
run/main_scene="res://Scenes/Maps/main_scene.tscn"
# 更新自动加载路径
GameManager="*res://_Core/managers/GameManager.gd"
SceneManager="*res://_Core/managers/SceneManager.gd"
EventSystem="*res://_Core/systems/EventSystem.gd"
NetworkManager="*res://_Core/managers/NetworkManager.gd"
ResponseHandler="*res://_Core/managers/ResponseHandler.gd"
```
### SceneManager.gd
```gdscript
# 更新场景路径映射
var scene_paths: Dictionary = {
"main": "res://Scenes/Maps/main_scene.tscn",
"auth": "res://UI/Windows/LoginWindow.tscn",
# ... 其他场景路径
}
```
### 测试文件
- `tests/auth/enhanced_toast_test.gd` - 已更新脚本路径
- `tests/auth/auth_ui_test.tscn` - 已更新场景路径
## 🎨 设计原则
### 1. 清晰的分层
- **_Core**: 框架代码,与具体游戏逻辑无关
- **Scenes**: 游戏世界,地图和实体
- **UI**: 所有界面HUD和弹窗
- **Config**: 静态数据,策划可编辑
### 2. 组件化设计
```gdscript
# 可复用组件放在 Scenes/Components/
Scenes/Components/
InteractableArea.tscn # 让任何物体"可交互"
MovementSync.gd # 网络位置同步
NameTag3D.tscn # 头顶名字条
```
### 3. 场景内聚
- 每个 .tscn 配套一个 .gd
- 脚本紧邻场景文件存放
- 符合 Godot 原生开发习惯
### 4. 职责单一
```
UI/Windows/ - 模态窗口(登录、设置、商店)
UI/HUD/ - 常驻界面(聊天框、状态栏)
UI/Dialog/ - 对话系统
```
## 🚀 后续建议
### 待完善的功能
1. **组件化开发**
- 创建 `Scenes/Components/` 下的可复用组件
- 使用组合优于继承的设计模式
2. **UI 独立化**
- 补充 `UI/HUD/` 下的常驻界面
- 创建 `UI/Dialog/` 对话系统
3. **场景管理**
- 补充更多地图场景到 `Scenes/Maps/`
- 添加玩家实体到 `Scenes/Entities/Player/`
4. **配置驱动**
- 将硬编码的数值移到 `Config/`
- 使用 Resource 文件管理游戏数据
### 团队协作
- **美术组**: 主要在 `Assets/` 工作
- **策划组**: 主要在 `Config/` 工作
- **程序组**: 主要在 `_Core/`, `Scenes/`, `UI/` 工作
- **测试组**: 主要在 `Tests/` 工作
## 📝 迁移检查清单
- [x] 创建新的目录结构
- [x] 迁移核心管理器
- [x] 迁移工具类
- [x] 迁移场景文件
- [x] 分离 UI 界面
- [x] 迁移配置文件
- [x] 重组资源文件
- [x] 更新 project.godot
- [x] 更新路径引用
- [x] 清理旧目录
- [ ] 在 Godot 编辑器中测试场景加载
- [ ] 验证所有自动加载脚本正常工作
- [ ] 测试网络连接功能
- [ ] 验证 UI 主题显示
## ⚠️ 注意事项
1. **场景引用更新**: 所有旧场景的引用都已更新,但建议在 Godot 编辑器中重新打开项目,让编辑器重新索引文件
2. **.import 文件**: 移动资源文件后Godot 可能会重新生成 .import 文件,这是正常的
3. **版本控制**: 如果使用 Git旧文件的删除会在下次提交时体现
4. **测试覆盖**: 迁移后建议运行完整的测试套件确保功能正常
## 🎓 参考资料
- [Godot 官方项目组织建议](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html)
- [GDScript 场景组织](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#scenes-and-scripts)
- [ECS 架构模式](https://github.com/SmitUS/Pine-Tree-ECS-For-Godot-4)
---
**重构完成!项目现在拥有清晰的架构,易于维护和扩展。** 🎉

View File

@@ -1,213 +0,0 @@
# 🏗️ 项目结构对比
## 旧结构 ❌
```
whale-town-front/
├── core/ # ❌ 概念模糊
│ ├── managers/ # - 框架代码?
│ ├── systems/ # - 还是业务逻辑?
│ └── utils/ # - 边界不清
├── module/ # ❌ 空壳目录(无 .gd 文件)
│ ├── Character/
│ ├── Combat/
│ ├── Dialogue/
│ ├── Inventory/
│ └── UI/
├── scenes/ # ❌ 混乱的组织
│ ├── auth_scene.tscn
│ ├── main_scene.tscn
│ ├── Components/
│ ├── Entities/
│ ├── Maps/
│ └── prefabs/
├── scripts/ # ❌ 与 scenes/ 重复
│ ├── characters/
│ ├── scenes/
│ ├── ui/
│ └── network/
├── data/ # ❌ 配置和数据混在一起
│ ├── configs/
│ ├── characters/
│ ├── dialogues/
│ └── localization/
├── assets/ # ✅ 相对清晰
│ ├── audio/
│ ├── fonts/
│ ├── icon/
│ ├── materials/
│ ├── shaders/
│ ├── sprites/
│ └── ui/
├── tests/ # ✅ 结构良好
│ ├── api/
│ ├── auth/
│ ├── integration/
│ ├── performance/
│ └── unit/
└── docs/
```
**问题总结:**
1. 🔴 脚本分散:`scripts/``scenes/` 都有脚本,职责不清
2. 🔴 空壳模块:`module/` 目录存在但无实际代码
3. 🔴 场景混乱:场景文件、预制体、脚本平级放置
4. 🔴 分层不明:`core/`, `module/`, `scripts/` 三层交叉
5. 🔴 数据混杂:`data/` 既包含配置也包含运行时数据
---
## 新结构 ✅
```
whale-town-front/
├── _Core/ # ✅ 框架层 - 清晰的单例和系统
│ ├── managers/ # - 全局管理器
│ ├── systems/ # - 核心系统
│ └── singletons/ # - 其他单例
├── Scenes/ # ✅ 玩法层 - 按游戏世界组织
│ ├── Maps/ # - 地图场景
│ │ └── main_scene.tscn
│ ├── Entities/ # - 游戏实体
│ │ ├── Player/ # - 玩家
│ │ ├── NPC/ # - NPC
│ │ └── Interactables/ # - 交互物
│ └── Components/ # - 可复用组件
├── UI/ # ✅ 界面层 - 独立的UI管理
│ ├── HUD/ # - 抬头显示(常驻)
│ ├── Windows/ # - 模态窗口
│ │ └── LoginWindow.tscn # (原 auth_scene)
│ ├── Dialog/ # - 对话系统
│ └── Theme/ # - 全局样式
│ ├── MainTheme.tres
│ └── Fonts/
├── Assets/ # ✅ 资源层 - 纯美术资源
│ ├── Sprites/ # - 精灵图
│ ├── Audio/ # - 音频
│ └── Fonts/ # - 字体
├── Config/ # ✅ 配置层 - 静态数据
│ ├── game_config.json
│ └── zh_CN.json
├── Utils/ # ✅ 工具层 - 通用函数库
│ └── StringUtils.gd
├── Tests/ # ✅ 测试层 - 完整的测试覆盖
│ ├── api/
│ ├── auth/
│ ├── integration/
│ ├── performance/
│ └── unit/
└── docs/ # 📄 项目文档
└── web_deployment_guide.md
```
**改进总结:**
1. 🟢 **分层清晰**: 框架、玩法、界面、资源、配置、工具各司其职
2. 🟢 **场景内聚**: .tscn 和 .gd 成对出现,逻辑紧耦合场景
3. 🟢 **UI 独立**: 所有界面统一管理,避免和游戏场景混淆
4. 🟢 **配置分离**: Config 只存放静态数据,策划可直接编辑
5. 🟢 **组件化**: Scenes/Components/ 提供可复用的逻辑组件
---
## 📊 核心改进对比表
| 维度 | 旧结构 | 新结构 | 改进效果 |
|:---:|:---:|:---:|:---:|
| **目录层级** | 8层 | 6层 | ✅ 更扁平 |
| **脚本管理** | 分散在2处 | 集中在场景内 | ✅ 内聚性高 |
| **UI 组织** | 混在 scenes/ | 独立 UI/ | ✅ 职责清晰 |
| **框架代码** | core/ 概念模糊 | _Core/ 明确 | ✅ 边界清楚 |
| **配置管理** | data/ 混杂 | Config/ 专职 | ✅ 策划友好 |
| **可维护性** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 提升67% |
| **学习曲线** | ⭐⭐ | ⭐⭐⭐⭐ | ✅ 新人友好 |
---
## 🎯 Godot 最佳实践对照
### ✅ 符合 Godot 规范
- [x] 场景脚本内聚(.tscn + .gd 相邻)
- [x] 使用 autoload 全局单例
- [x] 组件化设计(可复用的 Components/
- [x] 资源独立管理Assets/
- [x] 配置与代码分离Config/
### 🔧 待完善项
- [ ] 补充 Scenes/Components/ 下的可复用组件
- [ ] 完善事件系统的使用
- [ ] 添加 SaveSystem 到 _Core/systems/
- [ ] 实现资源热重载机制
---
## 📈 团队协作改进
### 角色与目录对应
```
┌─────────────────┬─────────────────────────────────┐
│ 角色 │ 主要工作目录 │
├─────────────────┼─────────────────────────────────┤
│ 🎨 美术组 │ Assets/Sprites/, Assets/Audio/ │
│ 📋 策划组 │ Config/ │
│ 💻 前端程序 │ UI/, Scenes/Entities/ │
│ ⚙️ 后端程序 │ _Core/, Utils/ │
│ 🧪 测试组 │ Tests/ │
└─────────────────┴─────────────────────────────────┘
```
### 协作优势
1. **减少冲突**: 不同角色在不同目录工作
2. **职责清晰**: 每个目录有明确的负责人
3. **易于审查**: PR 可以按目录分类评审
4. **快速定位**: 新人快速找到相关文件
---
## 🚀 扩展性对比
### 旧结构的扩展问题
```gdscript
//
module/FeatureName/ //
scenes/feature_scene/ //
scripts/feature_logic/ //
data/feature_config/ //
```
### 新结构的扩展方式
```gdscript
//
Scenes/Entities/NewFeature/ // +
Config/feature_config.json //
```
---
## 📚 参考架构
这个新结构参考了业界最佳实践:
- **Godot 官方**: [Project Organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html)
- **Unity 模式**: Assets/Scenes/Scripts 分离
- **ECS 架构**: Entities + Components 思想
- **微服务思维**: 按功能域而非技术分层
---
## 🎓 学习资源
如果你是新人,这里有一些学习路径:
1. **先熟悉目录** → 查看 [REFACTORING.md](./REFACTORING.md)
2. **理解核心系统** → 阅读 `_Core/systems/EventSystem.gd`
3. **学习场景管理** → 查看 `_Core/managers/SceneManager.gd`
4. **研究 UI 结构** → 打开 `UI/Windows/LoginWindow.tscn`
5. **运行测试** → 执行 `Tests/` 下的测试用例
---
**结论:新结构更加清晰、模块化、易于维护,符合 Godot 最佳实践!** 🎉

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,199 +0,0 @@
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

View File

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

View File

@@ -51,6 +51,8 @@ const SCENE_DATA_TRANSFER = "scene_data_transfer"
const TILEMAP_READY = "tilemap_ready"
const COMPONENT_MESSAGE = "component_message"
const POSITION_UPDATE = "position_update"
const GRID_POSITION_CHANGED = "grid_position_changed"
const GRID_SNAP_REQUESTED = "grid_snap_requested"
# ============================================================================
# 测试事件

View File

@@ -16,6 +16,13 @@ class_name ProjectPaths
const CORE_ROOT = "res://_Core/"
const CORE_MANAGERS = CORE_ROOT + "managers/"
const CORE_SYSTEMS = CORE_ROOT + "systems/"
const CORE_COMPONENTS = CORE_ROOT + "components/"
const CORE_UTILS = CORE_ROOT + "utils/"
# 系统文件路径
const GRID_SYSTEM = CORE_SYSTEMS + "GridSystem.gd"
const EVENT_SYSTEM = CORE_SYSTEMS + "EventSystem.gd"
const TILE_SYSTEM = CORE_SYSTEMS + "TileSystem.gd"
# ============================================================================
# 场景路径
@@ -31,8 +38,8 @@ const SCENES_ITEM_COMPONENTS = SCENES_COMPONENTS + "items/"
# ============================================================================
# UI路径
# ============================================================================
const UI_ROOT = "res://UI/"
const UI_WINDOWS = UI_ROOT + "Windows/"
const UI_ROOT = "res://scenes/ui/"
const UI_WINDOWS = UI_ROOT
# ============================================================================
# 资源路径
@@ -44,6 +51,10 @@ const ASSETS_FONTS = ASSETS_ROOT + "fonts/"
const ASSETS_MATERIALS = ASSETS_ROOT + "materials/"
const ASSETS_SHADERS = ASSETS_ROOT + "shaders/"
# 地形资源路径
const ASSETS_TERRAIN = ASSETS_SPRITES + "terrain/"
const ASSETS_GRASS = ASSETS_TERRAIN + "grass/"
# ============================================================================
# 数据路径
# ============================================================================
@@ -69,7 +80,7 @@ const TESTS_AUTH = TESTS_ROOT + "auth/"
# ============================================================================
# 工具路径
# ============================================================================
const UTILS_ROOT = "res://Utils/"
const UTILS_ROOT = "res://_Core/utils/"
# ============================================================================
# 模块路径

View File

@@ -0,0 +1,2 @@
# 基础组件实现目录
# 存放项目的基础组件类

View File

@@ -0,0 +1,589 @@
class_name AuthManager
# ============================================================================
# AuthManager.gd - 认证管理器
# ============================================================================
# 认证系统的业务逻辑管理器,负责处理所有认证相关的业务逻辑
#
# 核心职责:
# - 用户登录业务逻辑(密码登录 + 验证码登录)
# - 用户注册业务逻辑
# - 表单验证逻辑
# - 验证码管理逻辑
# - 网络请求管理
# - 响应处理和状态管理
#
# 使用方式:
# var auth_manager = AuthManager.new()
# auth_manager.login_success.connect(_on_login_success)
# auth_manager.execute_password_login(username, password)
#
# 注意事项:
# - 这是业务逻辑层不包含任何UI相关代码
# - 通过信号与UI层通信
# - 所有验证逻辑都在这里实现
# ============================================================================
extends RefCounted
# ============ 信号定义 ============
# 登录成功信号
signal login_success(username: String)
# 登录失败信号
signal login_failed(message: String)
# 注册成功信号
signal register_success(message: String)
# 注册失败信号
signal register_failed(message: String)
# 验证码发送成功信号
signal verification_code_sent(message: String)
# 验证码发送失败信号
signal verification_code_failed(message: String)
# 表单验证失败信号
signal form_validation_failed(field: String, message: String)
# 网络状态变化信号
signal network_status_changed(is_connected: bool, message: String)
# 按钮状态变化信号
signal button_state_changed(button_name: String, is_loading: bool, text: String)
# Toast消息信号
signal show_toast_message(message: String, is_success: bool)
# ============ 枚举定义 ============
# 登录模式枚举
enum LoginMode {
PASSWORD, # 密码登录模式
VERIFICATION # 验证码登录模式
}
# ============ 成员变量 ============
# 登录状态
var current_login_mode: LoginMode = LoginMode.PASSWORD
var is_processing: bool = false
# 验证码管理
var verification_codes_sent: Dictionary = {}
var code_cooldown: float = 60.0
var current_email: String = ""
# 网络请求管理
var active_request_ids: Array = []
# ============ 生命周期方法 ============
# 初始化管理器
func _init():
print("AuthManager 初始化完成")
# 清理资源
func cleanup():
# 取消所有活动的网络请求
for request_id in active_request_ids:
NetworkManager.cancel_request(request_id)
active_request_ids.clear()
# ============ 登录相关方法 ============
# 执行密码登录
#
# 参数:
# username: String - 用户名/邮箱
# password: String - 密码
#
# 功能:
# - 验证输入参数
# - 发送登录请求
# - 处理响应结果
func execute_password_login(username: String, password: String):
if is_processing:
show_toast_message.emit("请等待当前操作完成", false)
return
# 验证输入
var validation_result = validate_login_inputs(username, password)
if not validation_result.valid:
form_validation_failed.emit(validation_result.field, validation_result.message)
return
# 设置处理状态
is_processing = true
button_state_changed.emit("main_btn", true, "登录中...")
show_toast_message.emit("正在验证登录信息...", true)
# 发送网络请求
var request_id = NetworkManager.login(username, password, _on_login_response)
if request_id != "":
active_request_ids.append(request_id)
else:
_reset_login_state()
show_toast_message.emit("网络请求失败", false)
# 执行验证码登录
#
# 参数:
# identifier: String - 用户标识符
# verification_code: String - 验证码
func execute_verification_login(identifier: String, verification_code: String):
if is_processing:
show_toast_message.emit("请等待当前操作完成", false)
return
# 验证输入
if identifier.is_empty():
form_validation_failed.emit("username", "请输入用户名/手机/邮箱")
return
if verification_code.is_empty():
form_validation_failed.emit("verification", "请输入验证码")
return
# 设置处理状态
is_processing = true
button_state_changed.emit("main_btn", true, "登录中...")
show_toast_message.emit("正在验证验证码...", 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:
_reset_login_state()
show_toast_message.emit("网络请求失败", false)
# 切换登录模式
func toggle_login_mode():
if current_login_mode == LoginMode.PASSWORD:
current_login_mode = LoginMode.VERIFICATION
else:
current_login_mode = LoginMode.PASSWORD
# 获取当前登录模式
func get_current_login_mode() -> LoginMode:
return current_login_mode
# ============ 注册相关方法 ============
# 执行用户注册
#
# 参数:
# username: String - 用户名
# email: String - 邮箱
# password: String - 密码
# confirm_password: String - 确认密码
# verification_code: String - 邮箱验证码
func execute_register(username: String, email: String, password: String, confirm_password: String, verification_code: String):
if is_processing:
show_toast_message.emit("请等待当前操作完成", false)
return
# 验证注册表单
var validation_result = validate_register_form(username, email, password, confirm_password, verification_code)
if not validation_result.valid:
form_validation_failed.emit(validation_result.field, validation_result.message)
return
# 设置处理状态
is_processing = true
button_state_changed.emit("register_btn", true, "注册中...")
show_toast_message.emit("正在创建账户...", true)
# 发送注册请求
var request_id = NetworkManager.register(username, password, username, email, verification_code, _on_register_response)
if request_id != "":
active_request_ids.append(request_id)
else:
_reset_register_state()
show_toast_message.emit("网络请求失败", false)
# ============ 验证码相关方法 ============
# 发送邮箱验证码
#
# 参数:
# email: String - 邮箱地址
func send_email_verification_code(email: String):
# 验证邮箱格式
var email_validation = validate_email(email)
if not email_validation.valid:
form_validation_failed.emit("email", email_validation.message)
return
# 检查冷却时间
if not _can_send_verification_code(email):
var remaining = get_remaining_cooldown_time(email)
show_toast_message.emit("该邮箱请等待 %d 秒后再次发送" % remaining, false)
return
# 记录发送状态
_record_verification_code_sent(email)
# 发送请求
var request_id = NetworkManager.send_email_verification(email, _on_send_code_response)
if request_id != "":
active_request_ids.append(request_id)
else:
_reset_verification_code_state(email)
show_toast_message.emit("网络请求失败", false)
# 发送登录验证码
#
# 参数:
# identifier: String - 用户标识符
func send_login_verification_code(identifier: String):
if identifier.is_empty():
form_validation_failed.emit("username", "请先输入用户名/手机/邮箱")
return
button_state_changed.emit("get_code_btn", true, "发送中...")
show_toast_message.emit("正在发送登录验证码...", 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:
button_state_changed.emit("get_code_btn", false, "获取验证码")
show_toast_message.emit("网络请求失败", false)
# 发送密码重置验证码
#
# 参数:
# identifier: String - 用户标识符
func send_password_reset_code(identifier: String):
if identifier.is_empty():
show_toast_message.emit("请先输入邮箱或手机号", false)
return
if not _is_valid_identifier(identifier):
show_toast_message.emit("请输入有效的邮箱或手机号", false)
return
button_state_changed.emit("forgot_password_btn", true, "发送中...")
show_toast_message.emit("正在发送密码重置验证码...", true)
var request_id = NetworkManager.forgot_password(identifier, _on_forgot_password_response)
if request_id != "":
active_request_ids.append(request_id)
else:
button_state_changed.emit("forgot_password_btn", false, "忘记密码")
show_toast_message.emit("网络请求失败", false)
# ============ 验证方法 ============
# 验证登录输入
func validate_login_inputs(username: String, password: String) -> Dictionary:
var result = {"valid": false, "field": "", "message": ""}
if username.is_empty():
result.field = "username"
result.message = "用户名不能为空"
return result
if password.is_empty():
result.field = "password"
result.message = "密码不能为空"
return result
result.valid = true
return result
# 验证注册表单
func validate_register_form(username: String, email: String, password: String, confirm_password: String, verification_code: String) -> Dictionary:
var result = {"valid": false, "field": "", "message": ""}
# 验证用户名
var username_validation = validate_username(username)
if not username_validation.valid:
result.field = "username"
result.message = username_validation.message
return result
# 验证邮箱
var email_validation = validate_email(email)
if not email_validation.valid:
result.field = "email"
result.message = email_validation.message
return result
# 验证密码
var password_validation = validate_password(password)
if not password_validation.valid:
result.field = "password"
result.message = password_validation.message
return result
# 验证确认密码
var confirm_validation = validate_confirm_password(password, confirm_password)
if not confirm_validation.valid:
result.field = "confirm"
result.message = confirm_validation.message
return result
# 验证验证码
var code_validation = validate_verification_code(verification_code)
if not code_validation.valid:
result.field = "verification"
result.message = code_validation.message
return result
# 检查是否已发送验证码
if not _has_sent_verification_code(email):
result.field = "verification"
result.message = "请先获取邮箱验证码"
return result
result.valid = true
return result
# 验证用户名
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_response(success: bool, data: Dictionary, error_info: Dictionary):
_reset_login_state()
var result = ResponseHandler.handle_login_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
if result.success:
var username = ""
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
username = data.data.user.username
# 延迟发送登录成功信号
await Engine.get_main_loop().create_timer(1.0).timeout
login_success.emit(username)
else:
login_failed.emit(result.message)
# 处理验证码登录响应
func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary):
_reset_login_state()
var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
if result.success:
var username = ""
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
username = data.data.user.username
await Engine.get_main_loop().create_timer(1.0).timeout
login_success.emit(username)
else:
login_failed.emit(result.message)
# 处理注册响应
func _on_register_response(success: bool, data: Dictionary, error_info: Dictionary):
_reset_register_state()
var result = ResponseHandler.handle_register_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
if result.success:
register_success.emit(result.message)
else:
register_failed.emit(result.message)
# 处理发送验证码响应
func _on_send_code_response(success: bool, data: Dictionary, error_info: Dictionary):
var result = ResponseHandler.handle_send_verification_code_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
if result.success:
verification_code_sent.emit(result.message)
else:
verification_code_failed.emit(result.message)
_reset_verification_code_state(current_email)
# 处理发送登录验证码响应
func _on_send_login_code_response(success: bool, data: Dictionary, error_info: Dictionary):
button_state_changed.emit("get_code_btn", false, "获取验证码")
var result = ResponseHandler.handle_send_login_code_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
# 处理忘记密码响应
func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary):
button_state_changed.emit("forgot_password_btn", false, "忘记密码")
var result = ResponseHandler.handle_send_login_code_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
# ============ 网络测试 ============
# 测试网络连接
func test_network_connection():
var request_id = NetworkManager.get_app_status(_on_network_test_response)
if request_id != "":
active_request_ids.append(request_id)
# 处理网络测试响应
func _on_network_test_response(success: bool, data: Dictionary, error_info: Dictionary):
var result = ResponseHandler.handle_network_test_response(success, data, error_info)
network_status_changed.emit(result.success, result.message)
# ============ 私有辅助方法 ============
# 重置登录状态
func _reset_login_state():
is_processing = false
button_state_changed.emit("main_btn", false, "进入小镇")
# 重置注册状态
func _reset_register_state():
is_processing = false
button_state_changed.emit("register_btn", false, "注册")
# 检查是否可以发送验证码
func _can_send_verification_code(email: String) -> bool:
if not verification_codes_sent.has(email):
return true
var email_data = verification_codes_sent[email]
if not email_data.sent:
return true
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
return (current_timestamp - email_data.time) >= code_cooldown
# 获取剩余冷却时间
func get_remaining_cooldown_time(email: String) -> int:
if not verification_codes_sent.has(email):
return 0
var email_data = verification_codes_sent[email]
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
return int(code_cooldown - (current_timestamp - email_data.time))
# 记录验证码发送状态
func _record_verification_code_sent(email: String):
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
if not verification_codes_sent.has(email):
verification_codes_sent[email] = {}
verification_codes_sent[email].sent = true
verification_codes_sent[email].time = current_timestamp
current_email = email
# 重置验证码状态
func _reset_verification_code_state(email: String):
if verification_codes_sent.has(email):
verification_codes_sent[email].sent = false
# 检查是否已发送验证码
func _has_sent_verification_code(email: String) -> bool:
if not verification_codes_sent.has(email):
return false
return verification_codes_sent[email].get("sent", false)
# 验证标识符格式
func _is_valid_identifier(identifier: String) -> bool:
return StringUtils.is_valid_email(identifier) or _is_valid_phone(identifier)
# 验证手机号格式
func _is_valid_phone(phone: String) -> bool:
var regex = RegEx.new()
regex.compile("^\\+?[1-9]\\d{1,14}$")
return regex.search(phone) != null

View File

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

View File

@@ -1,50 +1,142 @@
extends Node
# 游戏管理器 - 全局游戏状态管理
# 单例模式,管理游戏的整体状态和生命周期
# ============================================================================
# GameManager.gd - 游戏管理器
# ============================================================================
# 全局单例管理器,负责游戏状态管理和生命周期控制
#
# 核心职责:
# - 游戏状态切换 (加载、认证、游戏中、暂停等)
# - 用户信息管理
# - 全局配置访问
# - 系统初始化和清理
#
# 使用方式:
# GameManager.change_state(GameManager.GameState.IN_GAME)
# GameManager.set_current_user("player123")
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 状态变更会触发 game_state_changed 信号
# - 状态切换应该通过 change_state() 方法进行
# ============================================================================
# ============ 信号定义 ============
# 游戏状态变更信号
# 参数: new_state - 新的游戏状态
signal game_state_changed(new_state: GameState)
# ============ 枚举定义 ============
# 游戏状态枚举
# 定义了游戏的各种运行状态
enum GameState {
LOADING, # 加载中
AUTH, # 认证状态
MAIN_MENU, # 主菜单
IN_GAME, # 游戏中
PAUSED, # 暂停
SETTINGS # 设置
LOADING, # 加载中 - 游戏启动时的初始化状态
AUTH, # 认证状态 - 用户登录/注册界面
MAIN_MENU, # 主菜单 - 游戏主界面
IN_GAME, # 游戏中 - 正在进行游戏
PAUSED, # 暂停 - 游戏暂停状态
SETTINGS # 设置 - 设置界面
}
var current_state: GameState = GameState.LOADING
var previous_state: GameState = GameState.LOADING
var current_user: String = ""
var game_version: String = "1.0.0"
# ============ 成员变量 ============
# 状态管理
var current_state: GameState = GameState.LOADING # 当前游戏状态
var previous_state: GameState = GameState.LOADING # 上一个游戏状态
# 用户信息
var current_user: String = "" # 当前登录用户名
# 游戏配置
var game_version: String = "1.0.0" # 游戏版本号
# ============ 生命周期方法 ============
# 初始化游戏管理器
# 在节点准备就绪时调用,设置初始状态
func _ready():
print("GameManager 初始化完成")
change_state(GameState.AUTH)
change_state(GameState.AUTH) # 启动时进入认证状态
# ============ 状态管理方法 ============
# 切换游戏状态
#
# 参数:
# new_state: GameState - 要切换到的新状态
#
# 功能:
# - 检查状态是否需要切换
# - 记录状态变更历史
# - 发送状态变更信号
# - 输出状态变更日志
func change_state(new_state: GameState):
# 避免重复切换到相同状态
if current_state == new_state:
return
# 记录状态变更
previous_state = current_state
current_state = new_state
# 输出状态变更日志
print("游戏状态变更: ", GameState.keys()[previous_state], " -> ", GameState.keys()[current_state])
# 发送状态变更信号
game_state_changed.emit(new_state)
# 获取当前游戏状态
#
# 返回值:
# GameState - 当前的游戏状态
func get_current_state() -> GameState:
return current_state
# 获取上一个游戏状态
#
# 返回值:
# GameState - 上一个游戏状态
#
# 使用场景:
# - 从暂停状态恢复时,返回到之前的状态
# - 错误处理时回退到安全状态
func get_previous_state() -> GameState:
return previous_state
# ============ 用户管理方法 ============
# 设置当前登录用户
#
# 参数:
# username: String - 用户名
#
# 功能:
# - 存储当前登录用户信息
# - 输出用户设置日志
#
# 注意事项:
# - 用户登录成功后调用此方法
# - 用户登出时应传入空字符串
func set_current_user(username: String):
current_user = username
print("当前用户设置为: ", username)
# 获取当前登录用户
#
# 返回值:
# String - 当前登录的用户名,未登录时为空字符串
func get_current_user() -> String:
return current_user
# 检查用户是否已登录
#
# 返回值:
# bool - true表示已登录false表示未登录
#
# 使用场景:
# - 进入需要登录的功能前检查
# - UI显示逻辑判断
func is_user_logged_in() -> bool:
return not current_user.is_empty()

View File

@@ -1,45 +1,96 @@
extends Node
# 网络请求管理器 - 统一处理所有HTTP请求
# ============================================================================
# NetworkManager.gd - 网络请求管理器
# ============================================================================
# 全局单例管理器统一处理所有HTTP请求
#
# 核心职责:
# - 统一的HTTP请求接口 (GET, POST, PUT, DELETE, PATCH)
# - 认证相关API封装 (登录、注册、验证码等)
# - 请求状态管理和错误处理
# - 支持API v1.1.1规范的响应处理
#
# 使用方式:
# NetworkManager.login("user@example.com", "password", callback)
# var request_id = NetworkManager.get_request("/api/data", callback)
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 所有请求都是异步的,通过回调函数或信号处理结果
# - 支持请求超时和取消功能
# - 自动处理JSON序列化和反序列化
# ============================================================================
# 信号定义
# ============ 信号定义 ============
# 请求完成信号
# 参数:
# request_id: String - 请求唯一标识符
# success: bool - 请求是否成功
# data: Dictionary - 响应数据
signal request_completed(request_id: String, success: bool, data: Dictionary)
# 请求失败信号
# 参数:
# request_id: String - 请求唯一标识符
# error_type: String - 错误类型名称
# message: String - 错误消息
signal request_failed(request_id: String, error_type: String, message: String)
# API配置
# ============ 常量定义 ============
# API基础URL - 所有请求的根地址
const API_BASE_URL = "https://whaletownend.xinghangee.icu"
# 默认请求超时时间(秒)
const DEFAULT_TIMEOUT = 30.0
# 请求类型枚举
# ============ 枚举定义 ============
# HTTP请求方法枚举
enum RequestType {
GET,
POST,
PUT,
DELETE,
PATCH
GET, # 获取数据
POST, # 创建数据
PUT, # 更新数据
DELETE, # 删除数据
PATCH # 部分更新数据
}
# 错误类型枚举
# 用于分类不同类型的网络错误
enum ErrorType {
NETWORK_ERROR, # 网络连接错误
TIMEOUT_ERROR, # 请求超时
PARSE_ERROR, # JSON解析错误
HTTP_ERROR, # HTTP状态码错误
BUSINESS_ERROR # 业务逻辑错误
NETWORK_ERROR, # 网络连接错误 - 无法连接到服务器
TIMEOUT_ERROR, # 请求超时 - 服务器响应时间过长
PARSE_ERROR, # JSON解析错误 - 服务器返回格式错误
HTTP_ERROR, # HTTP状态码错误 - 4xx, 5xx状态码
BUSINESS_ERROR # 业务逻辑错误 - API返回的业务错误
}
# 请求状态
# ============ 请求信息类 ============
# 请求信息封装类
# 存储单个HTTP请求的所有相关信息
class RequestInfo:
var id: String
var url: String
var method: RequestType
var headers: PackedStringArray
var body: String
var timeout: float
var start_time: float
var http_request: HTTPRequest
var callback: Callable
var id: String # 请求唯一标识符
var url: String # 完整的请求URL
var method: RequestType # HTTP请求方法
var headers: PackedStringArray # 请求头数组
var body: String # 请求体内容
var timeout: float # 超时时间(秒)
var start_time: float # 请求开始时间戳
var http_request: HTTPRequest # Godot HTTPRequest节点引用
var callback: Callable # 完成时的回调函数
# 构造函数
#
# 参数:
# request_id: String - 请求唯一标识符
# request_url: String - 请求URL
# request_method: RequestType - HTTP方法
# request_headers: PackedStringArray - 请求头(可选)
# request_body: String - 请求体(可选)
# request_timeout: float - 超时时间可选默认使用DEFAULT_TIMEOUT
func _init(request_id: String, request_url: String, request_method: RequestType,
request_headers: PackedStringArray = [], request_body: String = "",
request_timeout: float = DEFAULT_TIMEOUT):
@@ -49,40 +100,107 @@ class RequestInfo:
headers = request_headers
body = request_body
timeout = request_timeout
# 记录请求开始时间(简化版时间戳)
start_time = Time.get_time_dict_from_system().hour * 3600 + Time.get_time_dict_from_system().minute * 60 + Time.get_time_dict_from_system().second
# 活动请求管理
var active_requests: Dictionary = {}
var request_counter: int = 0
# ============ 成员变量 ============
# 活动请求管理
var active_requests: Dictionary = {} # 存储所有活动请求 {request_id: RequestInfo}
var request_counter: int = 0 # 请求计数器用于生成唯一ID
# ============ 生命周期方法 ============
# 初始化网络管理器
# 在节点准备就绪时调用
func _ready():
print("NetworkManager 已初始化")
# ============ 公共API接口 ============
# 发送GET请求
#
# 参数:
# endpoint: String - API端点路径如: "/api/users"
# callback: Callable - 完成时的回调函数(可选)
# timeout: float - 超时时间可选默认30秒
#
# 返回值:
# String - 请求ID可用于取消请求或跟踪状态
#
# 使用示例:
# var request_id = NetworkManager.get_request("/api/users", my_callback)
func get_request(endpoint: String, callback: Callable = Callable(), timeout: float = DEFAULT_TIMEOUT) -> String:
return send_request(endpoint, RequestType.GET, [], "", callback, timeout)
# 发送POST请求
#
# 参数:
# endpoint: String - API端点路径
# data: Dictionary - 要发送的数据将自动转换为JSON
# callback: Callable - 完成时的回调函数(可选)
# timeout: float - 超时时间(可选)
#
# 返回值:
# String - 请求ID
#
# 使用示例:
# var data = {"name": "张三", "age": 25}
# var request_id = NetworkManager.post_request("/api/users", data, my_callback)
func post_request(endpoint: String, data: Dictionary, callback: Callable = Callable(), timeout: float = DEFAULT_TIMEOUT) -> String:
var body = JSON.stringify(data)
var headers = ["Content-Type: application/json"]
return send_request(endpoint, RequestType.POST, headers, body, callback, timeout)
# 发送PUT请求
#
# 参数:
# endpoint: String - API端点路径
# data: Dictionary - 要更新的数据
# callback: Callable - 完成时的回调函数(可选)
# timeout: float - 超时时间(可选)
#
# 返回值:
# String - 请求ID
func put_request(endpoint: String, data: Dictionary, callback: Callable = Callable(), timeout: float = DEFAULT_TIMEOUT) -> String:
var body = JSON.stringify(data)
var headers = ["Content-Type: application/json"]
return send_request(endpoint, RequestType.PUT, headers, body, callback, timeout)
# 发送DELETE请求
#
# 参数:
# endpoint: String - API端点路径
# callback: Callable - 完成时的回调函数(可选)
# timeout: float - 超时时间(可选)
#
# 返回值:
# String - 请求ID
func delete_request(endpoint: String, callback: Callable = Callable(), timeout: float = DEFAULT_TIMEOUT) -> String:
return send_request(endpoint, RequestType.DELETE, [], "", callback, timeout)
# ============ 认证相关API ============
# 用户登录
#
# 参数:
# identifier: String - 用户标识符(邮箱或手机号)
# password: String - 用户密码
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 回调函数签名:
# func callback(success: bool, data: Dictionary, error_info: Dictionary)
#
# 使用示例:
# NetworkManager.login("user@example.com", "password123", func(success, data, error):
# if success:
# print("登录成功: ", data)
# else:
# print("登录失败: ", error.message)
# )
func login(identifier: String, password: String, callback: Callable = Callable()) -> String:
var data = {
"identifier": identifier,
@@ -91,6 +209,18 @@ func login(identifier: String, password: String, callback: Callable = Callable()
return post_request("/auth/login", data, callback)
# 验证码登录
#
# 参数:
# identifier: String - 用户标识符(邮箱或手机号)
# verification_code: String - 验证码
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 使用场景:
# - 用户忘记密码时的替代登录方式
# - 提供更安全的登录选项
func verification_code_login(identifier: String, verification_code: String, callback: Callable = Callable()) -> String:
var data = {
"identifier": identifier,
@@ -99,11 +229,39 @@ func verification_code_login(identifier: String, verification_code: String, call
return post_request("/auth/verification-code-login", data, callback)
# 发送登录验证码
#
# 参数:
# identifier: String - 用户标识符(邮箱或手机号)
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 向已注册用户发送登录验证码
# - 支持邮箱和手机号
# - 有频率限制保护
func send_login_verification_code(identifier: String, callback: Callable = Callable()) -> String:
var data = {"identifier": identifier}
return post_request("/auth/send-login-verification-code", data, callback)
# 用户注册
#
# 参数:
# username: String - 用户名
# password: String - 密码
# nickname: String - 昵称
# email: String - 邮箱地址(可选)
# email_verification_code: String - 邮箱验证码(可选)
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 注意事项:
# - 如果提供邮箱,建议同时提供验证码
# - 用户名和邮箱必须唯一
# - 密码需要符合安全要求
func register(username: String, password: String, nickname: String, email: String = "",
email_verification_code: String = "", callback: Callable = Callable()) -> String:
var data = {
@@ -112,6 +270,7 @@ func register(username: String, password: String, nickname: String, email: Strin
"nickname": nickname
}
# 可选参数处理
if email != "":
data["email"] = email
if email_verification_code != "":
@@ -120,11 +279,35 @@ func register(username: String, password: String, nickname: String, email: Strin
return post_request("/auth/register", data, callback)
# 发送邮箱验证码
#
# 参数:
# email: String - 邮箱地址
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 向指定邮箱发送验证码
# - 用于注册时的邮箱验证
# - 支持测试模式(开发环境)
func send_email_verification(email: String, callback: Callable = Callable()) -> String:
var data = {"email": email}
return post_request("/auth/send-email-verification", data, callback)
# 验证邮箱
#
# 参数:
# email: String - 邮箱地址
# verification_code: String - 验证码
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 验证邮箱验证码的有效性
# - 通常在注册流程中使用
func verify_email(email: String, verification_code: String, callback: Callable = Callable()) -> String:
var data = {
"email": email,
@@ -133,20 +316,66 @@ func verify_email(email: String, verification_code: String, callback: Callable =
return post_request("/auth/verify-email", data, callback)
# 获取应用状态
#
# 参数:
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 检查API服务器状态
# - 获取应用基本信息
# - 用于网络连接测试
func get_app_status(callback: Callable = Callable()) -> String:
return get_request("/", callback)
# 重新发送邮箱验证码
#
# 参数:
# email: String - 邮箱地址
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 使用场景:
# - 用户未收到验证码时重新发送
# - 验证码过期后重新获取
func resend_email_verification(email: String, callback: Callable = Callable()) -> String:
var data = {"email": email}
return post_request("/auth/resend-email-verification", data, callback)
# 忘记密码 - 发送重置验证码
#
# 参数:
# identifier: String - 用户标识符(邮箱或手机号)
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 向用户发送密码重置验证码
# - 用于密码找回流程的第一步
func forgot_password(identifier: String, callback: Callable = Callable()) -> String:
var data = {"identifier": identifier}
return post_request("/auth/forgot-password", data, callback)
# 重置密码
#
# 参数:
# identifier: String - 用户标识符
# verification_code: String - 重置验证码
# new_password: String - 新密码
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 使用验证码重置用户密码
# - 密码找回流程的第二步
func reset_password(identifier: String, verification_code: String, new_password: String, callback: Callable = Callable()) -> String:
var data = {
"identifier": identifier,
@@ -156,6 +385,19 @@ func reset_password(identifier: String, verification_code: String, new_password:
return post_request("/auth/reset-password", data, callback)
# 修改密码
#
# 参数:
# user_id: String - 用户ID
# old_password: String - 旧密码
# new_password: String - 新密码
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 已登录用户修改密码
# - 需要验证旧密码
func change_password(user_id: String, old_password: String, new_password: String, callback: Callable = Callable()) -> String:
var data = {
"user_id": user_id,
@@ -165,6 +407,21 @@ func change_password(user_id: String, old_password: String, new_password: String
return put_request("/auth/change-password", data, callback)
# GitHub OAuth登录
#
# 参数:
# github_id: String - GitHub用户ID
# username: String - GitHub用户名
# nickname: String - 显示昵称
# email: String - GitHub邮箱
# avatar_url: String - 头像URL可选
# callback: Callable - 完成时的回调函数(可选)
#
# 返回值:
# String - 请求ID
#
# 功能:
# - 通过GitHub账号登录或注册
# - 支持第三方OAuth认证
func github_login(github_id: String, username: String, nickname: String, email: String, avatar_url: String = "", callback: Callable = Callable()) -> String:
var data = {
"github_id": github_id,
@@ -173,6 +430,7 @@ func github_login(github_id: String, username: String, nickname: String, email:
"email": email
}
# 可选头像URL
if avatar_url != "":
data["avatar_url"] = avatar_url
@@ -440,4 +698,4 @@ func get_request_info(request_id: String) -> Dictionary:
func _notification(what):
if what == NOTIFICATION_WM_CLOSE_REQUEST:
# 应用关闭时取消所有请求
cancel_all_requests()
cancel_all_requests()

View File

@@ -587,4 +587,4 @@ func handle_response(operation_type: String, success: bool, data: Dictionary, er
result.success = false
result.message = _get_error_message(data.get("error_code", ""), data.get("message", "操作失败"), error_info)
result.toast_type = "error"
return result
return result

View File

@@ -1,33 +1,96 @@
extends Node
# 场景管理器 - 负责场景切换和管理
# 提供场景切换的统一接口
# ============================================================================
# SceneManager.gd - 场景管理器
# ============================================================================
# 全局单例管理器,负责场景切换和管理
#
# 核心职责:
# - 场景切换的统一接口
# - 场景路径映射管理
# - 场景切换过渡效果
# - 场景状态跟踪
#
# 使用方式:
# SceneManager.change_scene("main")
# SceneManager.register_scene("custom", "res://scenes/custom.tscn")
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 场景切换是异步操作,支持过渡效果
# - 场景名称必须在 scene_paths 中注册
# ============================================================================
# ============ 信号定义 ============
# 场景切换完成信号
# 参数: scene_name - 切换到的场景名称
signal scene_changed(scene_name: String)
# 场景切换开始信号
# 参数: scene_name - 即将切换到的场景名称
signal scene_change_started(scene_name: String)
var current_scene_name: String = ""
var is_changing_scene: bool = false
# ============ 成员变量 ============
# 场景路径映射
# 场景状态
var current_scene_name: String = "" # 当前场景名称
var is_changing_scene: bool = false # 是否正在切换场景
# 场景路径映射表
# 将场景名称映射到实际的文件路径
# 便于统一管理和修改场景路径
var scene_paths: Dictionary = {
"main": "res://Scenes/Maps/main_scene.tscn",
"auth": "res://UI/Windows/LoginWindow.tscn",
"game": "res://Scenes/Maps/game_scene.tscn",
"battle": "res://Scenes/Maps/battle_scene.tscn",
"inventory": "res://UI/Windows/InventoryWindow.tscn",
"shop": "res://UI/Windows/ShopWindow.tscn",
"settings": "res://UI/Windows/SettingsWindow.tscn"
"main": "res://scenes/MainScene.tscn", # 主场景 - 游戏入口
"auth": "res://scenes/ui/LoginWindow.tscn", # 认证场景 - 登录窗口
"game": "res://scenes/maps/game_scene.tscn", # 游戏场景 - 主要游戏内容
"battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统
"inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面
"shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面
"settings": "res://scenes/ui/SettingsWindow.tscn" # 设置界面
}
# ============ 生命周期方法 ============
# 初始化场景管理器
# 在节点准备就绪时调用
func _ready():
print("SceneManager 初始化完成")
# ============ 场景切换方法 ============
# 切换到指定场景
#
# 参数:
# scene_name: String - 要切换到的场景名称必须在scene_paths中注册
# use_transition: bool - 是否使用过渡效果默认为true
#
# 返回值:
# bool - 切换是否成功
#
# 功能:
# - 检查场景切换状态和场景是否存在
# - 显示过渡效果(可选)
# - 执行场景切换
# - 更新当前场景状态
# - 发送相关信号
#
# 使用示例:
# var success = SceneManager.change_scene("main", true)
# if success:
# print("场景切换成功")
#
# 注意事项:
# - 场景切换是异步操作
# - 切换过程中会阻止新的切换请求
# - 场景名称必须预先注册
func change_scene(scene_name: String, use_transition: bool = true):
# 防止重复切换
if is_changing_scene:
print("场景切换中,忽略新的切换请求")
return false
# 检查场景是否存在
if not scene_paths.has(scene_name):
print("错误: 未找到场景 ", scene_name)
return false
@@ -35,40 +98,89 @@ func change_scene(scene_name: String, use_transition: bool = true):
var scene_path = scene_paths[scene_name]
print("开始切换场景: ", current_scene_name, " -> ", scene_name)
# 设置切换状态
is_changing_scene = true
scene_change_started.emit(scene_name)
# 显示过渡效果
if use_transition:
await show_transition()
# 执行场景切换
var error = get_tree().change_scene_to_file(scene_path)
if error != OK:
print("场景切换失败: ", error)
is_changing_scene = false
return false
# 更新状态
current_scene_name = scene_name
is_changing_scene = false
scene_changed.emit(scene_name)
# 隐藏过渡效果
if use_transition:
await hide_transition()
print("场景切换完成: ", scene_name)
return true
# ============ 查询方法 ============
# 获取当前场景名称
#
# 返回值:
# String - 当前场景的名称
func get_current_scene_name() -> String:
return current_scene_name
# ============ 场景注册方法 ============
# 注册新场景
#
# 参数:
# scene_name: String - 场景名称(用于切换时引用)
# scene_path: String - 场景文件路径
#
# 功能:
# - 将场景名称和路径添加到映射表
# - 支持运行时动态注册场景
#
# 使用示例:
# SceneManager.register_scene("boss_battle", "res://scenes/boss/boss_battle.tscn")
func register_scene(scene_name: String, scene_path: String):
scene_paths[scene_name] = scene_path
print("注册场景: ", scene_name, " -> ", scene_path)
# ============ 过渡效果方法 ============
# 显示场景切换过渡效果
#
# 功能:
# - 显示场景切换时的过渡动画
# - 为用户提供视觉反馈
#
# 注意事项:
# - 这是异步方法需要await等待完成
# - 当前实现为简单的延时,可扩展为复杂动画
#
# TODO: 实现淡入淡出、滑动等过渡效果
func show_transition():
# TODO: 实现场景切换过渡效果
print("显示场景切换过渡效果")
await get_tree().create_timer(0.2).timeout
# 隐藏场景切换过渡效果
#
# 功能:
# - 隐藏场景切换完成后的过渡动画
# - 恢复正常的游戏显示
#
# 注意事项:
# - 这是异步方法需要await等待完成
# - 与show_transition()配对使用
#
# TODO: 实现与show_transition()对应的隐藏效果
func hide_transition():
# TODO: 隐藏场景切换过渡效果
print("隐藏场景切换过渡效果")

View File

@@ -0,0 +1,234 @@
class_name ToastManager
# ============================================================================
# ToastManager.gd - Toast消息管理器
# ============================================================================
# 负责创建和管理Toast消息的显示
#
# 核心功能:
# - 创建Toast消息实例
# - 管理Toast动画和生命周期
# - 支持多个Toast同时显示
# - 自动排列和清理Toast
# - 支持中文字体显示
#
# 使用方式:
# var toast_manager = ToastManager.new()
# toast_manager.setup(toast_container)
# toast_manager.show_toast("消息内容", true)
#
# 注意事项:
# - 需要提供一个容器节点来承载Toast
# - 自动处理Toast的位置计算和动画
# - 支持Web平台的字体处理
# ============================================================================
extends RefCounted
# ============ 成员变量 ============
# Toast容器和管理
var toast_container: Control # Toast消息容器
var active_toasts: Array = [] # 当前显示的Toast消息列表
var toast_counter: int = 0 # Toast计数器用于生成唯一ID
# ============ 初始化方法 ============
# 设置Toast管理器
#
# 参数:
# container: Control - Toast消息的容器节点
func setup(container: Control):
toast_container = container
print("ToastManager 初始化完成")
# ============ 公共方法 ============
# 显示Toast消息
#
# 参数:
# message: String - 消息内容
# is_success: bool - 是否为成功消息(影响颜色)
func show_toast(message: String, is_success: bool = true):
if toast_container == null:
print("错误: toast_container 节点不存在")
return
print("显示Toast消息: ", message, " 成功: ", is_success)
_create_toast_instance(message, is_success)
# 清理所有Toast
func clear_all_toasts():
for toast in active_toasts:
if is_instance_valid(toast):
toast.queue_free()
active_toasts.clear()
# ============ 私有方法 ============
# 创建Toast实例
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 = toast_container.get_viewport().get_visible_rect().size.x
var final_x = toast_container.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 toast_container.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)
# Toast入场动画
func _animate_toast_in(toast_panel: Panel, final_x: float):
var tween = toast_container.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
# 等待3秒后开始退场动画
await toast_container.get_tree().create_timer(3.0).timeout
_animate_toast_out(toast_panel)
# Toast退场动画
func _animate_toast_out(toast_panel: Panel):
if not is_instance_valid(toast_panel):
return
var tween = toast_container.create_tween()
tween.set_ease(Tween.EASE_IN)
tween.set_trans(Tween.TRANS_QUART)
var end_x = toast_container.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)
# 清理Toast
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()
# 重新排列Toast位置
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 = toast_container.create_tween()
tween.tween_property(toast, "position:y", current_y, 0.2)
current_y += toast.size.y + 15

View File

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

View File

@@ -1,44 +1,125 @@
extends Node
# 全局事件系统 - 提供解耦的事件通信机制
# 允许不同模块之间通过事件进行通信,避免直接依赖
# ============================================================================
# EventSystem.gd - 全局事件系统
# ============================================================================
# 全局单例管理器,提供解耦的事件通信机制
#
# 核心职责:
# - 事件监听器注册和管理
# - 事件发送和分发
# - 自动清理无效监听器
# - 支持带参数的事件通信
#
# 使用方式:
# EventSystem.connect_event("player_moved", _on_player_moved)
# EventSystem.emit_event("player_moved", {"position": Vector2(100, 200)})
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 监听器会自动检查目标节点的有效性
# - 建议使用EventNames类中定义的事件名称常量
# ============================================================================
# ============ 成员变量 ============
# 事件监听器存储
# 结构: {event_name: [{"callback": Callable, "target": Node}, ...]}
var event_listeners: Dictionary = {}
# ============ 生命周期方法 ============
# 初始化事件系统
# 在节点准备就绪时调用
func _ready():
print("EventSystem 初始化完成")
# ============ 事件监听器管理 ============
# 注册事件监听器
#
# 参数:
# event_name: String - 事件名称建议使用EventNames中的常量
# callback: Callable - 回调函数
# target: Node - 目标节点(可选,用于自动清理)
#
# 功能:
# - 将回调函数注册到指定事件
# - 支持同一事件多个监听器
# - 自动管理监听器生命周期
#
# 使用示例:
# EventSystem.connect_event(EventNames.PLAYER_MOVED, _on_player_moved, self)
#
# 注意事项:
# - 如果提供target参数当target节点被销毁时会自动清理监听器
# - 同一个callback可以监听多个事件
func connect_event(event_name: String, callback: Callable, target: Node = null):
# 初始化事件监听器数组
if not event_listeners.has(event_name):
event_listeners[event_name] = []
# 创建监听器信息
var listener_info = {
"callback": callback,
"target": target
}
# 添加到监听器列表
event_listeners[event_name].append(listener_info)
print("注册事件监听器: ", event_name, " -> ", callback)
# 移除事件监听器
#
# 参数:
# event_name: String - 事件名称
# callback: Callable - 要移除的回调函数
# target: Node - 目标节点(可选,用于精确匹配)
#
# 功能:
# - 从指定事件中移除特定的监听器
# - 支持精确匹配callback + target
#
# 使用示例:
# EventSystem.disconnect_event(EventNames.PLAYER_MOVED, _on_player_moved, self)
func disconnect_event(event_name: String, callback: Callable, target: Node = null):
if not event_listeners.has(event_name):
return
var listeners = event_listeners[event_name]
# 从后往前遍历,避免删除元素时索引问题
for i in range(listeners.size() - 1, -1, -1):
var listener = listeners[i]
# 匹配callback和target
if listener.callback == callback and listener.target == target:
listeners.remove_at(i)
print("移除事件监听器: ", event_name, " -> ", callback)
break
# ============ 事件发送 ============
# 发送事件
#
# 参数:
# event_name: String - 事件名称
# data: Variant - 事件数据(可选)
#
# 功能:
# - 向所有注册的监听器发送事件
# - 自动跳过无效的监听器
# - 支持任意类型的事件数据
#
# 使用示例:
# EventSystem.emit_event(EventNames.PLAYER_MOVED, {"position": Vector2(100, 200)})
# EventSystem.emit_event(EventNames.GAME_PAUSED) # 无数据事件
#
# 注意事项:
# - 事件发送是同步的,所有监听器会立即执行
# - 如果监听器执行出错,不会影响其他监听器
func emit_event(event_name: String, data: Variant = null):
print("发送事件: ", event_name, " 数据: ", data)
# 检查是否有监听器
if not event_listeners.has(event_name):
return
@@ -57,24 +138,58 @@ func emit_event(event_name: String, data: Variant = null):
else:
callback.call()
# ============ 维护方法 ============
# 清理无效的监听器
#
# 功能:
# - 遍历所有监听器,移除已销毁节点的监听器
# - 防止内存泄漏
# - 建议定期调用或在场景切换时调用
#
# 使用场景:
# - 场景切换时清理
# - 定期维护(如每分钟一次)
# - 内存优化时调用
func cleanup_invalid_listeners():
for event_name in event_listeners.keys():
var listeners = event_listeners[event_name]
# 从后往前遍历,避免删除元素时索引问题
for i in range(listeners.size() - 1, -1, -1):
var listener = listeners[i]
var target = listener.target
# 如果目标节点无效,移除监听器
if target != null and not is_instance_valid(target):
listeners.remove_at(i)
print("清理无效监听器: ", event_name)
# ============ 查询方法 ============
# 获取事件监听器数量
#
# 参数:
# event_name: String - 事件名称
#
# 返回值:
# int - 监听器数量
#
# 使用场景:
# - 调试时检查监听器数量
# - 性能分析
func get_listener_count(event_name: String) -> int:
if not event_listeners.has(event_name):
return 0
return event_listeners[event_name].size()
# 清空所有事件监听器
#
# 功能:
# - 移除所有已注册的事件监听器
# - 通常在游戏重置或退出时使用
#
# 警告:
# - 这是一个危险操作,会影响所有模块
# - 使用前请确保所有模块都能正确处理监听器丢失
func clear_all_listeners():
event_listeners.clear()
print("清空所有事件监听器")

143
_Core/systems/GridSystem.gd Normal file
View File

@@ -0,0 +1,143 @@
# ============================================================================
# 网格系统 - GridSystem.gd
#
# 提供32x32像素的最小网格单元控制用于规范地图大小和位置计算
#
# 使用方式:
# var grid_pos = GridSystem.world_to_grid(world_position)
# var world_pos = GridSystem.grid_to_world(grid_position)
# var snapped_pos = GridSystem.snap_to_grid(position)
# ============================================================================
class_name GridSystem
extends RefCounted
# ============================================================================
# 常量定义
# ============================================================================
const GRID_SIZE: int = 32 # 网格单元大小 32x32 像素
const HALF_GRID_SIZE: float = GRID_SIZE * 0.5 # 网格中心偏移
# ============================================================================
# 坐标转换方法
# ============================================================================
# 世界坐标转换为网格坐标
static func world_to_grid(world_pos: Vector2) -> Vector2i:
return Vector2i(
int(world_pos.x / GRID_SIZE),
int(world_pos.y / GRID_SIZE)
)
# 网格坐标转换为世界坐标(返回网格左上角)
static func grid_to_world(grid_pos: Vector2i) -> Vector2:
return Vector2(
grid_pos.x * GRID_SIZE,
grid_pos.y * GRID_SIZE
)
# 网格坐标转换为世界坐标(返回网格中心)
static func grid_to_world_center(grid_pos: Vector2i) -> Vector2:
return Vector2(
grid_pos.x * GRID_SIZE + HALF_GRID_SIZE,
grid_pos.y * GRID_SIZE + HALF_GRID_SIZE
)
# 将位置吸附到最近的网格点(左上角)
static func snap_to_grid(position: Vector2) -> Vector2:
return Vector2(
floor(position.x / GRID_SIZE) * GRID_SIZE,
floor(position.y / GRID_SIZE) * GRID_SIZE
)
# 将位置吸附到最近的网格中心
static func snap_to_grid_center(position: Vector2) -> Vector2:
var grid_pos = world_to_grid(position)
return grid_to_world_center(grid_pos)
# ============================================================================
# 距离和区域计算
# ============================================================================
# 计算两个网格坐标之间的曼哈顿距离
static func grid_distance_manhattan(grid_pos1: Vector2i, grid_pos2: Vector2i) -> int:
return abs(grid_pos1.x - grid_pos2.x) + abs(grid_pos1.y - grid_pos2.y)
# 计算两个网格坐标之间的欧几里得距离
static func grid_distance_euclidean(grid_pos1: Vector2i, grid_pos2: Vector2i) -> float:
var diff = grid_pos1 - grid_pos2
return sqrt(diff.x * diff.x + diff.y * diff.y)
# 获取指定网格坐标周围的邻居网格4方向
static func get_grid_neighbors_4(grid_pos: Vector2i) -> Array[Vector2i]:
return [
Vector2i(grid_pos.x, grid_pos.y - 1), # 上
Vector2i(grid_pos.x + 1, grid_pos.y), # 右
Vector2i(grid_pos.x, grid_pos.y + 1), # 下
Vector2i(grid_pos.x - 1, grid_pos.y) # 左
]
# 获取指定网格坐标周围的邻居网格8方向
static func get_grid_neighbors_8(grid_pos: Vector2i) -> Array[Vector2i]:
var neighbors: Array[Vector2i] = []
for x in range(-1, 2):
for y in range(-1, 2):
if x == 0 and y == 0:
continue
neighbors.append(Vector2i(grid_pos.x + x, grid_pos.y + y))
return neighbors
# ============================================================================
# 区域和边界检查
# ============================================================================
# 检查网格坐标是否在指定矩形区域内
static func is_grid_in_bounds(grid_pos: Vector2i, min_grid: Vector2i, max_grid: Vector2i) -> bool:
return (grid_pos.x >= min_grid.x and grid_pos.x <= max_grid.x and
grid_pos.y >= min_grid.y and grid_pos.y <= max_grid.y)
# 获取矩形区域内的所有网格坐标
static func get_grids_in_rect(min_grid: Vector2i, max_grid: Vector2i) -> Array[Vector2i]:
var grids: Array[Vector2i] = []
for x in range(min_grid.x, max_grid.x + 1):
for y in range(min_grid.y, max_grid.y + 1):
grids.append(Vector2i(x, y))
return grids
# ============================================================================
# 地图尺寸规范化
# ============================================================================
# 将像素尺寸规范化为网格尺寸的倍数
static func normalize_size_to_grid(pixel_size: Vector2i) -> Vector2i:
return Vector2i(
int(ceil(float(pixel_size.x) / GRID_SIZE)) * GRID_SIZE,
int(ceil(float(pixel_size.y) / GRID_SIZE)) * GRID_SIZE
)
# 计算指定像素尺寸需要多少个网格单元
static func get_grid_count(pixel_size: Vector2i) -> Vector2i:
return Vector2i(
int(ceil(float(pixel_size.x) / GRID_SIZE)),
int(ceil(float(pixel_size.y) / GRID_SIZE))
)
# ============================================================================
# 调试和可视化辅助
# ============================================================================
# 获取网格的边界矩形(用于调试绘制)
static func get_grid_rect(grid_pos: Vector2i) -> Rect2:
var world_pos = grid_to_world(grid_pos)
return Rect2(world_pos, Vector2(GRID_SIZE, GRID_SIZE))
# 打印网格信息(调试用)
static func print_grid_info(world_pos: Vector2) -> void:
var grid_pos = world_to_grid(world_pos)
var snapped_pos = snap_to_grid(world_pos)
var center_pos = grid_to_world_center(grid_pos)
print("世界坐标: ", world_pos)
print("网格坐标: ", grid_pos)
print("吸附位置: ", snapped_pos)
print("网格中心: ", center_pos)

View File

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

2
_Core/utils/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# 核心工具类目录
# 存放字符串处理、数学计算等工具类

386
_Core/utils/StringUtils.gd Normal file
View File

@@ -0,0 +1,386 @@
class_name StringUtils
# ============================================================================
# StringUtils.gd - 字符串工具类
# ============================================================================
# 静态工具类,提供常用的字符串处理功能
#
# 核心功能:
# - 输入验证(邮箱、用户名、密码)
# - 字符串格式化和转换
# - 时间格式化和相对时间计算
# - 文件大小格式化
#
# 使用方式:
# var is_valid = StringUtils.is_valid_email("user@example.com")
# var formatted_time = StringUtils.format_utc_to_local_time(utc_string)
#
# 注意事项:
# - 所有方法都是静态的,无需实例化
# - 验证方法返回布尔值或包含详细信息的字典
# - 时间处理方法支持UTC到本地时间的转换
# ============================================================================
# ============ 输入验证方法 ============
# 验证邮箱格式
#
# 参数:
# email: String - 待验证的邮箱地址
#
# 返回值:
# bool - true表示格式正确false表示格式错误
#
# 验证规则:
# - 必须包含@符号
# - @前后都必须有内容
# - 域名部分必须包含至少一个点
# - 顶级域名至少2个字符
#
# 使用示例:
# if StringUtils.is_valid_email("user@example.com"):
# print("邮箱格式正确")
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
# 验证用户名格式
#
# 参数:
# username: String - 待验证的用户名
#
# 返回值:
# bool - true表示格式正确false表示格式错误
#
# 验证规则:
# - 只能包含字母、数字、下划线
# - 长度不能为空且不超过50个字符
# - 不能包含空格或特殊字符
#
# 使用示例:
# if StringUtils.is_valid_username("user_123"):
# print("用户名格式正确")
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
# 验证密码强度
#
# 参数:
# password: String - 待验证的密码
#
# 返回值:
# Dictionary - 包含验证结果的详细信息
# {
# "valid": bool, # 是否符合最低要求
# "message": String, # 验证结果消息
# "strength": int # 强度等级 (1-4)
# }
#
# 验证规则:
# - 最少8位最多128位
# - 必须包含字母和数字
# - 强度评级:包含字母(+1)、数字(+1)、特殊字符(+1)、长度>=12(+1)
#
# 使用示例:
# var result = StringUtils.validate_password_strength("MyPass123!")
# if result.valid:
# print("密码强度: ", result.message)
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
# ============ 字符串格式化方法 ============
# 截断字符串
#
# 参数:
# text: String - 原始字符串
# max_length: int - 最大长度
# suffix: String - 截断后缀(默认为"..."
#
# 返回值:
# String - 截断后的字符串
#
# 功能:
# - 如果字符串长度超过限制,截断并添加后缀
# - 如果字符串长度未超过限制,返回原字符串
#
# 使用示例:
# var short_text = StringUtils.truncate("这是一个很长的文本", 10, "...")
# # 结果: "这是一个很长..."
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
# 首字母大写
#
# 参数:
# text: String - 原始字符串
#
# 返回值:
# String - 首字母大写的字符串
#
# 使用示例:
# var capitalized = StringUtils.capitalize_first("hello world")
# # 结果: "Hello world"
static func capitalize_first(text: String) -> String:
if text.is_empty():
return text
return text[0].to_upper() + text.substr(1)
# 转换为标题格式
#
# 参数:
# text: String - 原始字符串
#
# 返回值:
# String - 每个单词首字母大写的字符串
#
# 功能:
# - 将每个单词的首字母转换为大写
# - 其余字母转换为小写
# - 以空格分隔单词
#
# 使用示例:
# var title = StringUtils.to_title_case("hello world game")
# # 结果: "Hello World Game"
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标签
#
# 参数:
# html: String - 包含HTML标签的字符串
#
# 返回值:
# String - 移除HTML标签后的纯文本
#
# 功能:
# - 使用正则表达式移除所有HTML标签
# - 保留标签之间的文本内容
#
# 使用示例:
# var plain_text = StringUtils.strip_html_tags("<p>Hello <b>World</b></p>")
# # 结果: "Hello World"
static func strip_html_tags(html: String) -> String:
var regex = RegEx.new()
regex.compile("<[^>]*>")
return regex.sub(html, "", true)
# 格式化文件大小
#
# 参数:
# bytes: int - 文件大小(字节)
#
# 返回值:
# String - 格式化后的文件大小字符串
#
# 功能:
# - 自动选择合适的单位B, KB, MB, GB, TB
# - 保留一位小数(除了字节)
# - 使用1024作为换算基数
#
# 使用示例:
# var size_text = StringUtils.format_file_size(1536)
# # 结果: "1.5 KB"
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时间字符串转换为本地时间显示
#
# 参数:
# utc_time_str: String - UTC时间字符串格式: 2025-12-25T11:23:52.175Z
#
# 返回值:
# String - 格式化的本地时间字符串
#
# 功能:
# - 解析ISO 8601格式的UTC时间
# - 转换为本地时区时间
# - 格式化为易读的中文时间格式
#
# 使用示例:
# var local_time = StringUtils.format_utc_to_local_time("2025-12-25T11:23:52.175Z")
# # 结果: "2025年12月25日 19:23:52" (假设本地时区为UTC+8)
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
]
# 获取相对时间描述
#
# 参数:
# utc_time_str: String - UTC时间字符串
#
# 返回值:
# String - 相对时间描述(如"5分钟后"、"2小时30分钟后"
#
# 功能:
# - 计算指定时间与当前时间的差值
# - 返回人性化的相对时间描述
# - 支持秒、分钟、小时的组合显示
#
# 使用示例:
# var relative_time = StringUtils.get_relative_time_until("2025-12-25T12:00:00Z")
# # 结果: "30分钟后" 或 "现在可以重试"
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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rwawg1rprjtq"
path="res://.godot/imported/datawhale_house_asset_1.png-1892bbe725ca0e23a398f090f9971251.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_asset_1.png"
dest_files=["res://.godot/imported/datawhale_house_asset_1.png-1892bbe725ca0e23a398f090f9971251.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ctq2aeb3vbo5f"
path="res://.godot/imported/datawhale_house_single_story_enhanced.png-0676f1c2301056dd7cf2d93eb21a3a60.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_single_story_enhanced.png"
dest_files=["res://.godot/imported/datawhale_house_single_story_enhanced.png-0676f1c2301056dd7cf2d93eb21a3a60.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b43a04p4lsnb8"
path="res://.godot/imported/datawhale_house_three_story_1.png-a40630de3429bf9e1bf1cd3532ecf56c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_three_story_1.png"
dest_files=["res://.godot/imported/datawhale_house_three_story_1.png-a40630de3429bf9e1bf1cd3532ecf56c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvl4gndee3t0n"
path="res://.godot/imported/datawhale_house_three_story_2.png-df2ee031e5447ffbed65ac6bac803db9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_three_story_2.png"
dest_files=["res://.godot/imported/datawhale_house_three_story_2.png-df2ee031e5447ffbed65ac6bac803db9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csc5qyjif3vl"
path="res://.godot/imported/datawhale_house_two_story_1.png-76e2778208e3479c04c73b08468c76d4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_two_story_1.png"
dest_files=["res://.godot/imported/datawhale_house_two_story_1.png-76e2778208e3479c04c73b08468c76d4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmgfv1cftsme"
path="res://.godot/imported/datawhale_house_two_story_2.png-bb0f6234ed095fde7809a3e6b884addd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_two_story_2.png"
dest_files=["res://.godot/imported/datawhale_house_two_story_2.png-bb0f6234ed095fde7809a3e6b884addd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cf7x0l67wunuk"
path="res://.godot/imported/datawhale_house_variation_1.png-b41961ae39e80d59edd846cc251e1858.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_1.png"
dest_files=["res://.godot/imported/datawhale_house_variation_1.png-b41961ae39e80d59edd846cc251e1858.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cj7keute3ukqg"
path="res://.godot/imported/datawhale_house_variation_2.png-aadb0ada84a92f6854549c6ed9c7d472.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_2.png"
dest_files=["res://.godot/imported/datawhale_house_variation_2.png-aadb0ada84a92f6854549c6ed9c7d472.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmqs5tttttfu2"
path="res://.godot/imported/datawhale_house_variation_3.png-f112b3d4595f7f8d227d991047cc4611.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_3.png"
dest_files=["res://.godot/imported/datawhale_house_variation_3.png-f112b3d4595f7f8d227d991047cc4611.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://btr8upx4fwrv0"
path="res://.godot/imported/datawhale_house_variation_4.png-2571fe57a76b75a1da89191cfe4e8fd7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_4.png"
dest_files=["res://.godot/imported/datawhale_house_variation_4.png-2571fe57a76b75a1da89191cfe4e8fd7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ck71qx8lrrn18"
path="res://.godot/imported/datawhale_house_variation_5.png-59996e43196ff3a448d95fd9ffa3d256.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_5.png"
dest_files=["res://.godot/imported/datawhale_house_variation_5.png-59996e43196ff3a448d95fd9ffa3d256.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cuafv1aw6ig1v"
path="res://.godot/imported/datawhale_house_variation_6.png-a54f6449146245b220dbb331d353358d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/building/datawhale_house_variation_6.png"
dest_files=["res://.godot/imported/datawhale_house_variation_6.png-a54f6449146245b220dbb331d353358d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2gci3tcylfiw"
path="res://.godot/imported/curb.png-aea973bea0e48d7135256b05941024a3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/curb.png"
dest_files=["res://.godot/imported/curb.png-aea973bea0e48d7135256b05941024a3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djmpsp6t8vbra"
path="res://.godot/imported/download_1767426187137.png-a7252aa9f644c4f3ab14cefb1a59847c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/download_1767426187137.png"
dest_files=["res://.godot/imported/download_1767426187137.png-a7252aa9f644c4f3ab14cefb1a59847c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3yr7cietnip3"
path="res://.godot/imported/floor_tile.png-922ec9c726f71491a3ebe25e6696192d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/floor_tile.png"
dest_files=["res://.godot/imported/floor_tile.png-922ec9c726f71491a3ebe25e6696192d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://7o0xyqmqbvov"
path="res://.godot/imported/square.png-f3b8edd32d9382a7b98d24fd60e1b771.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/square.png"
dest_files=["res://.godot/imported/square.png-f3b8edd32d9382a7b98d24fd60e1b771.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt33hewme0p1k"
path="res://.godot/imported/square1.png-5d845f041b32e4a2880ddc03c7e210e2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/square1.png"
dest_files=["res://.godot/imported/square1.png-5d845f041b32e4a2880ddc03c7e210e2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ignbtjvnp5k7"
path="res://.godot/imported/广场瓦片集.png-b224b40553b9f690e690f67a89e2b520.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/广场瓦片集.png"
dest_files=["res://.godot/imported/广场瓦片集.png-b224b40553b9f690e690f67a89e2b520.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dvsb51jintro"
path="res://.godot/imported/草地.png-2fa7f2346d7dc837788dd21e5693cec7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/草地.png"
dest_files=["res://.godot/imported/草地.png-2fa7f2346d7dc837788dd21e5693cec7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -1 +0,0 @@
# 保持目录结构 - 角色精灵资源目录

View File

@@ -1 +0,0 @@
# 保持目录结构 - 特效精灵资源目录

View File

@@ -1 +0,0 @@
# 保持目录结构 - 环境精灵资源目录

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

100
claude.md Normal file
View File

@@ -0,0 +1,100 @@
# 🎯 CLAUDE.md - WhaleTown Project Instructions
## 1. Project Vision & Context
- **Project**: "WhaleTown" - A 2D top-down pixel art RPG.
- **Engine**: Godot 4.5+ (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**:
```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)
```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()

123
cloude.md
View File

@@ -1,123 +0,0 @@
# 🎯 WhaleTown 项目开发规范
## 1. 项目愿景与背景
- **项目名称**: "WhaleTown" - 一个2D俯视角像素风RPG游戏
- **引擎**: Godot 4.2+ (严格禁止使用Godot 3.x语法)
- **架构**: 严格分层架构:`_Core`(框架层)、`Scenes`(游戏层)、`UI`(界面层)
- **核心原则**: "信号向上,调用向下"。通过`EventSystem`实现高度解耦
## 2. 🛠 命令参考与设置
- **输入映射 (必需配置)**:
- `move_left`, `move_right`, `move_up`, `move_down` (WASD / 方向键)
- `interact` (E键 / 空格键)
- `pause` (ESC键)
- **运行游戏**: `godot --path .`
- **运行测试 (GUT)**: `godot --headless -s addons/gut/gut_cmdline.gd -gdir=res://tests/ -ginclude_subdirs`
- **初始化结构**: `mkdir -p _Core/managers _Core/systems Scenes/Maps Scenes/Entities Scenes/Components UI/Windows UI/HUD Assets/Sprites tests/unit tests/integration`
## 3. 📂 文件路径规则 (严格小写)
*注意:根目录文件夹必须小写。脚本和场景必须放在一起。*
- **核心管理器**: `_Core/managers/[Name].gd`
- **核心系统**: `_Core/systems/[Name].gd`
- **实体**: `Scenes/Entities/[EntityName]/[EntityName].tscn` (脚本`.gd`放在同一文件夹)
- **地图**: `Scenes/Maps/[map_name].tscn`
- **组件**: `Scenes/Components/[ComponentName].gd` (可复用逻辑节点)
- **UI窗口**: `UI/Windows/[WindowName].tscn`
- **测试**: `tests/[unit|integration]/test_[name].gd` (文件夹名为小写`tests`)
## 4. 📋 编码标准 (必须遵守)
- **类型安全**: 始终使用严格静态类型:`var speed: float = 100.0`, `func _ready() -> void`
- **命名约定**:
- 每个脚本顶部必须有`class_name PascalCase`
- 变量/函数:`snake_case`。常量:`SCREAMING_SNAKE_CASE`
- 私有成员:使用下划线前缀`_` (例如:`var _health: int`)
- **节点访问**: 对UI和内部场景组件使用`%UniqueName`
- **信号**: 使用"信号向上,调用向下"原则。父节点调用子节点方法;子节点发出信号
- **禁止模式**:
- ❌ 禁止使用`yield()` -> 使用`await`
- ❌ 禁止在`_process`中使用`get_node()` -> 使用`@onready`缓存
- ❌ 禁止线性过滤 -> 所有Sprite2D/TileMap资源必须使用**最近邻**过滤
## 5. 🏛 架构与通信
- **事件系统**: 使用`_Core/systems/EventSystem.gd`进行跨模块消息传递
- **事件注册**: 在`_Core/EventNames.gd`中使用`class_name EventNames`
```gdscript
class_name EventNames
const PLAYER_MOVED = "player_moved"
const INTERACT_PRESSED = "interact_pressed"
const NPC_TALKED = "npc_talked"
```
- **单例**: 只允许GameManager、SceneManager、EventSystem作为自动加载
- **解耦**: 底层实体不得直接引用GameManager。使用事件系统
## 6. 🏗 实现细节
- **玩家**: 使用CharacterBody2D。必须包含Camera2D设置`position_smoothing_enabled = true`
- **NPC/交互物**: 使用名为InteractionArea的Area2D。通过EventSystem触发
- **TileMap图层**:
- 图层0地面 (无碰撞)
- 图层1障碍物 (启用物理层)
- 图层2装饰 (启用Y排序)
- **相机**: 必须通过`TileMap.get_used_rect()`自动计算边界
## 7. 🧪 测试要求 (强制性)
- **覆盖率**: `_Core/`中的每个管理器/系统都必须有GUT测试
- **命名**: 测试文件必须以`test_`开头并继承`GutTest`
- **示例**:
```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. 🧘 开发哲学
- **流畅体验**: 每个交互(UI弹窗、NPC对话)都必须有Tween动画占位符
- **零魔法数字**: 所有速度/计时器必须使用`@export`或在`Config/`中定义
- **简洁性**: 如果一个函数做两件事,就拆分它
- **隐藏逻辑**: 隐藏的逻辑(如ResponseHandler.gd)必须和HUD一样干净
## 9. 📝 代码模板 (实体模式)
```gdscript
extends CharacterBody2D
class_name Player
# 1. 导出变量与常量
@export var move_speed: float = 200.0
# 2. 节点引用
@onready var sprite: Sprite2D = %Sprite2D
# 3. 生命周期
func _physics_process(delta: float) -> void:
_move(delta)
# 4. 私有方法
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()
```
## 10. 🎨 UI设计规范
- **响应式布局**: 使用Anchor和Margin实现自适应
- **主题统一**: 所有UI组件使用统一主题资源
- **动画效果**: 界面切换必须有过渡动画
- **无障碍支持**: 支持键盘导航
## 11. 🔧 性能优化
- **对象池**: 频繁创建销毁的对象使用对象池
- **视锥剔除**: 只渲染可见区域的对象
- **批量处理**: 合并相似的渲染调用
- **内存管理**: 及时释放不需要的资源
## 12. 📚 最佳实践
- **模块化**: 功能拆分为独立模块
- **可测试**: 设计易于测试的代码结构
- **文档化**: 为复杂逻辑添加详细注释
- **版本控制**: 遵循Git提交规范
---
**记住:代码质量是游戏成功的基础!**

View File

@@ -20,15 +20,12 @@
```
WhaleTown/
├── 🔧 _Core/ # [框架层] 核心系统和管理器
├── 🎬 scenes/ # [游戏层] 游戏场景和组件
├── 🎨 UI/ # [界面层] 用户界面
├── 🖼 assets/ # [资源层] 美术资源
├── ⚙️ Config/ # [配置层] 游戏配置和数据
├── 🛠️ Utils/ # [工具层] 通用工具脚本
├── 🧪 tests/ # [测试层] 测试文件
├── 🚀 tools/ # [构建层] 构建和部署工具
├── 🌐 web_assets/ # [发布层] Web版本资源
├── 🔧 _Core/ # [核心层] 功能实现与组件实现,项目最基本的底层实现
├── 🎬 scenes/ # [场景层] 场景与视觉呈现,包含地图、人物等视觉部分
├── 🎨 assets/ # [资源层] 静态资源存储,包括图片、音乐、视频、贴图等
├── Config/ # [配置层] 配置文件管理,用于配置各类环境
├── 🧪 tests/ # [测试层] 测试文件系统,放置所有组件的测试代码
├── 🌐 web_assets/ # [发布层] Web导出资源专门用于Web平台导出
└── 📚 docs/ # [文档层] 项目文档
```
@@ -36,9 +33,9 @@ WhaleTown/
## 📁 详细目录结构
### 1. 🔧 框架层 (_Core/)
### 1. 🔧 核心层 (_Core/)
> **负责团队**: 开发团队
> **职责**: 游戏底层框架,全局管理器和核心系统
> **职责**: 功能实现与组件实现,项目最基本的底层实现
```
_Core/
@@ -47,15 +44,25 @@ _Core/
│ ├── SceneManager.gd # 场景切换管理
│ ├── NetworkManager.gd # 网络通信管理
│ └── ResponseHandler.gd # API响应处理
── systems/ # 核心系统
└── EventSystem.gd # 全局事件系统
── systems/ # 核心系统
└── EventSystem.gd # 全局事件系统
├── components/ # 基础组件实现
│ ├── BaseCharacter.gd # 基础角色组件
│ ├── BaseItem.gd # 基础物品组件
│ └── BaseUI.gd # 基础UI组件
├── utils/ # 🔨 核心工具类
│ ├── StringUtils.gd # 字符串处理工具
│ ├── MathUtils.gd # 数学计算工具
│ └── PixelUtils.gd # 像素风游戏专用工具
├── EventNames.gd # 事件名称定义
└── ProjectPaths.gd # 路径统一管理
```
**特点**:
- 自动加载 (AutoLoad) 单例
- 全局可访问
- 与具体游戏逻辑无关
- 提供基础服务
- 全局可访问的核心功能
- 与具体游戏逻辑无关的底层实现
- 提供基础服务和组件框架
**使用示例**:
```gdscript
@@ -69,167 +76,208 @@ SceneManager.change_scene("main")
EventSystem.emit_event("player_moved", {"position": Vector2(100, 200)})
```
### 2. 🎬 游戏层 (scenes/)
### 2. 🎬 场景层 (scenes/)
> **负责团队**: 开发团队
> **职责**: 游戏场景、实体和可复用组件
> **职责**: 场景与视觉呈现,包含地图场景、人物场景等一系列视觉呈现的部分
```
scenes/
├── Maps/ # 游戏地图场景
├── main_scene.tscn # 主游戏场景
│ └── MainScene.gd # 主场景脚本
└── Components/ # 可复用组件
├── characters/ # 角色组件
── effects/ # 特效组件
├── items/ # 物品组件
── ui/ # UI组件
├── MainScene.tscn # 🎯 主入口场景 - 所有图像显示的入口文件
├── MainScene.gd # 主场景控制器脚本
├── maps/ # 地图场景
│ ├── main_world.tscn # 主世界地图
├── dungeon_01.tscn # 地牢场景
── town_center.tscn # 城镇中心
├── characters/ # 人物场景
── player/ # 玩家角色
│ │ ├── Player.tscn # 玩家场景
│ │ └── Player.gd # 玩家脚本
│ ├── npcs/ # NPC角色
│ └── enemies/ # 敌人角色
├── ui/ # UI界面场景
│ ├── menus/ # 菜单界面
│ ├── hud/ # 游戏HUD
│ └── dialogs/ # 对话框
├── effects/ # 特效场景
│ ├── particles/ # 粒子效果
│ └── animations/ # 动画效果
└── prefabs/ # 预制体组件
├── items/ # 物品预制体
└── interactive/ # 交互对象预制体
```
**设计原则**:
- **场景内聚**: 脚本紧邻场景文件存放
- **组件化**: 可复用组件统一管理
- **模块化**: 按功能类型分类组织
- **分类明确**: 按功能类型地图、人物、UI、特效分类
- **模块化**: 可复用的预制体统一管理
- **视觉导向**: 主要负责游戏的视觉呈现和UI实现
### 3. 🎨 界面层 (UI/)
> **负责团队**: 开发团队 + 美术团队协作
> **职责**: 用户界面和主题管理
```
UI/
├── Windows/ # 模态窗口
│ ├── LoginWindow.tscn # 登录窗口
│ └── AuthScene.gd # 认证场景脚本
└── Theme/ # 全局主题
├── MainTheme.tres # 主题资源
└── Fonts/ # 字体文件
└── msyh.ttc # 微软雅黑字体
```
**组织方式**:
- **Windows/**: 模态窗口和对话框
- **Theme/**: 统一的UI主题和样式
- **Fonts/**: 字体资源管理
### 4. 🖼️ 资源层 (assets/)
### 3. 🎨 资源层 (assets/)
> **负责团队**: 美术团队
> **职责**: 所有美术资源和素材
> **职责**: 所有静态资源的存储,包括图片、音乐、视频、贴图等素材
```
assets/
├── sprites/ # 精灵图资源
│ ├── characters/ # 角色精灵
│ ├── effects/ # 特效精灵
│ ├── environment/ # 环境精灵
── icon/ # 图标资源
├── sprites/ # 精灵图资源
│ ├── characters/ # 角色精灵玩家、NPC、敌人
│ ├── environment/ # 环境精灵(地形、建筑、装饰)
│ ├── items/ # 物品精灵(道具、装备、收集品)
── effects/ # 特效精灵(爆炸、魔法、粒子)
│ └── ui/ # UI精灵按钮、图标、边框
├── audio/ # 音频资源
│ ├── music/ # 背景音乐
│ ├── sounds/ # 音效
│ └── voice/ # 语音
├── ui/ # UI专用资源
│ ├── auth/ # 认证界面资源
│ │ ├── bg_auth_scene.png # 认证背景
│ │ └── login_frame_smart_transparent.png
│ ├── chinese_theme.tres # 中文主题
│ └── datawhale_logo.png # 项目Logo
│ ├── music/ # 背景音乐BGM
│ ├── sounds/ # 音效SFX
│ └── voice/ # 语音(对话、旁白)
├── fonts/ # 字体文件
│ ├── pixel_fonts/ # 像素风字体
│ └── ui_fonts/ # UI专用字体
├── materials/ # 材质资源
└── shaders/ # 着色器
│ ├── pixel_materials/ # 像素风材质
│ └── shader_materials/ # 着色器材质
├── shaders/ # 着色器文件
│ ├── pixel_shaders/ # 像素风着色器
│ └── effect_shaders/ # 特效着色器
├── ui/ # UI专用资源
│ ├── themes/ # UI主题
│ ├── icons/ # 图标资源
│ └── backgrounds/ # 背景图片
└── icon/ # 应用图标
├── icon.svg # 矢量图标
└── icon.png # 位图图标
```
**美术团队工作流程**:
1. 按分类将资源放入对应目录
2. 遵循命名规范 (snake_case)
3. 确保资源格式符合Godot要求
4. 配置正确的导入设置 (像素艺术使用最近邻过滤)
**像素风游戏资源特点**:
- **像素完美**: 所有精灵使用像素完美设置Filter: Off, Mipmaps: Off
- **统一风格**: 保持一致的像素密度和调色板
- **分辨率标准**: 建议使用16x16、32x32等标准像素尺寸
- **动画帧**: 角色动画使用精灵表Sprite Sheet组织
### 5. ⚙️ 配置层 (Config/)
### 4. ⚙️ 配置层 (Config/)
> **负责团队**: 策划团队
> **职责**: 游戏配置、数据和本地化
> **职责**: 配置文件管理,主要用来配置各类环境
```
Config/
├── game_config.json # 游戏主配置
── zh_CN.json # 中文本地化
── zh_CN.json # 中文本地化
├── environment/ # 环境配置
│ ├── development.json # 开发环境配置
│ ├── testing.json # 测试环境配置
│ └── production.json # 生产环境配置
├── gameplay/ # 游戏玩法配置
│ ├── character_stats.json # 角色属性配置
│ ├── item_database.json # 物品数据库
│ └── level_config.json # 关卡配置
└── localization/ # 本地化配置
├── en_US.json # 英文本地化
├── zh_CN.json # 中文本地化
└── ja_JP.json # 日文本地化
```
**策划团队工作内容**:
- **游戏数值配置**: 角色属性、物品数据、关卡参数
- **本地化文本**: 多语言文本翻译和管理
- **游戏平衡**: 数值平衡和游戏性调整
**配置文件特点**:
- **环境分离**: 开发、测试、生产环境配置分离
- **数据驱动**: 游戏数值通过配置文件控制
- **本地化支持**: 多语言文本管理
- **热更新**: 支持运行时配置更新
**配置文件示例**:
```json
// game_config.json
{
"player": {
"move_speed": 200.0,
"max_health": 100,
"jump_height": 400.0
},
"game": {
"target_fps": 60,
"auto_save_interval": 300
}
}
```
### 6. 🛠️ 工具层 (Utils/)
### 5. 🧪 测试层 (tests/)
> **负责团队**: 开发团队
> **职责**: 通用工具和辅助脚本
```
Utils/
└── StringUtils.gd # 字符串处理工具
```
**特点**:
- 纯函数,无依赖
- 可被任何层调用
- 提供通用功能
### 7. 🧪 测试层 (tests/)
> **负责团队**: 开发团队
> **职责**: 质量保证和自动化测试
> **职责**: 测试文件系统,放置所有对应组件的测试代码,方便快速进行功能性与性能测试
```
tests/
├── api/ # API接口测试
├── auth/ # 认证功能测试
├── unit/ # 单元测试
│ ├── core/ # 核心组件测试
│ ├── characters/ # 角色组件测试
│ └── systems/ # 系统功能测试
├── integration/ # 集成测试
└── performance/ # 性能测试
│ ├── scene_transitions/ # 场景切换测试
│ ├── save_load/ # 存档系统测试
│ └── network/ # 网络功能测试
├── performance/ # 性能测试
│ ├── framerate/ # 帧率测试
│ ├── memory/ # 内存使用测试
│ └── loading_times/ # 加载时间测试
├── api/ # API接口测试
│ ├── auth_tests.py # 认证接口测试
│ └── game_api_tests.py # 游戏API测试
└── ui/ # UI功能测试
├── menu_tests/ # 菜单测试
└── dialog_tests/ # 对话框测试
```
### 8. 🚀 构建层 (tools/)
> **负责团队**: 开发团队/DevOps
> **职责**: 构建脚本和部署工具
**测试类型说明**:
- **单元测试**: 测试单个组件的功能正确性
- **集成测试**: 测试组件间的交互和协作
- **性能测试**: 监控游戏性能指标,确保流畅运行
- **API测试**: 验证网络接口的正确性和稳定性
- **UI测试**: 测试用户界面的交互和响应
```
tools/
├── build_web.bat # Windows Web构建脚本
├── build_web.sh # Linux/macOS Web构建脚本
├── serve_web.bat # Windows 本地服务器
├── serve_web.sh # Linux/macOS 本地服务器
└── README.md # 工具使用说明
```
### 9. 🌐 发布层 (web_assets/)
### 6. 🌐 Web导出层 (web_assets/)
> **负责团队**: 自动生成
> **职责**: Web版本发布资源
> **职责**: Web导出资源专门用于Web平台导出的相关资源和配置文件
```
web_assets/
├── index.html # Web版本入口
├── index.js # Godot Web导出JS
├── index.wasm # WebAssembly文件
├── index.pck # 游戏资源包
└── [其他Web资源] # 图标、配置等
├── html/ # HTML模板文件
├── index.html # Web版本入口页面
│ └── loading.html # 加载页面模板
├── css/ # 样式文件
│ ├── game.css # 游戏样式
│ └── loading.css # 加载样式
├── js/ # JavaScript脚本
│ ├── game_loader.js # 游戏加载器
│ └── utils.js # 工具函数
├── icons/ # Web应用图标
│ ├── favicon.ico # 网站图标
│ └── app_icons/ # PWA应用图标
└── config/ # Web配置文件
├── manifest.json # PWA清单文件
└── service-worker.js # 服务工作者
```
### 10. 📚 文档层 (docs/)
**Web导出特点**:
- **PWA支持**: 支持渐进式Web应用功能
- **响应式设计**: 适配不同屏幕尺寸
- **加载优化**: 优化资源加载和缓存策略
- **跨平台兼容**: 确保在各种浏览器中正常运行
### 7. 📚 文档层 (docs/)
> **负责团队**: 全体团队
> **职责**: 项目文档和开发指南
```
docs/
├── 01-项目入门/ # 新人必读文档
├── 02-开发规范/ # 编码标准文档
├── 03-技术实现/ # 开发指导文档
├── 04-高级开发/ # 进阶技巧文档
├── 05-部署运维/ # 发布部署文档
├── 06-功能模块/ # 功能文档
└── AI_docs/ # 🤖 AI专用文档
├── README.md # AI文档总览
├── architecture_guide.md # 架构执行指南
├── coding_standards.md # 代码风格规范
├── templates/ # 代码模板库
│ ├── components.md # 组件模板集合
│ ├── managers.md # 管理器模板集合
│ └── ui_templates.md # UI模板集合
├── workflows/ # 工作流程指南
│ ├── feature_development.md # 功能开发流程
│ ├── bug_fixing.md # Bug修复流程
│ └── testing_workflow.md # 测试执行流程
└── quick_reference/ # 快速参考手册
├── code_snippets.md # 常用代码片段
├── api_reference.md # API快速参考
└── troubleshooting.md # 故障排除指南
```
**AI_docs特点**:
- **结构化执行**: 每个文档都包含可直接执行的步骤和代码模板
- **标准化规范**: 为AI编程助手提供统一的开发标准和最佳实践
- **模板驱动**: 提供完整的代码模板,确保代码一致性和质量
- **工作流导向**: 包含详细的开发工作流程提升AI协作效率
```
docs/
├── 01-项目入门/ # 新人必读
@@ -245,18 +293,18 @@ docs/
## 🤝 团队协作指南
### 开发团队 🎮
**主要工作区域**: `_Core/`, `scenes/`, `UI/`, `Utils/`, `tests/`
**主要工作区域**: `_Core/`, `scenes/`, `tests/`
**日常工作流程**:
1.`_Core/` 中开发核心系统管理器
2.`scenes/` 中创建游戏场景和组件
3.`UI/` 中实现用户界面
4.`Utils/` 中编写通用工具
5.`tests/` 中编写测试用例
1.`_Core/` 中开发核心系统管理器、基础组件和工具类
2.`scenes/` 中创建游戏场景、角色、UI界面和特效
3.`tests/` 中编写各类测试用例确保代码质量
**协作要点**:
- 遵循架构设计原则,使用事件系统通信
- 遵循架构设计原则,使用事件系统进行模块通信
- 保持代码模块化和可复用性
- 将通用工具类放在 `_Core/utils/` 中统一管理
- 针对像素风游戏特点优化性能
- 及时编写测试和文档
### 美术团队 🎨
@@ -264,28 +312,30 @@ docs/
**日常工作流程**:
1. 按分类整理美术资源到 `assets/` 对应目录
2. 遵循命名规范,使用 snake_case 命名
3. 配置正确的Godot导入设置
4. 与开发团队协作调整UI资源
2. 确保像素艺术风格的一致性和像素完美
3. 配置正确的Godot导入设置关闭过滤、禁用Mipmaps
4. 与开发团队协作调整UI和游戏资源
**协作要点**:
- 严格按照目录结构组织资源
- 确保资源格式和质量符合要求
- 及时与开发团队沟通资源需求
**像素风游戏特殊要求**:
- 严格遵循像素完美原则
- 保持统一的像素密度和调色板
- 使用标准像素尺寸16x16、32x32等
- 精灵动画使用精灵表组织
### 策划团队 📋
**主要工作区域**: `Config/`
**日常工作流程**:
1.`Config/` 中维护游戏配置文件
2. 管理游戏数值和平衡性调整
1.`Config/` 中维护游戏配置和数值平衡
2. 管理多环境配置(开发、测试、生产)
3. 负责本地化文本的翻译和维护
4. 与开发团队协作实现游戏功能
4. 设计游戏玩法和关卡数据
**协作要点**:
- 使用JSON格式编写配置文件
- 保持配置文件的结构清晰
- 保持配置文件的结构清晰和可维护性
- 及时更新本地化文本
- 与开发团队协作实现数据驱动的游戏功能
---
@@ -320,15 +370,24 @@ docs/
### 资源管理规范
- 所有资源必须放在 `assets/` 对应分类目录下
- 使用描述性的文件名,避免使用数字编号
- 像素艺术资源必须关闭过滤 (Filter: Off)
- **像素艺术资源必须关闭过滤** (Filter: Off, Mipmaps: Off)
- 保持统一的像素密度和调色板
- 使用标准像素尺寸16x16、32x32、64x64等
- 及时清理不使用的资源文件
### 代码组织规范
- 脚本文件与场景文件放在同一目录
- 使用事件系统实现模块间通信
- 使用事件系统实现模块间通信,避免直接引用
- 保持单一职责原则,避免过度耦合
- 针对像素风游戏优化性能(避免浮点数位置、使用整数坐标)
- 及时编写注释和文档
### 像素风游戏特殊规范
- **像素完美**: 确保所有精灵在整数坐标上渲染
- **统一风格**: 保持一致的像素密度和艺术风格
- **性能优化**: 使用对象池管理频繁创建销毁的对象
- **分辨率适配**: 使用像素完美的缩放方式适配不同分辨率
---
**记住:良好的项目结构是团队协作成功的基础!**

View File

@@ -0,0 +1,59 @@
# 网格瓦片系统
## 概述
网格瓦片系统提供32x32像素的标准化网格管理用于规范地图元素的位置和大小。
## 核心组件
### GridSystem (核心系统)
- **位置**: `_Core/systems/GridSystem.gd`
- **功能**: 提供网格坐标转换、位置计算等基础功能
- **类型**: 静态工具类
### GrassTile (瓦片组件)
- **脚本**: `scenes/prefabs/GrassTile.gd`
- **场景**: `scenes/prefabs/grass_tile_prefab.tscn`
- **功能**: 可视化的草地瓦片自动对齐32x32网格
## 使用方法
### 在编辑器中使用
1. 拖拽 `scenes/prefabs/grass_tile_prefab.tscn` 到场景中
2. 在Inspector中设置Texture和Grid Position
3. 瓦片会自动对齐到网格
### 通过代码使用
```gdscript
# 预加载场景
const GrassTileScene = preload("res://scenes/prefabs/grass_tile_prefab.tscn")
# 创建瓦片
var grass = GrassTileScene.instantiate()
add_child(grass)
grass.set_grid_position(Vector2i(0, 0))
```
## 网格规范
### 基础规格
- **网格大小**: 32x32像素
- **坐标系**: 左上角为原点(0,0)
- **对齐方式**: 瓦片中心对齐到网格中心
### 纹理要求
- 尺寸必须是32的倍数
- 推荐格式: PNG
- 推荐尺寸: 32x32, 64x64, 96x96
## API参考
### GridSystem 方法
- `world_to_grid(world_pos: Vector2) -> Vector2i`
- `grid_to_world_center(grid_pos: Vector2i) -> Vector2`
- `snap_to_grid(position: Vector2) -> Vector2`
### GrassTile 属性和方法
- `grid_position: Vector2i` - 网格坐标
- `set_grid_position(pos: Vector2i)` - 设置网格位置
- `snap_to_grid()` - 对齐到网格

0
docs/AI_docs/.gitkeep Normal file
View File

View File

@@ -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的输出面板和调试器它们通常会提供详细的错误信息和解决建议。

View File

@@ -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. 添加必要的错误处理和日志记录

View File

@@ -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. **测试覆盖**: 测试是否充分?
### 持续改进
- 收集用户反馈
- 监控性能指标
- 定期重构优化
- 更新文档和示例
---
**🎯 记住**: 这个流程确保了功能开发的质量和一致性。严格遵循每个阶段的要求,将大大提高开发效率和代码质量。

View File

@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="whaleTown"
run/main_scene="res://Scenes/Maps/main_scene.tscn"
run/main_scene="res://scenes/MainScene.tscn"
config/features=PackedStringArray("4.5", "Forward Plus")
config/icon="res://icon.svg"

View File

@@ -1 +0,0 @@
# 保持目录结构 - 角色预制体目录

View File

@@ -1 +0,0 @@
# 保持目录结构 - 特效预制体目录

View File

@@ -1 +0,0 @@
# 保持目录结构 - 物品预制体目录

View File

@@ -1 +0,0 @@
# 保持目录结构 - UI预制体目录

View File

@@ -1,5 +1,16 @@
extends Control
# ============================================================================
# MainScene.gd - 主场景控制器
# ============================================================================
# 这是游戏的主入口场景,负责管理所有图像显示和界面切换
# 功能包括:
# - 登录/注册界面管理
# - 主游戏界面显示
# - 用户状态管理
# - 游戏功能模块入口
# ============================================================================
# 场景节点引用
@onready var auth_scene: Control = $AuthScene
@onready var main_game_ui: Control = $MainGameUI
@@ -113,4 +124,4 @@ func _input(event):
get_tree().quit()
GameState.MAIN_GAME:
# 在游戏中按ESC可能显示菜单或返回登录
show_auth_scene()
show_auth_scene()

1
scenes/MainScene.gd.uid Normal file
View File

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

View File

@@ -1,28 +1,14 @@
[gd_scene load_steps=4 format=3 uid="uid://4ptgx76y83mx"]
[gd_scene load_steps=3 format=3 uid="uid://cjabtnqbdd2ey"]
[ext_resource type="Texture2D" uid="uid://bx17oy8lvaca4" path="res://assets/ui/auth/bg_auth_scene.png" id="1_background"]
[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://UI/Windows/LoginWindow.tscn" id="2_main"]
[ext_resource type="Script" uid="uid://blp30m0tuach8" path="res://Scenes/Maps/MainScene.gd" id="3_script"]
[ext_resource type="Script" path="res://scenes/MainScene.gd" id="1_script"]
[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://scenes/ui/AuthScene.tscn" id="2_main"]
[node name="Main" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("3_script")
[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
script = ExtResource("1_script")
[node name="AuthScene" parent="." instance=ExtResource("2_main")]
layout_mode = 1
@@ -33,24 +19,12 @@ layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="UIOverlay" type="ColorRect" parent="MainGameUI"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.3)
[node name="TopBar" type="Panel" parent="MainGameUI"]
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 60.0
grow_horizontal = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MainGameUI/TopBar"]
layout_mode = 1
@@ -61,26 +35,16 @@ offset_left = 20.0
offset_top = 10.0
offset_right = -20.0
offset_bottom = -10.0
grow_horizontal = 2
grow_vertical = 2
[node name="WelcomeLabel" type="Label" parent="MainGameUI/TopBar/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "🐋 欢迎来到鲸鱼镇!"
vertical_alignment = 1
[node name="UserLabel" type="Label" parent="MainGameUI/TopBar/HBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "当前用户: [用户名]"
horizontal_alignment = 2
size_flags_horizontal = 3
text = "当前用户: "
vertical_alignment = 1
[node name="LogoutButton" type="Button" parent="MainGameUI/TopBar/HBoxContainer"]
layout_mode = 2
text = "退出"
text = "出"
[node name="MainContent" type="Control" parent="MainGameUI"]
layout_mode = 1
@@ -88,80 +52,29 @@ anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 60.0
grow_horizontal = 2
grow_vertical = 2
[node name="CenterContainer" type="CenterContainer" parent="MainGameUI/MainContent"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="MainGameUI/MainContent/CenterContainer"]
custom_minimum_size = Vector2(600, 400)
layout_mode = 2
[node name="GameTitle" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_font_sizes/font_size = 24
text = "🏘️ 鲸鱼镇主界面"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
layout_mode = 2
[node name="GameMenuGrid" type="GridContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
columns = 2
[node name="ExploreButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
custom_minimum_size = Vector2(280, 80)
layout_mode = 2
text = "🗺️ 探索小镇"
[node name="InventoryButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
custom_minimum_size = Vector2(280, 80)
layout_mode = 2
text = "🎒 背包物品"
[node name="ShopButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
custom_minimum_size = Vector2(280, 80)
layout_mode = 2
text = "🏪 商店购物"
[node name="FriendsButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
custom_minimum_size = Vector2(280, 80)
layout_mode = 2
text = "👥 好友列表"
[node name="HSeparator2" type="HSeparator" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
layout_mode = 2
[node name="StatusPanel" type="Panel" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 120)
layout_mode = 2
custom_minimum_size = Vector2(400, 150)
[node name="StatusContainer" type="VBoxContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel"]
[node name="StatusContainer" type="MarginContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 10.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = -10.0
grow_horizontal = 2
grow_vertical = 2
[node name="StatusTitle" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "📊 玩家状态"
horizontal_alignment = 1
offset_bottom = -20.0
[node name="StatusGrid" type="GridContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer"]
layout_mode = 2
@@ -169,20 +82,40 @@ columns = 2
[node name="LevelLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "等级: 1"
[node name="CoinsLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "金币: 100"
[node name="ExpLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "经验: 0/100"
[node name="EnergyLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "体力: 100/100"
[node name="GameMenuGrid" type="GridContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"]
layout_mode = 2
columns = 2
[node name="ExploreButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
layout_mode = 2
custom_minimum_size = Vector2(150, 50)
text = "🗺️ 探索小镇"
[node name="InventoryButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
layout_mode = 2
custom_minimum_size = Vector2(150, 50)
text = "🎒 背包"
[node name="ShopButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
layout_mode = 2
custom_minimum_size = Vector2(150, 50)
text = "🏪 商店"
[node name="FriendsButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"]
layout_mode = 2
custom_minimum_size = Vector2(150, 50)
text = "👥 好友"

2
scenes/Maps/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# 地图场景目录
# 存放游戏关卡、世界地图等地图场景

View File

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

16
scenes/Maps/square.tscn Normal file
View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=4 format=3 uid="uid://5cc0c6cpnhe8"]
[ext_resource type="Texture2D" uid="uid://7o0xyqmqbvov" path="res://assets/sprites/environment/square.png" id="1_a2ug0"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_1t0sv"]
texture = ExtResource("1_a2ug0")
15:1/size_in_atlas = Vector2i(2, 2)
15:1/0 = 0
[sub_resource type="TileSet" id="TileSet_g3awv"]
sources/0 = SubResource("TileSetAtlasSource_1t0sv")
[node name="square" type="Node2D"]
[node name="TileMapLayer" type="TileMapLayer" parent="."]
tile_set = SubResource("TileSet_g3awv")

View File

@@ -0,0 +1,2 @@
# 人物场景目录
# 存放角色、NPC、敌人等人物相关场景

2
scenes/effects/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# 特效场景目录
# 存放粒子效果、动画等特效场景

2
scenes/prefabs/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# 预制体组件目录
# 存放可复用的预制体组件

122
scenes/prefabs/GrassTile.gd Normal file
View File

@@ -0,0 +1,122 @@
# ============================================================================
# 简单草地瓦片 - GrassTile.gd
#
# 一个简单的32x32草地瓦片自动对齐网格
# 使用方法:
# 1. 在场景中实例化 grass_tile_prefab.tscn
# 2. 设置 texture 属性
# 3. 调用 set_grid_position() 设置网格位置
# ============================================================================
class_name GrassTile
extends Sprite2D
# ============================================================================
# 导出属性
# ============================================================================
@export var grid_position: Vector2i = Vector2i.ZERO : set = set_grid_position
@export var auto_snap: bool = true # 是否自动对齐网格
# ============================================================================
# 信号
# ============================================================================
signal position_changed(new_grid_pos: Vector2i)
func _ready():
# 如果没有纹理,创建一个默认的占位符
if not texture:
_create_placeholder_texture()
# 验证纹理尺寸
_validate_texture()
# 自动对齐到网格
if auto_snap:
snap_to_grid()
# ============================================================================
# 公共方法
# ============================================================================
# 设置网格位置并自动对齐
func set_grid_position(new_pos: Vector2i):
if grid_position != new_pos:
grid_position = new_pos
if auto_snap:
snap_to_grid()
position_changed.emit(grid_position)
# 对齐到网格中心
func snap_to_grid():
position = Vector2(
grid_position.x * 32.0 + 16.0,
grid_position.y * 32.0 + 16.0
)
# 从世界坐标设置位置(会自动转换为网格坐标)
func set_world_position(world_pos: Vector2):
var new_grid_pos = Vector2i(
int(world_pos.x / 32.0),
int(world_pos.y / 32.0)
)
set_grid_position(new_grid_pos)
# 获取世界坐标
func get_world_position() -> Vector2:
return position
# ============================================================================
# 私有方法
# ============================================================================
# 创建占位符纹理
func _create_placeholder_texture():
var image = Image.create(32, 32, false, Image.FORMAT_RGBA8)
# 创建简单的草地图案
var grass_color = Color(0.3, 0.8, 0.3, 1.0) # 亮绿色
var dark_color = Color(0.2, 0.6, 0.2, 1.0) # 深绿色
# 填充基础颜色
image.fill(grass_color)
# 添加简单的格子图案
for x in range(32):
for y in range(32):
if (x + y) % 8 < 4:
image.set_pixel(x, y, dark_color)
# 创建纹理
var placeholder_texture = ImageTexture.new()
placeholder_texture.set_image(image)
texture = placeholder_texture
# 验证纹理尺寸
func _validate_texture():
if texture:
var size = Vector2i(texture.get_width(), texture.get_height())
if size.x % 32 != 0 or size.y % 32 != 0:
push_warning("GrassTile: 纹理尺寸不是32的倍数: " + str(size))
return false
return true
return false
# ============================================================================
# 调试方法
# ============================================================================
# 获取瓦片信息
func get_tile_info() -> Dictionary:
return {
"grid_position": grid_position,
"world_position": position,
"texture_size": texture.get_size() if texture else Vector2.ZERO,
"auto_snap": auto_snap
}
# 打印瓦片信息
func print_info():
var info = get_tile_info()
print("=== 草地瓦片信息 ===")
for key in info:
print(key, ": ", info[key])

View File

@@ -0,0 +1,2 @@
# 角色预制体目录
# 存放可复用的角色组件预制体

View File

@@ -0,0 +1,2 @@
# 特效预制体目录
# 存放可复用的特效组件预制体

View File

@@ -0,0 +1,10 @@
[gd_scene load_steps=3 format=3 uid="uid://bvxqm8n7qwqxe"]
[ext_resource type="Script" path="res://scenes/prefabs/GrassTile.gd" id="1_0x8qm"]
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_1"]
size = Vector2(32, 32)
[node name="GrassTile" type="Sprite2D"]
texture = SubResource("PlaceholderTexture2D_1")
script = ExtResource("1_0x8qm")

View File

@@ -0,0 +1,2 @@
# 物品预制体目录
# 存放可复用的物品组件预制体

View File

@@ -0,0 +1,2 @@
# UI预制体目录
# 存放可复用的UI组件预制体

2
scenes/ui/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# UI界面场景目录
# 存放菜单、HUD、对话框等UI界面场景

609
scenes/ui/AuthScene.gd Normal file
View File

@@ -0,0 +1,609 @@
extends Control
# ============================================================================
# AuthScene.gd - 认证场景视图控制器
# ============================================================================
# 认证系统的视图层控制器只负责UI相关的操作
#
# 核心职责:
# - UI控件的显示和隐藏
# - 用户输入的收集和传递
# - 错误提示的显示
# - 按钮状态的管理
# - Toast消息的显示
#
# 业务逻辑:
# - 所有业务逻辑都委托给AuthManager处理
# - 通过信号与AuthManager通信
# - 不包含任何验证逻辑或网络请求
#
# 注意事项:
# - 这是纯视图层,不处理业务逻辑
# - 通过事件系统与业务层解耦
# - 专注于用户体验和界面交互
# ============================================================================
# ============ 信号定义 ============
# 登录成功信号 - 传递给上层场景
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 auth_manager: AuthManager
# Toast消息管理器
var toast_manager: ToastManager
# 验证码冷却计时器
var cooldown_timer: Timer = null
# 当前登录模式(从管理器同步)
var current_login_mode: AuthManager.LoginMode = AuthManager.LoginMode.PASSWORD
# ============ 生命周期方法 ============
# 初始化认证场景
func _ready():
_setup_controllers() # 初始化控制器
_connect_signals() # 连接信号
_setup_ui() # 设置UI初始状态
print("认证场景视图已加载")
# 测试网络连接
auth_manager.test_network_connection()
# 设置控制器
func _setup_controllers():
# 创建业务逻辑管理器
auth_manager = AuthManager.new()
# 创建Toast管理器
toast_manager = ToastManager.new()
toast_manager.setup(toast_container)
# 连接管理器信号
_connect_controller_signals()
# 连接管理器信号
func _connect_controller_signals():
# 登录相关信号
auth_manager.login_success.connect(_on_controller_login_success)
auth_manager.login_failed.connect(_on_controller_login_failed)
# 注册相关信号
auth_manager.register_success.connect(_on_controller_register_success)
auth_manager.register_failed.connect(_on_controller_register_failed)
# 验证码相关信号
auth_manager.verification_code_sent.connect(_on_controller_verification_code_sent)
auth_manager.verification_code_failed.connect(_on_controller_verification_code_failed)
# 表单验证信号
auth_manager.form_validation_failed.connect(_on_controller_form_validation_failed)
# 网络状态信号
auth_manager.network_status_changed.connect(_on_controller_network_status_changed)
# 按钮状态信号
auth_manager.button_state_changed.connect(_on_controller_button_state_changed)
# Toast消息信号
auth_manager.show_toast_message.connect(_on_controller_show_toast_message)
# 设置UI初始状态
func _setup_ui():
show_login_panel()
_update_login_mode_ui()
# 连接UI信号
func _connect_signals():
# 主要按钮信号
main_btn.pressed.connect(_on_main_button_pressed)
# 登录界面按钮信号
login_btn.pressed.connect(_on_login_mode_toggle_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)
# 表单失焦验证事件
_connect_validation_signals()
# 实时输入验证事件
_connect_input_change_signals()
# 连接表单验证信号
func _connect_validation_signals():
# 登录表单
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)
# 连接输入变化信号
func _connect_input_change_signals():
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)
# ============ UI面板管理 ============
# 显示登录面板
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():
current_login_mode = auth_manager.get_current_login_mode()
if current_login_mode == AuthManager.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)
# ============ 按钮事件处理 ============
# 主按钮点击事件
func _on_main_button_pressed():
if current_login_mode == AuthManager.LoginMode.PASSWORD:
_execute_password_login()
else:
_execute_verification_login()
# 执行密码登录
func _execute_password_login():
var username = login_username.text.strip_edges()
var password = login_password.text
auth_manager.execute_password_login(username, password)
# 执行验证码登录
func _execute_verification_login():
var identifier = login_username.text.strip_edges()
var verification_code = login_verification.text.strip_edges()
auth_manager.execute_verification_login(identifier, verification_code)
# 登录模式切换按钮
func _on_login_mode_toggle_pressed():
auth_manager.toggle_login_mode()
_update_login_mode_ui()
# 清空输入框和错误提示
login_username.text = ""
login_password.text = ""
login_verification.text = ""
_hide_field_error(login_username_error)
_hide_field_error(login_password_error)
_hide_field_error(login_verification_error)
# 注册按钮点击事件
func _on_register_pressed():
var username = register_username.text.strip_edges()
var email = register_email.text.strip_edges()
var password = register_password.text
var confirm_password = register_confirm.text
var verification_code = verification_input.text.strip_edges()
auth_manager.execute_register(username, email, password, confirm_password, verification_code)
# 发送邮箱验证码按钮
func _on_send_code_pressed():
var email = register_email.text.strip_edges()
auth_manager.send_email_verification_code(email)
# 开始冷却计时器
_start_cooldown_timer(email)
# 获取登录验证码按钮
func _on_get_login_code_pressed():
var identifier = login_username.text.strip_edges()
auth_manager.send_login_verification_code(identifier)
# 忘记密码按钮
func _on_forgot_password_pressed():
var identifier = login_username.text.strip_edges()
if current_login_mode == AuthManager.LoginMode.PASSWORD:
# 密码登录模式:发送密码重置验证码
auth_manager.send_password_reset_code(identifier)
else:
# 验证码登录模式:发送登录验证码
auth_manager.send_login_verification_code(identifier)
# 跳转到注册面板
func _on_register_link_pressed():
show_register_panel()
# 返回登录面板
func _on_to_login_pressed():
show_login_panel()
# 回车键登录
func _on_login_enter(_text: String):
_on_main_button_pressed()
# ============ 控制器信号处理 ============
# 登录成功处理
func _on_controller_login_success(username: String):
# 清空表单
login_username.text = ""
login_password.text = ""
login_verification.text = ""
_hide_field_error(login_username_error)
_hide_field_error(login_password_error)
_hide_field_error(login_verification_error)
# 发送登录成功信号给上层
login_success.emit(username)
# 登录失败处理
func _on_controller_login_failed(_message: String):
# 登录失败时不需要额外处理Toast已经显示了错误信息
pass
# 注册成功处理
func _on_controller_register_success(_message: String):
# 清空注册表单
_clear_register_form()
# 切换到登录面板
show_login_panel()
# 将注册时的用户名填入登录框
login_username.text = register_username.text.strip_edges()
# 注册失败处理
func _on_controller_register_failed(_message: String):
# 注册失败时不需要额外处理Toast已经显示了错误信息
pass
# 验证码发送成功处理
func _on_controller_verification_code_sent(_message: String):
# 验证码发送成功,冷却计时器已经在按钮点击时启动
pass
# 验证码发送失败处理
func _on_controller_verification_code_failed(_message: String):
# 重置验证码按钮状态
_reset_verification_button()
# 表单验证失败处理
func _on_controller_form_validation_failed(field: String, message: String):
match field:
"username":
_show_field_error(login_username_error, message)
login_username.grab_focus()
"password":
_show_field_error(login_password_error, message)
login_password.grab_focus()
"verification":
if current_login_mode == AuthManager.LoginMode.VERIFICATION:
_show_field_error(login_verification_error, message)
login_verification.grab_focus()
else:
_show_field_error(verification_error, message)
verification_input.grab_focus()
"email":
_show_field_error(register_email_error, message)
register_email.grab_focus()
"confirm":
_show_field_error(register_confirm_error, message)
register_confirm.grab_focus()
# 网络状态变化处理
func _on_controller_network_status_changed(network_connected: bool, message: String):
# 可以在这里添加网络状态指示器
print("网络状态: ", "连接" if network_connected else "断开", " - ", message)
# 按钮状态变化处理
func _on_controller_button_state_changed(button_name: String, is_loading: bool, text: String):
match button_name:
"main_btn":
_set_button_state(main_btn, is_loading, text)
"register_btn":
_set_button_state(register_btn, is_loading, text)
"get_code_btn":
_set_button_state(get_code_btn, is_loading, text)
"forgot_password_btn":
_set_button_state(forgot_password_btn, is_loading, text)
# Toast消息处理
func _on_controller_show_toast_message(message: String, is_success: bool):
toast_manager.show_toast(message, is_success)
# ============ 验证码冷却管理 ============
# 开始冷却计时器
func _start_cooldown_timer(_email: String):
if cooldown_timer != null:
cooldown_timer.queue_free()
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 remaining_time = auth_manager.get_remaining_cooldown_time(register_email.text.strip_edges())
if remaining_time > 0:
send_code_btn.text = "重新发送(%d)" % remaining_time
else:
send_code_btn.text = "重新发送"
send_code_btn.disabled = false
if cooldown_timer != null:
cooldown_timer.queue_free()
cooldown_timer = null
# 重置验证码按钮状态
func _reset_verification_button():
if cooldown_timer != null:
cooldown_timer.queue_free()
cooldown_timer = null
send_code_btn.disabled = false
send_code_btn.text = "发送验证码"
# ============ UI工具方法 ============
# 设置按钮状态
func _set_button_state(button: Button, is_loading: bool, text: String):
button.disabled = is_loading
button.text = 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 _clear_register_form():
register_username.text = ""
register_email.text = ""
register_password.text = ""
register_confirm.text = ""
verification_input.text = ""
_reset_verification_button()
_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)
# ============ 表单验证事件处理 ============
# 登录用户名失焦验证
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()
var validation = auth_manager.validate_verification_code(verification_code)
if not validation.valid:
_show_field_error(login_verification_error, validation.message)
else:
_hide_field_error(login_verification_error)
# 注册用户名失焦验证
func _on_register_username_focus_exited():
var username = register_username.text.strip_edges()
var validation = auth_manager.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 = auth_manager.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 = auth_manager.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 = auth_manager.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 = auth_manager.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 _exit_tree():
# 清理控制器
if auth_manager:
auth_manager.cleanup()
# 清理Toast管理器
if toast_manager:
toast_manager.clear_all_toasts()
# 清理计时器
if cooldown_timer != null:
cooldown_timer.queue_free()
cooldown_timer = null
# 处理ESC键退出
func _input(event):
if event.is_action_pressed("ui_cancel"):
get_tree().quit()

View File

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

View File

@@ -2,7 +2,7 @@
[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" uid="uid://bs1vy3ierj66t" path="res://UI/Windows/AuthScene.gd" id="3_script"]
[ext_resource type="Script" path="res://scenes/ui/AuthScene.gd" id="3_script"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1"]

Some files were not shown because too many files have changed in this diff Show More