30 Commits

Author SHA1 Message Date
王浩
326ab7ce5c 合并主场景和个人小屋 2026-02-07 14:11:00 +08:00
WhaleTown Developer
603e7d9fc6 修bug 2026-01-20 20:11:07 +08:00
王浩
e989b4adf1 docs: 删除 chat_system.md 计划文档 2026-01-14 17:11:48 +08:00
王浩
6e70aac0b9 fix: 修复聊天消息显示问题
- AuthScene: 修复节点路径错误 (WhaleFrame, UsernameInput)
- ChatManager: 修复 timestamp 类型转换 (String -> float)
- ChatMessage: 修复节点引用获取方式和 UI 显示
- ChatUI: 优化消息列表布局对齐
2026-01-14 17:10:48 +08:00
625fe0ff6c merge upstream 2026-01-14 15:19:15 +08:00
WhaleTown Developer
0e5b9f947b refactor: 移除SocketIOClient并更新认证场景
- 移除SocketIOClient.gd及.uid文件
- 更新ChatManager以适配新的架构
- 添加认证UI图片资源和背景音乐
- 优化AuthScene布局和配置
- 更新MainScene和项目配置
2026-01-11 23:12:35 +08:00
7cca58cb07 Merge pull request 'feature/whaletown-developer-skill' (#12) from feature/whaletown-developer-skill into main
Reviewed-on: #12
2026-01-10 19:31:05 +08:00
王浩
749e2c257b assets: 添加认证UI图片资源 2026-01-09 23:24:32 +08:00
王浩
e335a35f6c feat(chat-ui): 更新聊天UI和场景配置
- 优化聊天消息显示
- 调整UI布局
2026-01-09 23:23:41 +08:00
王浩
136e1344a0 fix(chat): 修复WebSocket连接和消息格式
- 修复连接状态检测时机问题
- 修复聊天消息格式为 {t: chat, content, scope}
- 添加 _send_login_message 函数
- 移除空消息心跳避免服务器错误
2026-01-09 23:21:12 +08:00
WhaleTown Developer
25a21f92be feat(chat): 优化聊天UI布局和WebSocket连接
- 更新 WebSocket URL 以支持 Socket.IO 握手参数 (EIO=4)
- 重构聊天面板布局,使用绝对定位和百分比锚点
- 优化输入框样式,添加装饰元素
- 修复输入框焦点释放的事件冲突问题
- 将 ChatUI 集成到主场景中
- 改进主场景容器布局设置
2026-01-08 23:59:21 +08:00
王浩
9c2e3bf15a refactor(chat-ui): 移除HeaderContainer和SendButton,添加装饰图片,修复Enter键监听
- 删除 HeaderContainer 和 StatusLabel(状态显示)
- 删除 SendButton(发送按钮)
- 添加聊天框背景图和装饰图片
- 设置 TextureRect modulate 为白色,修复黑框问题
- 移除 ChatPanel 背景色,使背景透明
- 修复 is_key_pressed 错误,改用 InputEventKey 类型检查
- 移除 _update_connection_status 函数及相关调用
2026-01-08 18:14:48 +08:00
WhaleTown Developer
9e288dbb62 docs: 更新聊天系统实施进度
- 简化文档,移除详细修复记录
- 更新实施状态:所有编译错误已修复
- 记录待后端解决的 Zulip 集成问题
2026-01-08 00:31:07 +08:00
WhaleTown Developer
c8e73bec59 fix: 修复聊天系统编译错误
- 修复 WebSocketManager/SocketIOClient 函数缩进错误
- 重命名 is_connected() 避免与 Object 基类冲突
- 修复 tscn 文件多余前导空格
- 修复测试文件 GUT 断言函数调用
- 添加 GUT 测试框架
2026-01-08 00:11:12 +08:00
王浩
16f24ab26f feat:添加聊天UI资源文件
- 添加聊天界面UI资源(缩略框背景、输入框背景、装饰图片)
- 修复 GrassTile.gd.uid 文件缺失

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 17:46:26 +08:00
王浩
414225e8c1 docs:更新项目规范文档
- 在 claude.md 添加 Plan Mode Protocol 章节
- 明确规划阶段的输出要求和执行报告流程
- 强制要求在每个 TODO 完成后更新文档并等待用户确认

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 17:42:48 +08:00
王浩
fb7cba4088 feat:实现聊天系统核心功能
- 添加 SocketIOClient.gd 实现 Socket.IO 协议封装
- 添加 WebSocketManager.gd 管理连接生命周期和自动重连
- 添加 ChatManager.gd 实现聊天业务逻辑与会话管理
  - 支持当前会话缓存(最多 100 条消息)
  - 支持历史消息按需加载(每次 100 条)
  - 每次登录/重连自动重置会话缓存
  - 客户端频率限制(10 条/分钟)
  - Token 管理与认证
- 添加 ChatMessage.gd/tscn 消息气泡 UI 组件
- 添加 ChatUI.gd/tscn 聊天界面
- 在 EventNames.gd 添加 7 个聊天事件常量
- 在 AuthManager.gd 添加 game_token 管理方法
- 添加完整的单元测试(128 个测试用例)
  - test_socketio_client.gd (42 个测试)
  - test_websocket_manager.gd (38 个测试)
  - test_chat_manager.gd (48 个测试)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 17:42:31 +08:00
7b85147994 chore: 合并 main 分支,统一 CLAUDE.md 格式规范
合并 main 分支对 CLAUDE.md 的格式改进,同时保留 feature 分支新增的标准开发工作流(第 8 节)。

主要改动:
- 更新 Godot 版本要求从 4.2+ 到 4.5+
- 规范化 Markdown 格式(代码块、粗体、列表)
- 保留新增的「Standard Development Workflow」章节
- 调整章节编号(第 9、10 节)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 19:41:29 +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
43e0c2b928 feat:添加whaletown-developer标准开发工作流技能
- 创建whaletown-developer skill自动化7步开发流程
- 添加完整的使用说明文档和质量检查清单
- 更新CLAUDE.md集成标准开发工作流说明
- 新增标准开发工作流详细文档

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 00:46:48 +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
185 changed files with 10910 additions and 436 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Skill(whaletown-developer)"
]
}
}

View File

@@ -0,0 +1,335 @@
---
name: whaletown-developer
description: Automate WhaleTown project's standard development workflow. Use this skill when implementing features, fixing bugs, creating scenes, or any code development tasks. Guides through 7-step process - architecture analysis, implementation, comment/naming validation, testing, and Git commit generation following project conventions.
---
# WhaleTown Standard Development Workflow Skill
This skill automates the standard development workflow for the WhaleTown project, ensuring all developers follow unified specifications and quality standards.
## When to Use This Skill
Activate this skill when:
- Implementing new features ("实现XX功能", "添加XX系统")
- Fixing bugs ("修复XX Bug", "解决XX问题")
- Creating scenes ("创建XX场景", "设计XX界面")
- Developing modules ("开发XX模块", "构建XX组件")
- Any code development task requiring adherence to project standards
## Development Workflow Overview
```
Step 1: Architecture Analysis (读取架构规范)
Step 2: Implementation (按规范编码)
Step 3: Comment Validation (注释规范检查)
Step 4: Naming Validation (命名规范检查)
Step 5: Test Writing (编写测试代码)
Step 6: Test Execution (运行测试验证)
Step 7: Git Commit (生成规范提交信息)
```
## Step-by-Step Workflow
### Step 1: Architecture Analysis
Read and apply the architecture specifications before implementation.
**Actions:**
1. Read `docs/02-开发规范/架构与通信规范.md`
2. Determine file location based on feature type:
- Core systems → `_Core/managers/` or `_Core/systems/`
- Game scenes → `scenes/Maps/`, `scenes/Entities/`, `scenes/Components/`
- UI components → `scenes/ui/`
3. Identify communication method (MUST use EventSystem for cross-module communication)
4. List dependencies (required managers and systems)
5. Design event definitions (add to `_Core/EventNames.gd`)
**Layered Architecture:**
```
UI Layer (界面层) → scenes/ui/
Scenes Layer (游戏层) → scenes/Maps/, scenes/Entities/
_Core Layer (框架层) → _Core/managers/, _Core/systems/
```
**Communication Principle:** "Signal Up, Call Down"
- Parents call child methods (downward calls)
- Children emit signals to notify parents (upward signals)
- Cross-module communication MUST use EventSystem
### Step 2: Implementation
Implement the feature following strict project conventions.
**Requirements:**
- **Type Safety**: All variables and functions MUST have type annotations
```gdscript
var speed: float = 200.0
func move(delta: float) -> void:
```
- **Godot 4.2+ Syntax**: NO `yield()`, use `await`
- **Node Caching**: Use `@onready` to cache node references, avoid `get_node()` in `_process()`
- **EventSystem Communication**: Use EventSystem for cross-module messaging
```gdscript
EventSystem.emit_event(EventNames.PLAYER_MOVED, {"position": global_position})
EventSystem.connect_event(EventNames.INTERACT_PRESSED, _on_interact_pressed)
```
- **Nearest Filter**: All Sprite2D/TileMap resources MUST use Nearest filter (no Linear filter)
- **AutoLoad Restrictions**: Only GameManager, SceneManager, EventSystem, NetworkManager, ResponseHandler allowed as autoloads
- **Low-level Entities**: Do NOT directly reference GameManager in Player/NPC entities, use events instead
### Step 3: Comment Validation
Ensure code comments meet project standards.
**Actions:**
1. Read `docs/02-开发规范/代码注释规范.md`
2. Verify file header comment is complete:
```gdscript
# ============================================================================
# 文件名: FeatureName.gd
# 作用: 简短描述功能
#
# 主要功能:
# - 功能点1
# - 功能点2
#
# 依赖: 列出依赖的管理器/系统
# 作者: [开发者名称]
# 创建时间: YYYY-MM-DD
# ============================================================================
```
3. Verify all public functions have complete documentation:
```gdscript
# 函数功能描述
#
# 参数:
# param_name: Type - 参数说明
#
# 返回值:
# Type - 返回值说明
#
# 使用示例:
# var result = function_name(param)
func function_name(param: Type) -> ReturnType:
```
4. Ensure complex logic has inline comments explaining WHY, not WHAT
### Step 4: Naming Validation
Verify all naming follows project conventions.
**Actions:**
1. Read `docs/02-开发规范/命名规范.md`
2. Validate naming conventions:
- **Class names**: PascalCase (`class_name PlayerController`)
- **Variables/Functions**: camelCase (`var moveSpeed: float`, `func updateMovement()`)
- **Constants**: UPPER_CASE (`const MAX_HEALTH: int = 100`)
- **Private members**: Underscore prefix (`var _velocity: Vector2`)
- **Scene files**: snake_case with suffix (`player_scene.tscn`, `enemy_prefab.tscn`)
- **Script files**: PascalCase.gd (`PlayerController.gd`, `GameManager.gd`)
**Common Patterns:**
```gdscript
# ✅ Correct
const MAX_SPEED: float = 300.0
var currentHealth: int
var _isInitialized: bool = false
func getPlayerPosition() -> Vector2:
func _calculateDamage(baseDamage: int) -> int:
# ❌ Incorrect
const maxSpeed: float = 300.0 # Constants must be UPPER_CASE
var CurrentHealth: int # Variables must be camelCase
var is_initialized: bool = false # No snake_case for variables
func GetPlayerPosition() -> Vector2: # Functions must be camelCase
```
### Step 5: Test Writing
Create unit tests for the implemented functionality.
**Actions:**
1. Read `docs/03-技术实现/测试指南.md`
2. For _Core/ managers/systems, MUST create corresponding test file in `tests/unit/`
3. Test file naming: `test_[name].gd`
4. Test file structure:
```gdscript
extends GutTest
## [FeatureName] 单元测试
var feature: FeatureName
func before_each():
feature = preload("res://_Core/managers/FeatureName.gd").new()
add_child(feature)
func after_each():
feature.queue_free()
func test_initialization():
var result = feature.initialize()
assert_true(result, "Feature should initialize successfully")
func test_core_functionality():
# Test core functionality
pass
```
### Step 6: Test Execution
Run tests to ensure code quality.
**Actions:**
1. Run GUT tests using Bash tool:
```bash
godot --headless -s addons/gut/gut_cmdline.gd -gdir=res://tests/ -ginclude_subdirs
```
2. Verify all tests pass
3. If tests fail:
- Identify the root cause
- Fix the implementation or test
- Re-run tests until all pass
### Step 7: Git Commit
Generate standardized Git commit message.
**Actions:**
1. Read `docs/02-开发规范/Git提交规范.md`
2. Determine commit type based on changes:
- `feat` - New features
- `fix` - Bug fixes
- `docs` - Documentation updates
- `refactor` - Code refactoring
- `perf` - Performance optimization
- `test` - Test additions/modifications
- `scene` - Scene file changes
- `ui` - UI related changes
3. Generate commit message using Chinese colon ():
```
<类型><简短描述>
[可选的详细描述]
```
4. Follow principles:
- **One commit, one change** - Most important rule
- Use imperative verbs (添加, 修复, 更新)
- Keep description concise (< 50 characters)
- If multiple types of changes, split into separate commits
**Examples:**
```bash
# ✅ Good commits
git commit -m "feat实现玩家二段跳功能"
git commit -m "fix修复角色跳跃时的碰撞检测问题"
git commit -m "test添加角色控制器单元测试"
# ❌ Bad commits
git commit -m "fix + feat修复Bug并添加新功能" # Mixed types
git commit -m "update player" # Vague, English
```
## Progress Tracking
Use TodoWrite tool to track workflow progress:
```gdscript
TodoWrite.create_todos([
"Step 1: 架构分析 - 读取架构规范",
"Step 2: 功能实现 - 按规范编码",
"Step 3: 注释规范检查",
"Step 4: 命名规范检查",
"Step 5: 测试代码编写",
"Step 6: 测试验证 - 运行测试",
"Step 7: Git 提交 - 生成提交信息"
])
```
Mark each step as `completed` immediately after finishing.
## Quality Checklist
Before completing the workflow, verify:
- [ ] File location follows layered architecture (_Core, scenes, UI)
- [ ] Uses EventSystem for cross-module communication
- [ ] Event names added to EventNames.gd
- [ ] All variables and functions have type annotations
- [ ] Naming conventions correct (PascalCase/camelCase/UPPER_CASE)
- [ ] File header comment complete
- [ ] Public functions have complete documentation
- [ ] Unit tests created and passing
- [ ] Git commit message follows specification
- [ ] No Godot 3.x syntax (yield → await)
- [ ] Node references cached with @onready
- [ ] Sprite2D/TileMap use Nearest filter
## Reference Documents
The skill automatically reads these documents at appropriate steps:
- Architecture: `docs/02-开发规范/架构与通信规范.md`
- Comments: `docs/02-开发规范/代码注释规范.md`
- Naming: `docs/02-开发规范/命名规范.md`
- Testing: `docs/03-技术实现/测试指南.md`
- Git: `docs/02-开发规范/Git提交规范.md`
- Project Instructions: `claude.md` (root directory)
For detailed checklist reference, see `references/checklist.md` in this skill directory.
## Example Workflow
User request: "实现玩家二段跳功能"
1. **Architecture Analysis** ✅
- Read architecture spec
- Target: `scenes/Entities/Player/Player.gd`
- Communication: Emit `PLAYER_DOUBLE_JUMPED` event
- Dependencies: EventSystem, Input
- Event: Add `PLAYER_DOUBLE_JUMPED` to EventNames.gd
2. **Implementation** ✅
- Create double jump logic with type annotations
- Use EventSystem.emit_event() for notifications
- Cache references with @onready
- Use await instead of yield
3. **Comment Validation** ✅
- Add file header with feature description
- Document double jump function parameters
- Add inline comments for jump logic
4. **Naming Validation** ✅
- Verify: `var canDoubleJump: bool` (camelCase)
- Verify: `const MAX_DOUBLE_JUMPS: int` (UPPER_CASE)
- Verify: `func performDoubleJump()` (camelCase)
5. **Test Writing** ✅
- Create `tests/unit/test_player_double_jump.gd`
- Test initialization, jump execution, limits
6. **Test Execution** ✅
- Run: `godot --headless -s addons/gut/gut_cmdline.gd`
- All tests pass ✅
7. **Git Commit** ✅
```bash
git add scenes/Entities/Player/Player.gd _Core/EventNames.gd tests/unit/test_player_double_jump.gd
git commit -m "feat实现玩家二段跳功能"
```
## Notes
- This skill enforces quality standards through automated validation
- Each step builds upon the previous, ensuring comprehensive quality control
- Skipping steps will result in incomplete or non-compliant code
- The 7-step workflow is designed for team consistency and maintainability

View File

@@ -0,0 +1,285 @@
# WhaleTown Development Quality Checklist
快速参考检查清单,用于验证代码是否符合项目规范。
## 架构检查清单
### 文件位置
- [ ] 核心系统文件位于 `_Core/managers/``_Core/systems/`
- [ ] 游戏场景文件位于 `scenes/Maps/`, `scenes/Entities/`, `scenes/Components/`
- [ ] UI 组件文件位于 `scenes/ui/`
- [ ] 测试文件位于 `tests/unit/``tests/integration/`
### 通信方式
- [ ] 跨模块通信使用 EventSystem
- [ ] 新增事件定义在 `_Core/EventNames.gd`
- [ ] 遵循 "Signal Up, Call Down" 原则
- [ ] 父节点调用子节点方法(向下调用)
- [ ] 子节点发出信号通知父节点(向上信号)
### 依赖管理
- [ ] 仅使用允许的自动加载GameManager, SceneManager, EventSystem, NetworkManager, ResponseHandler
- [ ] 底层实体Player, NPC不直接访问 GameManager
- [ ] 底层实体通过事件系统与全局管理器通信
---
## 代码规范检查清单
### 类型安全
- [ ] 所有变量都有类型注解:`var speed: float = 200.0`
- [ ] 所有函数都有参数和返回值类型:`func move(delta: float) -> void:`
- [ ] 常量都有类型注解:`const MAX_HEALTH: int = 100`
### Godot 4.2+ 语法
- [ ] 使用 `await` 代替 `yield()`
- [ ] 使用 `@onready` 缓存节点引用
- [ ] 避免在 `_process()` 中使用 `get_node()`
- [ ] 信号连接使用 `.connect()` 语法
### 资源设置
- [ ] 所有 Sprite2D 使用 Nearest 滤镜(不使用 Linear
- [ ] 所有 TileMap 使用 Nearest 滤镜
---
## 命名规范检查清单
### 类名
- [ ] 使用 PascalCase`class_name PlayerController`
- [ ] 文件名与类名一致:`PlayerController.gd`
### 变量
- [ ] 公共变量使用 camelCase`var moveSpeed: float`
- [ ] 私有变量使用下划线前缀:`var _velocity: Vector2`
- [ ] 布尔变量使用 is/has/can 前缀:`var isJumping: bool`
### 函数
- [ ] 使用 camelCase`func updateMovement()`
- [ ] 获取函数使用 `get` 前缀:`func getPlayerPosition()`
- [ ] 设置函数使用 `set` 前缀:`func setHealth(value: int)`
- [ ] 判断函数使用 `is/has/can` 前缀:`func isAlive()`, `func canJump()`
- [ ] 私有函数使用下划线前缀:`func _calculateDamage()`
### 常量
- [ ] 使用 UPPER_CASE`const MAX_HEALTH: int = 100`
- [ ] 使用下划线分隔:`const JUMP_FORCE: float = -400.0`
### 枚举
- [ ] 枚举类型使用 PascalCase`enum PlayerState`
- [ ] 枚举值使用 UPPER_CASE`IDLE, WALKING, RUNNING`
### 文件命名
- [ ] 脚本文件PascalCase.gd (`PlayerController.gd`)
- [ ] 场景文件snake_case_scene.tscn (`main_scene.tscn`)
- [ ] 预制体文件snake_case_prefab.tscn (`player_prefab.tscn`)
- [ ] 资源文件snake_case (`sprite_player_idle.png`)
---
## 注释规范检查清单
### 文件头注释
- [ ] 包含文件名
- [ ] 包含作用描述
- [ ] 列出主要功能
- [ ] 列出依赖的管理器/系统
- [ ] 包含作者和创建时间
示例:
```gdscript
# ============================================================================
# 文件名: PlayerController.gd
# 作用: 玩家角色控制器,处理玩家输入和移动逻辑
#
# 主要功能:
# - 处理键盘和手柄输入
# - 控制角色移动和跳跃
# - 管理角色状态切换
#
# 依赖: EventSystem, InputManager
# 作者: [开发者名称]
# 创建时间: 2025-01-03
# ============================================================================
```
### 函数注释
- [ ] 公共函数有完整注释
- [ ] 包含功能描述
- [ ] 列出参数说明(名称、类型、含义)
- [ ] 说明返回值(类型、含义)
- [ ] 提供使用示例(对于复杂函数)
- [ ] 标注注意事项(如果有)
示例:
```gdscript
# 处理玩家输入并更新移动状态
#
# 参数:
# delta: float - 帧时间间隔
#
# 返回值: 无
#
# 注意事项:
# - 需要在 _physics_process 中调用
# - 会自动处理重力和碰撞
func handleMovement(delta: float) -> void:
```
### 行内注释
- [ ] 复杂逻辑有注释说明
- [ ] 注释解释 WHY为什么不解释 WHAT是什么
- [ ] 避免显而易见的注释
- [ ] 使用 TODO/FIXME/NOTE 等标记
---
## 测试规范检查清单
### 测试文件
- [ ] _Core/ 中的管理器/系统都有对应测试文件
- [ ] 测试文件位于 `tests/unit/``tests/integration/`
- [ ] 测试文件命名:`test_[name].gd`
- [ ] 测试文件继承自 GutTest`extends GutTest`
### 测试结构
- [ ] 包含测试类注释
- [ ] 实现 `before_each()` 进行测试前置设置
- [ ] 实现 `after_each()` 进行测试清理
- [ ] 测试方法命名:`test_[功能名称]()`
### 测试覆盖
- [ ] 测试核心功能的正常流程
- [ ] 测试错误处理和边界条件
- [ ] 测试初始化和清理逻辑
- [ ] 所有测试都能通过
示例:
```gdscript
extends GutTest
## PlayerController 单元测试
var player: PlayerController
func before_each():
player = preload("res://scenes/Entities/Player/PlayerController.gd").new()
add_child(player)
func after_each():
player.queue_free()
func test_initialization():
var result = player.initialize()
assert_true(result, "Player should initialize successfully")
func test_movement():
# 测试移动功能
pass
```
---
## Git 提交规范检查清单
### 提交类型
- [ ] 使用正确的提交类型:
- `feat` - 新功能
- `fix` - Bug 修复
- `docs` - 文档更新
- `refactor` - 代码重构
- `test` - 测试相关
- `scene` - 场景文件
- `ui` - UI 相关
### 提交格式
- [ ] 使用中文冒号(:)
- [ ] 描述简洁明了(< 50 字符)
- [ ] 使用动词开头(添加、修复、更新)
- [ ] 一次提交只包含一种类型的改动
### 提交原则
- [ ] 一次提交只做一件事
- [ ] 提交的代码能够正常运行
- [ ] 避免 fix + feat 混合提交
- [ ] 如需多种改动,拆分成多次提交
示例:
```bash
# ✅ 正确
git commit -m "feat实现玩家二段跳功能"
git commit -m "fix修复角色跳跃时的碰撞检测问题"
git commit -m "test添加角色控制器单元测试"
# ❌ 错误
git commit -m "fix + feat修复Bug并添加新功能" # 混合类型
git commit -m "update player" # 描述不清晰,使用英文
git commit -m "fix: 修复Bug" # 使用英文冒号
```
---
## 完整工作流检查清单
使用此清单验证开发任务是否完整执行 7 步工作流:
### Step 1: 架构分析
- [ ] 已读取 `docs/02-开发规范/架构与通信规范.md`
- [ ] 已确定文件位置_Core, scenes, UI
- [ ] 已确定通信方式EventSystem
- [ ] 已列出依赖的管理器/系统
- [ ] 已设计事件定义(如需要)
### Step 2: 功能实现
- [ ] 代码遵循分层架构
- [ ] 所有变量和函数有类型注解
- [ ] 使用 Godot 4.2+ 语法
- [ ] 使用 EventSystem 进行跨模块通信
- [ ] 使用 @onready 缓存节点引用
### Step 3: 注释规范检查
- [ ] 已读取 `docs/02-开发规范/代码注释规范.md`
- [ ] 文件头注释完整
- [ ] 公共函数有完整注释
- [ ] 复杂逻辑有行内注释
### Step 4: 命名规范检查
- [ ] 已读取 `docs/02-开发规范/命名规范.md`
- [ ] 类名使用 PascalCase
- [ ] 变量/函数使用 camelCase
- [ ] 常量使用 UPPER_CASE
- [ ] 私有成员使用下划线前缀
### Step 5: 测试代码编写
- [ ] 已读取 `docs/03-技术实现/测试指南.md`
- [ ] 创建了测试文件 `tests/unit/test_[name].gd`
- [ ] 测试文件继承自 GutTest
- [ ] 编写了核心功能测试
### Step 6: 测试验证
- [ ] 运行了 GUT 测试命令
- [ ] 所有测试通过
- [ ] 如有失败,已修复并重新测试
### Step 7: Git 提交
- [ ] 已读取 `docs/02-开发规范/Git提交规范.md`
- [ ] 生成了符合规范的提交信息
- [ ] 提交类型正确
- [ ] 使用中文冒号
- [ ] 遵循"一次提交只做一件事"原则
---
## 快速自检问题
在提交代码前,问自己以下问题:
1. **架构**: 文件放在正确的位置了吗?
2. **通信**: 是否使用 EventSystem 进行跨模块通信?
3. **类型**: 所有变量和函数都有类型注解吗?
4. **命名**: 命名是否符合规范PascalCase/camelCase/UPPER_CASE
5. **注释**: 文件头和公共函数有完整注释吗?
6. **测试**: 创建并运行测试了吗?所有测试都通过了吗?
7. **提交**: Git 提交信息符合规范吗?
如果以上问题都能回答"是",那么代码已经符合 WhaleTown 项目的质量标准!✅

View File

@@ -0,0 +1,312 @@
# WhaleTown Developer Skill 使用说明
## 📖 简介
`whaletown-developer` 是 WhaleTown 项目的标准开发工作流自动化技能,确保所有开发任务都遵循统一的项目规范和质量标准。
## 🎯 适用场景
在以下情况下使用此 skill
- ✅ 实现新功能("实现玩家二段跳"、"添加存档系统"
- ✅ 修复 Bug"修复角色碰撞问题"、"解决UI显示错误"
- ✅ 创建场景("创建商店场景"、"设计背包界面"
- ✅ 开发模块("开发任务系统"、"构建战斗组件"
- ✅ 任何需要遵循项目规范的代码开发任务
## 🚀 调用方式
### 方式一:通过 Claude推荐
```
用户:帮我实现一个 NPC
Claude/whaletown-developer 实现一个 NPC
```
### 方式二:直接请求
```
用户:使用 whaletown-developer skill 创建玩家移动系统
```
## 📋 7 步工作流程
skill 会自动执行以下标准化流程:
```
Step 1: 架构分析
↓ 读取架构规范,确定文件位置、通信方式、依赖关系
Step 2: 功能实现
↓ 按照类型安全、命名规范、EventSystem 通信等要求编码
Step 3: 注释规范检查
↓ 验证文件头、函数文档、行内注释是否完整
Step 4: 命名规范检查
↓ 验证 PascalCase/camelCase/UPPER_CASE 命名规范
Step 5: 测试代码编写
↓ 为核心功能创建 GUT 单元测试
Step 6: 测试验证
↓ 运行测试确保功能正常
Step 7: Git 提交
↓ 生成符合规范的提交信息
```
## 💡 使用示例
### 示例 1创建玩家控制器
**用户输入:**
```
帮我创建一个玩家角色控制器
```
**Skill 执行:**
1. 分析架构 → 确定放在 `scenes/Entities/Player/PlayerController.gd`
2. 实现功能 → 创建带类型注解的移动、跳跃逻辑
3. 检查注释 → 添加完整的文件头和函数文档
4. 检查命名 → 确保 `moveSpeed``MAX_HEALTH` 等命名正确
5. 编写测试 → 创建 `tests/unit/test_player_controller.gd`
6. 运行测试 → 验证功能正常
7. 生成提交 → `feat实现玩家角色控制器`
### 示例 2修复跳跃 Bug
**用户输入:**
```
修复玩家跳跃时的碰撞检测问题
```
**Skill 执行:**
1. 分析架构 → 定位到 `scenes/Entities/Player/Player.gd`
2. 实现修复 → 修改碰撞检测逻辑
3. 检查注释 → 添加修复说明注释
4. 检查命名 → 确保变量命名规范
5. 编写测试 → 添加碰撞测试用例
6. 运行测试 → 确认 Bug 已修复
7. 生成提交 → `fix修复玩家跳跃时的碰撞检测问题`
### 示例 3添加事件通信
**用户输入:**
```
实现 NPC 对话系统的事件通信
```
**Skill 执行:**
1. 分析架构 → 使用 EventSystem 跨模块通信
2. 实现功能 → 在 EventNames.gd 添加 `NPC_DIALOG_STARTED` 事件
3. 检查注释 → 文档化事件数据格式
4. 检查命名 → 确保事件名称符合规范
5. 编写测试 → 测试事件发送和接收
6. 运行测试 → 验证通信正常
7. 生成提交 → `feat实现NPC对话系统的事件通信`
## ✅ 质量保证
每次使用 skill 后,代码都会符合以下标准:
### 架构层面
- ✅ 文件位置符合分层架构_Core、scenes、UI
- ✅ 使用 EventSystem 实现跨模块通信
- ✅ 事件名称已添加到 EventNames.gd
- ✅ 遵循"Signal Up, Call Down"原则
### 代码质量
- ✅ 所有变量和函数都有类型注解
- ✅ 命名规范正确PascalCase/camelCase/UPPER_CASE
- ✅ 使用 Godot 4.2+ 语法await 而非 yield
- ✅ 节点引用使用 @onready 缓存
### 文档规范
- ✅ 文件头注释完整(文件名、作用、功能、依赖)
- ✅ 公共函数有完整文档(参数、返回值、示例)
- ✅ 复杂逻辑有行内注释说明
### 测试覆盖
- ✅ 核心功能有单元测试
- ✅ 测试文件命名规范test_*.gd
- ✅ 测试通过验证
### Git 规范
- ✅ 提交信息格式正确(类型:描述)
- ✅ 遵循"一次提交只做一件事"原则
- ✅ 使用中文冒号和动词开头
## 📚 相关文档
Skill 会自动读取以下规范文档:
- `docs/02-开发规范/架构与通信规范.md` - 分层架构和 EventSystem
- `docs/02-开发规范/代码注释规范.md` - 注释格式要求
- `docs/02-开发规范/命名规范.md` - 命名约定
- `docs/03-技术实现/测试指南.md` - 测试框架使用
- `docs/02-开发规范/Git提交规范.md` - 提交信息格式
- `CLAUDE.md` - 项目总体指导
## ⚙️ 配置文件
Skill 相关配置文件位置:
```
.claude/skills/whaletown-developer/
├── SKILL.md # Skill 定义文件
├── 使用说明.md # 本文档
└── references/
└── checklist.md # 质量检查清单
```
## 🔄 工作流程可视化
```
用户请求
调用 whaletown-developer skill
[Step 1] 架构分析
├─ 读取架构规范文档
├─ 确定文件位置
├─ 识别通信方式
└─ 设计事件定义
[Step 2] 功能实现
├─ 遵循类型安全
├─ 使用 EventSystem
├─ 缓存节点引用
└─ 使用 Godot 4.2+ 语法
[Step 3] 注释规范检查
├─ 验证文件头注释
├─ 验证函数文档
└─ 检查行内注释
[Step 4] 命名规范检查
├─ 类名 PascalCase
├─ 变量/函数 camelCase
├─ 常量 UPPER_CASE
└─ 私有成员 _prefix
[Step 5] 测试代码编写
├─ 创建测试文件
├─ 编写测试用例
└─ 覆盖核心功能
[Step 6] 测试验证
├─ 运行 GUT 测试
├─ 验证测试通过
└─ 修复失败测试
[Step 7] Git 提交
├─ 确定提交类型
├─ 生成提交信息
└─ 遵循提交规范
完成 ✅
```
## 🎓 最佳实践
### 1. 明确任务描述
```bash
# ✅ 好的描述
"实现玩家二段跳功能"
"修复敌人AI路径寻找Bug"
"创建商店购买界面"
# ❌ 模糊描述
"改一下玩家"
"修复Bug"
"做个界面"
```
### 2. 一次处理一个功能
```bash
# ✅ 推荐
用户:实现玩家移动
用户:实现玩家跳跃
# ❌ 不推荐
用户:实现玩家移动、跳跃、攻击、技能系统
```
### 3. 信任 Skill 流程
- Skill 会按照 7 步流程确保质量
- 不需要手动检查命名、注释等细节
- 专注于功能需求和业务逻辑
### 4. 查看生成的提交信息
- Skill 会在 Step 7 生成规范的提交信息
- 可以直接使用或根据需要微调
## ⚠️ 注意事项
1. **首次使用**
- 确保已阅读 `CLAUDE.md` 了解项目规范
- 确认所有规范文档都已存在
2. **测试环境**
- 确保 GUT 测试框架已安装(如需运行测试)
- Godot 可执行文件在 PATH 中Step 6 测试执行)
3. **中断处理**
- 如果工作流被中断,可以继续执行剩余步骤
- Skill 使用 TodoWrite 追踪进度
4. **规范更新**
- 项目规范文档更新时Skill 会自动读取最新版本
- 无需手动同步
## 🤝 反馈与改进
如果遇到问题或有改进建议:
1. 检查是否所有规范文档都已更新
2. 确认任务描述清晰明确
3. 查看 Skill 执行日志定位问题
4. 向团队报告问题或建议
## 📊 效果对比
### 不使用 Skill
```
开发者手动:
1. 不确定文件放哪里 ❌
2. 可能忘记类型注解 ❌
3. 注释不完整 ❌
4. 命名不一致 ❌
5. 没有测试 ❌
6. 提交信息格式错误 ❌
结果:代码质量参差不齐
```
### 使用 Skill
```
Skill 自动化:
1. 自动确定正确位置 ✅
2. 强制类型安全 ✅
3. 完整注释文档 ✅
4. 统一命名规范 ✅
5. 自动生成测试 ✅
6. 规范提交信息 ✅
结果:高质量、一致性代码
```
## 🎯 总结
whaletown-developer skill 是你的开发助手,它会:
- 🤖 **自动化** 7 步标准流程
- 📏 **标准化** 代码质量
- 🔒 **保证** 规范遵循
-**加速** 开发效率
- 🧪 **确保** 测试覆盖
**记住:专注于功能实现,让 Skill 处理规范和质量!**
---
**开始使用:** 只需告诉 Claude "帮我实现 XXX 功能" 即可!

View File

@@ -51,8 +51,21 @@ const SCENE_DATA_TRANSFER = "scene_data_transfer"
const TILEMAP_READY = "tilemap_ready" const TILEMAP_READY = "tilemap_ready"
const COMPONENT_MESSAGE = "component_message" const COMPONENT_MESSAGE = "component_message"
const POSITION_UPDATE = "position_update" const POSITION_UPDATE = "position_update"
const GRID_POSITION_CHANGED = "grid_position_changed"
const GRID_SNAP_REQUESTED = "grid_snap_requested"
# ============================================================================ # ============================================================================
# 测试事件 # 测试事件
# ============================================================================ # ============================================================================
const TEST_EVENT = "test_event" const TEST_EVENT = "test_event"
# ============================================================================
# 聊天事件
# ============================================================================
const CHAT_MESSAGE_SENT = "chat_message_sent"
const CHAT_MESSAGE_RECEIVED = "chat_message_received"
const CHAT_ERROR_OCCURRED = "chat_error_occurred"
const CHAT_CONNECTION_STATE_CHANGED = "chat_connection_state_changed"
const CHAT_POSITION_UPDATED = "chat_position_updated"
const CHAT_LOGIN_SUCCESS = "chat_login_success"
const CHAT_LOGIN_FAILED = "chat_login_failed"

View File

@@ -19,6 +19,11 @@ const CORE_SYSTEMS = CORE_ROOT + "systems/"
const CORE_COMPONENTS = CORE_ROOT + "components/" const CORE_COMPONENTS = CORE_ROOT + "components/"
const CORE_UTILS = CORE_ROOT + "utils/" 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"
# ============================================================================ # ============================================================================
# 场景路径 # 场景路径
# ============================================================================ # ============================================================================
@@ -46,6 +51,10 @@ const ASSETS_FONTS = ASSETS_ROOT + "fonts/"
const ASSETS_MATERIALS = ASSETS_ROOT + "materials/" const ASSETS_MATERIALS = ASSETS_ROOT + "materials/"
const ASSETS_SHADERS = ASSETS_ROOT + "shaders/" const ASSETS_SHADERS = ASSETS_ROOT + "shaders/"
# 地形资源路径
const ASSETS_TERRAIN = ASSETS_SPRITES + "terrain/"
const ASSETS_GRASS = ASSETS_TERRAIN + "grass/"
# ============================================================================ # ============================================================================
# 数据路径 # 数据路径
# ============================================================================ # ============================================================================

View File

@@ -68,6 +68,9 @@ enum LoginMode {
# ============ 成员变量 ============ # ============ 成员变量 ============
# 当前用户ID静态变量用于BaseLevel等场景访问
static var current_user_id: String = ""
# 登录状态 # 登录状态
var current_login_mode: LoginMode = LoginMode.PASSWORD var current_login_mode: LoginMode = LoginMode.PASSWORD
var is_processing: bool = false var is_processing: bool = false
@@ -80,19 +83,188 @@ var current_email: String = ""
# 网络请求管理 # 网络请求管理
var active_request_ids: Array = [] var active_request_ids: Array = []
# ============ Token 管理 ============
# 本地存储路径常量
const AUTH_CONFIG_PATH: String = "user://auth.cfg"
# Token 存储(内存中,用于快速访问)
var _access_token: String = "" # JWT访问令牌短期用于API和WebSocket
var _refresh_token: String = "" # JWT刷新令牌长期用于获取新access_token
var _user_info: Dictionary = {} # 用户信息
var _token_expiry: float = 0.0 # access_token过期时间Unix时间戳
# 游戏 token废弃保留供旧接口
var _game_token: String = "" # @deprecated 使用 _access_token 替代
# ============ 生命周期方法 ============ # ============ 生命周期方法 ============
# 初始化管理器 # 初始化管理器
func _init(): func _init() -> void:
print("AuthManager 初始化完成") _load_auth_data()
# 清理资源 # 清理资源
func cleanup(): func cleanup() -> void:
# 取消所有活动的网络请求 # 取消所有活动的网络请求
for request_id in active_request_ids: for request_id in active_request_ids:
NetworkManager.cancel_request(request_id) NetworkManager.cancel_request(request_id)
active_request_ids.clear() active_request_ids.clear()
# ============ Token 管理 ============
# 保存 Token 到内存
#
# 参数:
# data: Dictionary - 登录响应数据
#
# 功能:
# - 从登录响应中提取 access_token 和 refresh_token
# - 保存到内存变量中
# - 保存用户信息
func _save_tokens_to_memory(data: Dictionary) -> void:
if not data.has("data"):
push_warning("AuthManager: 登录响应中没有 data 字段")
return
var token_data: Dictionary = data.data
_access_token = token_data.get("access_token", "")
_refresh_token = token_data.get("refresh_token", "")
_user_info = token_data.get("user", {})
_token_expiry = Time.get_unix_time_from_system() + float(token_data.get("expires_in", 0))
# 设置当前用户ID用于BaseLevel等场景
if _user_info.has("id"):
AuthManager.current_user_id = str(_user_info.id)
_game_token = _access_token
# 保存 Token 到本地ConfigFile
#
# 参数:
# data: Dictionary - 登录响应数据
#
# 功能:
# - 将 refresh_token 和用户信息保存到 ConfigFile
# - access_token 不保存到本地,仅保存在内存中
func _save_tokens_to_local(data: Dictionary) -> void:
if not data.has("data"):
return
var token_data: Dictionary = data.data
var auth_data: Dictionary = {
"refresh_token": token_data.get("refresh_token", ""),
"user_id": token_data.get("user", {}).get("id", ""),
"username": token_data.get("user", {}).get("username", ""),
"saved_at": Time.get_unix_time_from_system()
}
var config: ConfigFile = ConfigFile.new()
config.load(AUTH_CONFIG_PATH)
config.set_value("auth", "refresh_token", auth_data["refresh_token"])
config.set_value("auth", "user_id", auth_data["user_id"])
config.set_value("auth", "username", auth_data["username"])
config.set_value("auth", "saved_at", auth_data["saved_at"])
var error: Error = config.save(AUTH_CONFIG_PATH)
if error != OK:
push_error("AuthManager: 保存Token到本地失败错误码: %d" % error)
# 从本地加载 Token游戏启动时调用
#
# 功能:
# - 从 ConfigFile 加载 refresh_token 和用户信息
# - access_token 需要通过 refresh_token 刷新获取
func _load_auth_data() -> void:
if not FileAccess.file_exists(AUTH_CONFIG_PATH):
return
var config: ConfigFile = ConfigFile.new()
var error: Error = config.load(AUTH_CONFIG_PATH)
if error != OK:
push_error("AuthManager: 加载本地认证数据失败,错误码: %d" % error)
return
_refresh_token = config.get_value("auth", "refresh_token", "")
var user_id: String = config.get_value("auth", "user_id", "")
var username: String = config.get_value("auth", "username", "")
if not _refresh_token.is_empty():
_user_info = {
"id": user_id,
"username": username
}
else:
push_warning("AuthManager: 本地认证数据无效(没有 refresh_token")
# 清除本地认证数据(登出时调用)
#
# 功能:
# - 清除内存中的 Token
# - 删除本地 ConfigFile
func _clear_auth_data() -> void:
_access_token = ""
_refresh_token = ""
_user_info = {}
_token_expiry = 0.0
_game_token = ""
if FileAccess.file_exists(AUTH_CONFIG_PATH):
DirAccess.remove_absolute(AUTH_CONFIG_PATH)
# ============ Token 访问方法 ============
# 设置游戏 token建议优先使用 _save_tokens_to_memory
#
# 参数:
# token: String - 游戏认证 token
#
# 使用场景:
# - 登录成功后设置 token
# - 从服务器响应中获取 token
func set_game_token(token: String) -> void:
_game_token = token
_access_token = token # 同步更新 access_token
# 获取游戏 token
#
# 返回值:
# String - access_token如果未设置则返回空字符串
#
# 使用场景:
# - ChatManager 连接 WebSocket 时需要 token
# - 其他需要游戏认证的场景
func get_game_token() -> String:
return _access_token
# 获取 access token
#
# 返回值:
# String - JWT访问令牌
#
# 使用场景:
# - API请求认证
# - WebSocket聊天认证
func get_access_token() -> String:
return _access_token
# 获取 refresh token
#
# 返回值:
# String - JWT刷新令牌
#
# 使用场景:
# - 刷新过期的 access token
func get_refresh_token() -> String:
return _refresh_token
# 获取用户信息
#
# 返回值:
# Dictionary - 用户信息字典
func get_user_info() -> Dictionary:
return _user_info
# ============ 登录相关方法 ============ # ============ 登录相关方法 ============
# 执行密码登录 # 执行密码登录
@@ -421,7 +593,7 @@ func validate_verification_code(code: String) -> Dictionary:
# ============ 网络响应处理 ============ # ============ 网络响应处理 ============
# 处理登录响应 # 处理登录响应
func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary): func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary) -> void:
_reset_login_state() _reset_login_state()
var result = ResponseHandler.handle_login_response(success, data, error_info) var result = ResponseHandler.handle_login_response(success, data, error_info)
@@ -430,9 +602,11 @@ func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary)
show_toast_message.emit(result.message, result.success) show_toast_message.emit(result.message, result.success)
if result.success: if result.success:
var username = "" # 保存 Token 到内存和本地
if data.has("data") and data.data.has("user") and data.data.user.has("username"): _save_tokens_to_memory(data)
username = data.data.user.username _save_tokens_to_local(data)
var username: String = _user_info.get("username", "")
# 延迟发送登录成功信号 # 延迟发送登录成功信号
await Engine.get_main_loop().create_timer(1.0).timeout await Engine.get_main_loop().create_timer(1.0).timeout
@@ -441,7 +615,7 @@ func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary)
login_failed.emit(result.message) login_failed.emit(result.message)
# 处理验证码登录响应 # 处理验证码登录响应
func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary): func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary) -> void:
_reset_login_state() _reset_login_state()
var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info) var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info)
@@ -450,9 +624,11 @@ func _on_verification_login_response(success: bool, data: Dictionary, error_info
show_toast_message.emit(result.message, result.success) show_toast_message.emit(result.message, result.success)
if result.success: if result.success:
var username = "" # 保存 Token 到内存和本地
if data.has("data") and data.data.has("user") and data.data.user.has("username"): _save_tokens_to_memory(data)
username = data.data.user.username _save_tokens_to_local(data)
var username: String = _user_info.get("username", "")
await Engine.get_main_loop().create_timer(1.0).timeout await Engine.get_main_loop().create_timer(1.0).timeout
login_success.emit(username) login_success.emit(username)

View File

@@ -0,0 +1,726 @@
extends Node
# ============================================================================
# ChatManager.gd - 聊天系统业务逻辑核心
# ============================================================================
# 管理聊天功能的核心业务逻辑
#
# 核心职责:
# - 聊天消息发送/接收协调
# - 客户端频率限制10条/分钟)
# - 消息历史管理最多100条
# - Signal Up: 通过信号和 EventSystem 向上通知
# - 整合 AuthManager 获取 token
#
# 使用方式:
# ChatManager.connect_to_chat_server()
# ChatManager.send_chat_message("Hello", "local")
# ChatManager.chat_message_received.connect(_on_message_received)
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 遵循 "Signal Up, Call Down" 架构
# - 所有聊天事件通过 EventSystem 广播
# ============================================================================
# ============================================================================
# 信号定义 (Signal Up)
# ============================================================================
# 聊天消息已发送信号
# 参数:
# message_id: String - 消息 ID
# timestamp: float - 时间戳
signal chat_message_sent(message_id: String, timestamp: float)
# 聊天消息已接收信号
# 参数:
# from_user: String - 发送者用户名
# content: String - 消息内容
# show_bubble: bool - 是否显示气泡
# timestamp: float - 时间戳
signal chat_message_received(from_user: String, content: String, show_bubble: bool, timestamp: float)
# 聊天错误发生信号
# 参数:
# error_code: String - 错误代码
# message: String - 错误消息
signal chat_error_occurred(error_code: String, message: String)
# 聊天连接状态变化信号
# 参数:
# state: int - 连接状态0=DISCONNECTED, 1=CONNECTING, 2=CONNECTED, 3=RECONNECTING, 4=ERROR
signal chat_connection_state_changed(state: int)
# 位置更新成功信号
# 参数:
# stream: String - Stream 名称
# topic: String - Topic 名称
signal chat_position_updated(stream: String, topic: String)
# ============================================================================
# 常量定义
# ============================================================================
# WebSocket 服务器 URL原生 WebSocket
const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game"
# 重连配置
const RECONNECT_MAX_ATTEMPTS: int = 5
const RECONNECT_BASE_DELAY: float = 3.0
# 频率限制配置
const RATE_LIMIT_MESSAGES: int = 10
const RATE_LIMIT_WINDOW: float = 60.0 # 秒
# 消息限制
const MAX_MESSAGE_LENGTH: int = 1000
# 当前会话消息限制(当前游戏会话,超过后删除最旧的)
const MAX_SESSION_MESSAGES: int = 100
# 历史消息分页大小(从 Zulip 后端每次加载的数量)
const HISTORY_PAGE_SIZE: int = 100
# 错误消息映射
const CHAT_ERROR_MESSAGES: Dictionary = {
"AUTH_FAILED": "聊天认证失败,请重新登录",
"RATE_LIMIT": "消息发送过于频繁,请稍后再试",
"CONTENT_FILTERED": "消息内容包含违规内容",
"CONTENT_TOO_LONG": "消息内容过长最大1000字符",
"PERMISSION_DENIED": "您没有权限发送消息",
"SESSION_EXPIRED": "会话已过期,请重新连接",
"ZULIP_ERROR": "消息服务暂时不可用",
"INTERNAL_ERROR": "服务器内部错误"
}
# ============================================================================
# 成员变量
# ============================================================================
# WebSocket 管理器
var _websocket_manager: ChatWebSocketManager
# 是否已登录
var _is_logged_in: bool = false
# 消息历史记录当前会话最多100条超过后删除最旧的
var _message_history: Array[Dictionary] = []
# 历史消息加载状态
var _history_loading: bool = false
var _has_more_history: bool = true
var _oldest_message_timestamp: float = 0.0
# 消息发送时间戳(用于频率限制)
var _message_timestamps: Array[float] = []
# 当前用户信息
var _current_username: String = ""
var _current_map: String = ""
# 游戏 token
var _game_token: String = ""
# 发送后本地回显去重(避免服务端也回发导致重复显示)
const SELF_ECHO_DEDUPE_WINDOW: float = 10.0
var _pending_self_messages: Array[Dictionary] = []
# ============================================================================
# 生命周期方法
# ============================================================================
# 初始化
func _ready() -> void:
# 创建 WebSocket 管理器
_websocket_manager = ChatWebSocketManager.new()
add_child(_websocket_manager)
# 连接信号
_connect_signals()
# 清理
func _exit_tree() -> void:
if is_instance_valid(_websocket_manager):
_websocket_manager.queue_free()
# ============================================================================
# 公共 API - Token 管理
# ============================================================================
# 设置游戏 token
#
# 参数:
# token: String - 游戏认证 token
#
# 使用示例:
# ChatManager.set_game_token("your_game_token")
func set_game_token(token: String) -> void:
_game_token = token
# 获取游戏 token
#
# 返回值:
# String - 当前游戏 token
func get_game_token() -> String:
return _game_token
# ============================================================================
# 公共 API - 连接管理
# ============================================================================
# 连接到聊天服务器
func connect_to_chat_server() -> void:
if _websocket_manager.is_websocket_connected():
push_warning("聊天服务器已连接")
return
_websocket_manager.connect_to_game_server()
# 断开聊天服务器
func disconnect_from_chat_server() -> void:
# 发送登出消息
if _is_logged_in:
var logout_data := {"type": "logout"}
_websocket_manager.send_message(JSON.stringify(logout_data))
_is_logged_in = false
# 断开连接
_websocket_manager.disconnect_websocket()
# 检查是否已连接
#
# 返回值:
# bool - 是否已连接
func is_chat_connected() -> bool:
return _websocket_manager.is_websocket_connected()
# ============================================================================
# 公共 API - 聊天操作
# ============================================================================
# 发送聊天消息
#
# 参数:
# content: String - 消息内容
# scope: String - 消息范围("local" 或具体 topic 名称)
#
# 使用示例:
# ChatManager.send_chat_message("Hello, world!", "local")
func send_chat_message(content: String, scope: String = "local") -> void:
# 检查连接状态
if not _websocket_manager.is_websocket_connected():
_handle_error("NOT_CONNECTED", "未连接到聊天服务器")
return
# 检查登录状态
if not _is_logged_in:
_handle_error("NOT_LOGGED_IN", "尚未登录聊天服务器")
return
# 检查消息长度
if content.length() > MAX_MESSAGE_LENGTH:
_handle_error("CONTENT_TOO_LONG", "消息内容过长")
return
# 检查频率限制
if not can_send_message():
var wait_time := get_time_until_next_message()
_handle_error("RATE_LIMIT", "请等待 %.1f 秒后再试" % wait_time)
return
# 构建消息数据
var message_data := {
"type": "chat",
"content": content,
"scope": scope
}
# 发送消息JSON 字符串)
var json_string := JSON.stringify(message_data)
var send_err: Error = _websocket_manager.send_message(json_string)
if send_err != OK:
_handle_error("SEND_FAILED", "WebSocket send failed: %s" % error_string(send_err))
return
# 记录发送时间
_record_message_timestamp()
# 添加到历史
_add_message_to_history({
"from_user": _current_username,
"content": content,
"timestamp": Time.get_unix_time_from_system(),
"is_self": true
})
var now_timestamp: float = Time.get_unix_time_from_system()
# 记录待去重的“自己消息”(如果服务端也回发 chat_render则避免重复显示
_pending_self_messages.append({
"content": content,
"expires_at": now_timestamp + SELF_ECHO_DEDUPE_WINDOW
})
# 本地回显UI 目前只订阅 CHAT_MESSAGE_RECEIVED所以这里也发一次 received
chat_message_received.emit(_current_username, content, true, now_timestamp)
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
"from_user": _current_username,
"content": content,
"show_bubble": true,
"timestamp": now_timestamp,
"is_self": true
})
# 消息发送完成回调
func _on_chat_message_sent(_request_id: String, success: bool, data: Dictionary, error_info: Dictionary) -> void:
if success:
var message_id: String = str(data.get("data", {}).get("id", ""))
var timestamp: float = Time.get_unix_time_from_system()
chat_message_sent.emit(message_id, timestamp)
EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, {
"message_id": message_id,
"timestamp": timestamp
})
else:
_handle_error("SEND_FAILED", error_info.get("message", "发送失败"))
# 更新玩家位置
#
# 参数:
# x: float - X 坐标
# y: float - Y 坐标
# map_id: String - 地图 ID
#
# 使用示例:
# ChatManager.update_player_position(150.0, 200.0, "novice_village")
func update_player_position(x: float, y: float, map_id: String) -> void:
if not _websocket_manager.is_websocket_connected():
return
var position_data := {
"type": "position",
"x": x,
"y": y,
"mapId": map_id
}
# 发送消息JSON 字符串)
var json_string := JSON.stringify(position_data)
_websocket_manager.send_message(json_string)
# ============================================================================
# 公共 API - 频率限制
# ============================================================================
# 检查是否可以发送消息
#
# 返回值:
# bool - 是否可以发送
func can_send_message() -> bool:
var current_time := Time.get_unix_time_from_system()
# 清理过期的时间戳
var filter_func := func(timestamp: float) -> bool:
return current_time - timestamp < RATE_LIMIT_WINDOW
_message_timestamps = _message_timestamps.filter(filter_func)
# 检查数量
return _message_timestamps.size() < RATE_LIMIT_MESSAGES
# 获取距离下次可发送消息的时间
#
# 返回值:
# float - 等待时间(秒)
func get_time_until_next_message() -> float:
if _message_timestamps.is_empty():
return 0.0
if _message_timestamps.size() < RATE_LIMIT_MESSAGES:
return 0.0
# 找到最早的时间戳
var earliest_timestamp: float = _message_timestamps[0]
var current_time := Time.get_unix_time_from_system()
var elapsed := current_time - earliest_timestamp
if elapsed >= RATE_LIMIT_WINDOW:
return 0.0
return RATE_LIMIT_WINDOW - elapsed
# ============================================================================
# 公共 API - 消息历史
# ============================================================================
# 获取消息历史
#
# 返回值:
# Array[Dictionary] - 消息历史数组
func get_message_history() -> Array[Dictionary]:
return _message_history.duplicate()
# 清空消息历史
func clear_message_history() -> void:
_message_history.clear()
# 重置当前会话(每次登录/重连时调用)
#
# 功能:
# - 清空当前会话消息缓存
# - 重置历史消息加载状态
# - 不影响 Zulip 后端的历史消息
#
# 使用场景:
# - 用户登录成功后
# - 重新连接到聊天服务器后
func reset_session() -> void:
_message_history.clear()
_history_loading = false
_has_more_history = true
_oldest_message_timestamp = 0.0
# 加载历史消息(按需从 Zulip 后端获取)
#
# 参数:
# count: int - 要加载的消息数量(默认 HISTORY_PAGE_SIZE
#
# 功能:
# - 从 Zulip 后端获取历史消息
# - 添加到当前会话历史开头
# - 触发 CHAT_MESSAGE_RECEIVED 事件显示消息
#
# 使用场景:
# - 用户滚动到聊天窗口顶部
# - 用户主动点击"加载历史"按钮
#
# 注意:
# - 这是异步操作,需要通过 Zulip API 实现
# - 当前实现为占位符,需要后端 API 支持
func load_history(_count: int = HISTORY_PAGE_SIZE) -> void:
if _history_loading:
return
if not _has_more_history:
return
_history_loading = true
# TODO: 实现从 Zulip 后端获取历史消息
# NetworkManager.get_chat_history(_oldest_message_timestamp, count, _on_history_loaded)
# 历史消息加载完成回调
func _on_history_loaded(messages: Array) -> void:
_history_loading = false
if messages.is_empty():
_has_more_history = false
return
# 将历史消息插入到当前会话历史开头
for i in range(messages.size() - 1, -1, -1):
var message: Dictionary = messages[i]
_message_history.push_front(message)
# 触发事件显示消息Signal Up
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
"from_user": message.get("from_user", ""),
"content": message.get("content", ""),
"show_bubble": false,
"timestamp": message.get("timestamp", 0.0),
"is_self": (not _current_username.is_empty() and message.get("from_user", "") == _current_username),
"is_history": true # 标记为历史消息
})
# 更新最旧消息时间戳
var oldest: Dictionary = messages.back()
if oldest.has("timestamp"):
_oldest_message_timestamp = oldest.timestamp
# 检查是否还有更多历史
if messages.size() < HISTORY_PAGE_SIZE:
_has_more_history = false
# ============================================================================
# 内部方法 - 信号连接
# ============================================================================
# 连接信号
func _connect_signals() -> void:
# WebSocket 管理器信号
_websocket_manager.connection_state_changed.connect(_on_connection_state_changed)
_websocket_manager.data_received.connect(_on_data_received)
# 发送登录消息
func _send_login_message() -> void:
var login_data := {
"type": "login",
"token": _game_token
}
var json_string := JSON.stringify(login_data)
_websocket_manager.send_message(json_string)
# 连接状态变化
func _on_connection_state_changed(state: int) -> void:
# 发射信号
chat_connection_state_changed.emit(state)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, {
"state": state
})
# 如果连接成功,发送登录消息
if state == 2: # CONNECTED
_send_login_message()
# ============================================================================
# 内部方法 - 消息处理
# ============================================================================
# WebSocket 数据接收
func _on_data_received(message: String) -> void:
# 解析 JSON 消息
var json := JSON.new()
var parse_result := json.parse(message)
if parse_result != OK:
push_error("ChatManager: JSON 解析失败")
return
var data: Dictionary = json.data
# 检查消息类型字段
var message_type: String = data.get("t", "")
match message_type:
"login_success":
_handle_login_success(data)
"login_error":
_handle_login_error(data)
"chat":
_handle_chat_render(data)
"chat_sent":
_handle_chat_sent(data)
"chat_error":
_handle_chat_error(data)
"chat_render":
_handle_chat_render(data)
"position_updated":
_handle_position_updated(data)
_:
push_warning("ChatManager: 未处理的消息类型 %s" % message_type)
# 处理登录成功
func _handle_login_success(data: Dictionary) -> void:
_is_logged_in = true
_current_username = data.get("username", "")
_current_map = data.get("currentMap", "")
# 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息)
reset_session()
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_LOGIN_SUCCESS, {
"username": _current_username,
"current_map": _current_map
})
# 处理登录失败
func _handle_login_error(data: Dictionary) -> void:
var error_message: String = data.get("message", "登录失败")
_is_logged_in = false
# 通过 EventSystem 广播错误Signal Up
EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, {
"error_code": "LOGIN_FAILED",
"message": error_message
})
# 处理聊天消息发送成功
func _handle_chat_sent(data: Dictionary) -> void:
var message_id: String = str(data.get("messageId", ""))
var timestamp: float = data.get("timestamp", 0.0)
# 发射信号
chat_message_sent.emit(message_id, timestamp)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, {
"message_id": message_id,
"timestamp": timestamp
})
# 处理聊天消息发送失败
func _handle_chat_error(data: Dictionary) -> void:
var error_message: String = data.get("message", "消息发送失败")
# 通过 EventSystem 广播错误Signal Up
EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, {
"error_code": "CHAT_SEND_FAILED",
"message": error_message
})
# 处理接收到的聊天消息
func _handle_chat_render(data: Dictionary) -> void:
# 兼容不同后端字段命名:
# - chat_render: {from, txt, bubble, timestamp}
# - chat: {content, scope, (可选 from/username/timestamp)}
var from_user: String = data.get("from", data.get("from_user", data.get("username", "")))
var content: String = data.get("txt", data.get("content", ""))
var show_bubble: bool = bool(data.get("bubble", data.get("show_bubble", false)))
var timestamp: float = _parse_chat_timestamp_to_unix(data.get("timestamp", 0.0))
var is_self: bool = (not _current_username.is_empty() and from_user == _current_username)
if is_self and _consume_pending_self_message(content):
# 已经本地回显过,避免重复显示
return
# 如果服务端没带发送者信息,但内容匹配最近自己发送的消息,则认为是自己消息
if from_user.is_empty() and _consume_pending_self_message(content):
from_user = _current_username
is_self = true
# 添加到历史
_add_message_to_history({
"from_user": from_user,
"content": content,
"timestamp": timestamp,
"is_self": is_self
})
# 发射信号
chat_message_received.emit(from_user, content, show_bubble, timestamp)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
"from_user": from_user,
"content": content,
"show_bubble": show_bubble,
"timestamp": timestamp,
"is_self": is_self
})
# 解析聊天消息时间戳(兼容 unix 秒 / ISO 8601 字符串)
func _parse_chat_timestamp_to_unix(timestamp_raw: Variant) -> float:
if typeof(timestamp_raw) == TYPE_INT or typeof(timestamp_raw) == TYPE_FLOAT:
var ts := float(timestamp_raw)
return ts if ts > 0.0 else Time.get_unix_time_from_system()
var ts_str := str(timestamp_raw)
if ts_str.strip_edges().is_empty():
return Time.get_unix_time_from_system()
# 纯数字字符串(必须整串都是数字/小数点,避免把 ISO 字符串前缀 "2026" 误判成时间戳)
var numeric_regex := RegEx.new()
numeric_regex.compile("^\\s*-?\\d+(?:\\.\\d+)?\\s*$")
if numeric_regex.search(ts_str) != null:
var ts_num := float(ts_str)
return ts_num if ts_num > 0.0 else Time.get_unix_time_from_system()
# ISO 8601: 2026-01-19T15:15:43.930Z
var regex := RegEx.new()
regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})")
var result := regex.search(ts_str)
if result == null:
return Time.get_unix_time_from_system()
var utc_dict := {
"year": int(result.get_string(1)),
"month": int(result.get_string(2)),
"day": int(result.get_string(3)),
"hour": int(result.get_string(4)),
"minute": int(result.get_string(5)),
"second": int(result.get_string(6))
}
return Time.get_unix_time_from_datetime_dict(utc_dict)
# 处理位置更新成功
func _handle_position_updated(data: Dictionary) -> void:
var stream: String = data.get("stream", "")
var topic: String = data.get("topic", "")
# 发射信号
chat_position_updated.emit(stream, topic)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_POSITION_UPDATED, {
"stream": stream,
"topic": topic
})
# 处理错误响应(如果需要)
func _handle_error_response(data: Dictionary) -> void:
var error_code: String = data.get("code", "")
var error_message: String = data.get("message", "")
_handle_error(error_code, error_message)
# 处理 Socket 错误(如果需要)
func _on_socket_error(error: String) -> void:
_handle_error("SOCKET_ERROR", error)
# ============================================================================
# 内部方法 - 工具函数
# ============================================================================
# 处理错误
func _handle_error(error_code: String, error_message: String) -> void:
push_error("ChatManager: [%s] %s" % [error_code, error_message])
# 获取用户友好的错误消息
var user_message: String = CHAT_ERROR_MESSAGES.get(error_code, error_message) as String
# 发射信号
chat_error_occurred.emit(error_code, user_message)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, {
"error_code": error_code,
"message": user_message
})
# 特殊处理认证失败
if error_code == "AUTH_FAILED" or error_code == "SESSION_EXPIRED":
_is_logged_in = false
EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, {
"error_code": error_code
})
# 记录消息发送时间戳
func _record_message_timestamp() -> void:
var current_time := Time.get_unix_time_from_system()
_message_timestamps.append(current_time)
# 消费一个待去重的“自己消息”(允许相同内容多次发送:每次消费一个)
func _consume_pending_self_message(content: String) -> bool:
var now := Time.get_unix_time_from_system()
# 先清理过期项
for i in range(_pending_self_messages.size() - 1, -1, -1):
var item: Dictionary = _pending_self_messages[i]
if float(item.get("expires_at", 0.0)) < now:
_pending_self_messages.remove_at(i)
# 再匹配内容
for i in range(_pending_self_messages.size() - 1, -1, -1):
if str(_pending_self_messages[i].get("content", "")) == content:
_pending_self_messages.remove_at(i)
return true
return false
# 添加消息到当前会话历史
func _add_message_to_history(message: Dictionary) -> void:
_message_history.append(message)
# 更新最旧消息时间戳(用于历史消息加载)
if _oldest_message_timestamp == 0.0 or message.timestamp < _oldest_message_timestamp:
_oldest_message_timestamp = message.timestamp
# 限制当前会话消息数量(超过后删除最旧的)
if _message_history.size() > MAX_SESSION_MESSAGES:
_message_history.pop_front()

View File

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

View File

@@ -57,7 +57,6 @@ var game_version: String = "1.0.0" # 游戏版本号
# 初始化游戏管理器 # 初始化游戏管理器
# 在节点准备就绪时调用,设置初始状态 # 在节点准备就绪时调用,设置初始状态
func _ready(): func _ready():
print("GameManager 初始化完成")
change_state(GameState.AUTH) # 启动时进入认证状态 change_state(GameState.AUTH) # 启动时进入认证状态
# ============ 状态管理方法 ============ # ============ 状态管理方法 ============
@@ -81,9 +80,6 @@ func change_state(new_state: GameState):
previous_state = current_state previous_state = current_state
current_state = new_state current_state = new_state
# 输出状态变更日志
print("游戏状态变更: ", GameState.keys()[previous_state], " -> ", GameState.keys()[current_state])
# 发送状态变更信号 # 发送状态变更信号
game_state_changed.emit(new_state) game_state_changed.emit(new_state)
@@ -100,7 +96,7 @@ func get_current_state() -> GameState:
# GameState - 上一个游戏状态 # GameState - 上一个游戏状态
# #
# 使用场景: # 使用场景:
# - 从暂停状态恢复时,返回到之前的状态 # - 从暂停状态恢复时,返回到暂停前状态
# - 错误处理时回退到安全状态 # - 错误处理时回退到安全状态
func get_previous_state() -> GameState: func get_previous_state() -> GameState:
return previous_state return previous_state
@@ -114,14 +110,12 @@ func get_previous_state() -> GameState:
# #
# 功能: # 功能:
# - 存储当前登录用户信息 # - 存储当前登录用户信息
# - 输出用户设置日志
# #
# 注意事项: # 注意事项:
# - 用户登录成功后调用此方法 # - 用户登录成功后调用此方法
# - 用户登出时应传入空字符串 # - 用户登出时应传入空字符串
func set_current_user(username: String): func set_current_user(username: String):
current_user = username current_user = username
print("当前用户设置为: ", username)
# 获取当前登录用户 # 获取当前登录用户
# #

View File

@@ -0,0 +1,155 @@
extends Node
# ============================================================================
# LocationManager.gd - 位置同步管理器
# ============================================================================
# 负责与后端 WebSocket 服务进行位置同步和多人会话管理
#
# 协议文档: new_docs/game_architecture_design.md
# 后端地址: wss://whaletownend.xinghangee.icu/location-broadcast
# ============================================================================
signal connected_to_server()
signal connection_closed()
signal connection_error()
signal session_joined(data: Dictionary)
signal user_joined(data: Dictionary)
signal user_left(data: Dictionary)
signal position_updated(data: Dictionary)
const WS_URL = "wss://whaletownend.xinghangee.icu/location-broadcast"
const PING_INTERVAL = 25.0 # 秒
var _socket: WebSocketPeer
var _connected: bool = false
var _ping_timer: float = 0.0
var _auth_token: String = ""
func _ready():
_socket = WebSocketPeer.new()
process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络
func _process(delta):
_socket.poll()
var state = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
if not _connected:
_on_connected()
# 处理接收到的数据包
while _socket.get_available_packet_count() > 0:
var packet = _socket.get_packet()
_handle_packet(packet)
# 心跳处理
_ping_timer += delta
if _ping_timer >= PING_INTERVAL:
_send_heartbeat()
_ping_timer = 0.0
elif state == WebSocketPeer.STATE_CLOSED:
if _connected:
_on_disconnected()
func connect_to_server():
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
return
var err = _socket.connect_to_url(WS_URL)
if err != OK:
push_error("LocationManager: WebSocket 连接请求失败,错误码: %d" % err)
connection_error.emit()
else:
# Godot WebSocket connect is non-blocking, wait for state change in _process
pass
func close_connection():
_socket.close()
func set_auth_token(token: String):
_auth_token = token
# ============ 协议发送 ============
func send_packet(event: String, data: Dictionary):
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var message = {
"event": event,
"data": data
}
var json_str = JSON.stringify(message)
_socket.put_packet(json_str.to_utf8_buffer())
func join_session(map_id: String, initial_pos: Vector2):
var data = {
"sessionId": map_id,
"initialPosition": {
"x": initial_pos.x,
"y": initial_pos.y,
"mapId": map_id
},
"token": _auth_token
}
send_packet("join_session", data)
func leave_session(map_id: String):
send_packet("leave_session", {"sessionId": map_id})
func send_position_update(map_id: String, pos: Vector2, anim_data: Dictionary = {}):
var data = {
"x": pos.x,
"y": pos.y,
"mapId": map_id,
"metadata": anim_data
}
if map_id == "":
push_warning("LocationManager: position_update 的 map_id 为空")
send_packet("position_update", data)
func _send_heartbeat():
send_packet("heartbeat", {"timestamp": Time.get_unix_time_from_system()})
# ============ 事件处理 ============
func _on_connected():
_connected = true
connected_to_server.emit()
func _on_disconnected():
_connected = false
connection_closed.emit()
func _handle_packet(packet: PackedByteArray):
var json_str = packet.get_string_from_utf8()
var json = JSON.new()
var err = json.parse(json_str)
if err != OK:
push_error("LocationManager: JSON 解析失败 - %s" % json.get_error_message())
return
var message = json.data
if not message is Dictionary or not message.has("event"):
return
var event = message["event"]
var data = message.get("data", {})
match event:
"session_joined":
session_joined.emit(data)
"user_joined":
user_joined.emit(data)
"user_left":
user_left.emit(data)
"position_update":
position_updated.emit(data)
"heartbeat_response":
pass # 静默处理
"error":
push_error("LocationManager: WebSocket 错误事件 - %s" % JSON.stringify(data))
_:
push_warning("LocationManager: 未处理的 WebSocket 事件 %s" % event)

View File

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

View File

@@ -109,13 +109,6 @@ class RequestInfo:
var active_requests: Dictionary = {} # 存储所有活动请求 {request_id: RequestInfo} var active_requests: Dictionary = {} # 存储所有活动请求 {request_id: RequestInfo}
var request_counter: int = 0 # 请求计数器用于生成唯一ID var request_counter: int = 0 # 请求计数器用于生成唯一ID
# ============ 生命周期方法 ============
# 初始化网络管理器
# 在节点准备就绪时调用
func _ready():
print("NetworkManager 已初始化")
# ============ 公共API接口 ============ # ============ 公共API接口 ============
# 发送GET请求 # 发送GET请求
@@ -193,14 +186,6 @@ func delete_request(endpoint: String, callback: Callable = Callable(), timeout:
# #
# 回调函数签名: # 回调函数签名:
# func callback(success: bool, data: Dictionary, error_info: Dictionary) # 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: func login(identifier: String, password: String, callback: Callable = Callable()) -> String:
var data = { var data = {
"identifier": identifier, "identifier": identifier,
@@ -472,41 +457,24 @@ func send_request(endpoint: String, method: RequestType, headers: PackedStringAr
var godot_method = _convert_to_godot_method(method) var godot_method = _convert_to_godot_method(method)
var error = http_request.request(full_url, headers, godot_method, body) var error = http_request.request(full_url, headers, godot_method, body)
print("=== 发送网络请求 ===")
print("请求ID: ", request_id)
print("URL: ", full_url)
print("方法: ", RequestType.keys()[method])
print("Headers: ", headers)
print("Body: ", body if body.length() < 200 else body.substr(0, 200) + "...")
print("发送结果: ", error)
if error != OK: if error != OK:
print("请求发送失败,错误码: ", error) push_error("NetworkManager: 请求发送失败,错误码: %d" % error)
_handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络请求发送失败: " + str(error)) _handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络请求发送失败: " + str(error))
return "" return ""
return request_id return request_id
# 请求完成回调 # 请求完成回调
func _on_request_completed(request_id: String, result: int, response_code: int, func _on_request_completed(request_id: String, _result: int, response_code: int,
headers: PackedStringArray, body: PackedByteArray): _headers: PackedStringArray, body: PackedByteArray):
print("=== 网络请求完成 ===")
print("请求ID: ", request_id)
print("结果: ", result)
print("状态码: ", response_code)
print("响应头: ", headers)
# 获取请求信息 # 获取请求信息
if not active_requests.has(request_id): if not active_requests.has(request_id):
print("警告: 未找到请求ID ", request_id) push_warning("NetworkManager: 未找到请求ID %s" % request_id)
return return
var _request_info = active_requests[request_id] var _request_info = active_requests[request_id]
var response_text = body.get_string_from_utf8() var response_text = body.get_string_from_utf8()
print("响应体长度: ", body.size(), " 字节")
print("响应内容: ", response_text if response_text.length() < 500 else response_text.substr(0, 500) + "...")
# 处理网络连接失败 # 处理网络连接失败
if response_code == 0: if response_code == 0:
_handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络连接失败,请检查网络连接") _handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络连接失败,请检查网络连接")
@@ -542,13 +510,11 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
# 特殊情况206测试模式 - 根据API文档这是成功的测试模式响应 # 特殊情况206测试模式 - 根据API文档这是成功的测试模式响应
elif response_code == 206 and error_code == "TEST_MODE_ONLY": elif response_code == 206 and error_code == "TEST_MODE_ONLY":
is_success = true is_success = true
print("🧪 测试模式响应: ", message)
# 201创建成功 # 201创建成功
elif response_code == 201: elif response_code == 201:
is_success = true is_success = true
if is_success: if is_success:
print("✅ 请求成功: ", request_id)
# 发送成功信号 # 发送成功信号
request_completed.emit(request_id, true, data) request_completed.emit(request_id, true, data)
@@ -556,8 +522,6 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
if request_info.callback.is_valid(): if request_info.callback.is_valid():
request_info.callback.call(true, data, {}) request_info.callback.call(true, data, {})
else: else:
print("❌ 请求失败: ", request_id, " - HTTP:", response_code, " 错误码:", error_code, " 消息:", message)
# 确定错误类型 # 确定错误类型
var error_type = _determine_error_type(response_code, error_code) var error_type = _determine_error_type(response_code, error_code)
@@ -579,7 +543,7 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
# 处理请求错误 # 处理请求错误
func _handle_request_error(request_id: String, error_type: ErrorType, message: String): func _handle_request_error(request_id: String, error_type: ErrorType, message: String):
print("❌ 请求错误: ", request_id, " - ", message) push_error("NetworkManager: 请求错误 %s - %s" % [request_id, message])
# 发送错误信号 # 发送错误信号
request_failed.emit(request_id, ErrorType.keys()[error_type], message) request_failed.emit(request_id, ErrorType.keys()[error_type], message)
@@ -639,8 +603,6 @@ func _cleanup_request(request_id: String):
# 从活动请求中移除 # 从活动请求中移除
active_requests.erase(request_id) active_requests.erase(request_id)
print("🧹 清理请求: ", request_id)
# 转换请求方法 # 转换请求方法
func _convert_to_godot_method(method: RequestType) -> HTTPClient.Method: func _convert_to_godot_method(method: RequestType) -> HTTPClient.Method:
match method: match method:
@@ -662,14 +624,12 @@ func _convert_to_godot_method(method: RequestType) -> HTTPClient.Method:
# 取消请求 # 取消请求
func cancel_request(request_id: String) -> bool: func cancel_request(request_id: String) -> bool:
if active_requests.has(request_id): if active_requests.has(request_id):
print("🚫 取消请求: ", request_id)
_cleanup_request(request_id) _cleanup_request(request_id)
return true return true
return false return false
# 取消所有请求 # 取消所有请求
func cancel_all_requests(): func cancel_all_requests():
print("🚫 取消所有请求")
var request_ids = active_requests.keys() var request_ids = active_requests.keys()
for request_id in request_ids: for request_id in request_ids:
cancel_request(request_id) cancel_request(request_id)

View File

@@ -128,21 +128,15 @@ func handle_send_verification_code_response(success: bool, data: Dictionary, err
var error_code = data.get("error_code", "") var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY": if error_code == "TEST_MODE_ONLY":
result.success = true result.success = true
result.message = "🧪 测试模式:验证码已生成,请查看控制台" result.message = "🧪 测试模式:验证码已生成"
result.toast_type = "success" result.toast_type = "success"
# 在控制台显示验证码
if data.has("data") and data.data.has("verification_code"): if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式验证码: ", data.data.verification_code)
result.message += "\n验证码: " + str(data.data.verification_code) result.message += "\n验证码: " + str(data.data.verification_code)
else: else:
result.success = true result.success = true
result.message = "📧 验证码已发送到您的邮箱,请查收" result.message = "📧 验证码已发送到您的邮箱,请查收"
result.toast_type = "success" result.toast_type = "success"
# 开发环境下显示验证码
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境验证码: ", data.data.verification_code)
else: else:
result = _handle_send_code_error(data, error_info) result = _handle_send_code_error(data, error_info)
@@ -156,18 +150,15 @@ func handle_send_login_code_response(success: bool, data: Dictionary, error_info
var error_code = data.get("error_code", "") var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY": if error_code == "TEST_MODE_ONLY":
result.success = true result.success = true
result.message = "测试模式:登录验证码已生成,请查看控制台" result.message = "测试模式:登录验证码已生成"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"): if data.has("data") and data.data.has("verification_code"):
print("测试模式登录验证码: ", data.data.verification_code) result.message += "\n验证码: " + str(data.data.verification_code)
else: else:
result.success = true result.success = true
result.message = "登录验证码已发送,请查收" result.message = "登录验证码已发送,请查收"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("开发环境登录验证码: ", data.data.verification_code)
else: else:
result = _handle_send_login_code_error(data, error_info) result = _handle_send_login_code_error(data, error_info)
@@ -214,18 +205,15 @@ func handle_resend_email_verification_response(success: bool, data: Dictionary,
var error_code = data.get("error_code", "") var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY": if error_code == "TEST_MODE_ONLY":
result.success = true result.success = true
result.message = "🧪 测试模式:验证码已重新生成,请查看控制台" result.message = "🧪 测试模式:验证码已重新生成"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"): if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式重新发送验证码: ", data.data.verification_code) result.message += "\n验证码: " + str(data.data.verification_code)
else: else:
result.success = true result.success = true
result.message = "📧 验证码已重新发送到您的邮箱,请查收" result.message = "📧 验证码已重新发送到您的邮箱,请查收"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境重新发送验证码: ", data.data.verification_code)
else: else:
result = _handle_resend_email_verification_error(data, error_info) result = _handle_resend_email_verification_error(data, error_info)
@@ -239,18 +227,15 @@ func handle_forgot_password_response(success: bool, data: Dictionary, error_info
var error_code = data.get("error_code", "") var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY": if error_code == "TEST_MODE_ONLY":
result.success = true result.success = true
result.message = "🧪 测试模式:重置验证码已生成,请查看控制台" result.message = "🧪 测试模式:重置验证码已生成"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"): if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式重置验证码: ", data.data.verification_code) result.message += "\n验证码: " + str(data.data.verification_code)
else: else:
result.success = true result.success = true
result.message = "📧 重置验证码已发送,请查收" result.message = "📧 重置验证码已发送,请查收"
result.toast_type = "success" result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境重置验证码: ", data.data.verification_code)
else: else:
result = _handle_forgot_password_error(data, error_info) result = _handle_forgot_password_error(data, error_info)

View File

@@ -36,6 +36,8 @@ signal scene_change_started(scene_name: String)
# 场景状态 # 场景状态
var current_scene_name: String = "" # 当前场景名称 var current_scene_name: String = "" # 当前场景名称
var is_changing_scene: bool = false # 是否正在切换场景 var is_changing_scene: bool = false # 是否正在切换场景
var _next_scene_position: Variant = null # 下一个场景的初始位置 (Vector2 or null)
var _next_spawn_name: String = "" # 下一个场景的出生点名称 (String)
# 场景路径映射表 # 场景路径映射表
# 将场景名称映射到实际的文件路径 # 将场景名称映射到实际的文件路径
@@ -47,16 +49,14 @@ var scene_paths: Dictionary = {
"battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统 "battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统
"inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面 "inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面
"shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面 "shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面
"settings": "res://scenes/ui/SettingsWindow.tscn" # 设置界面 "settings": "res://scenes/ui/SettingsWindow.tscn", # 设置界面
"square": "res://scenes/Maps/square.tscn", # 广场地图
"room": "res://scenes/Maps/room.tscn", # 房间地图
"fountain": "res://scenes/Maps/fountain.tscn", # 喷泉地图
"datawhale_home": "res://scenes/Maps/datawhale_home.tscn", # 数据鲸鱼之家
"community": "res://scenes/Maps/community.tscn" # 社区地图
} }
# ============ 生命周期方法 ============
# 初始化场景管理器
# 在节点准备就绪时调用
func _ready():
print("SceneManager 初始化完成")
# ============ 场景切换方法 ============ # ============ 场景切换方法 ============
# 切换到指定场景 # 切换到指定场景
@@ -77,8 +77,6 @@ func _ready():
# #
# 使用示例: # 使用示例:
# var success = SceneManager.change_scene("main", true) # var success = SceneManager.change_scene("main", true)
# if success:
# print("场景切换成功")
# #
# 注意事项: # 注意事项:
# - 场景切换是异步操作 # - 场景切换是异步操作
@@ -87,16 +85,15 @@ func _ready():
func change_scene(scene_name: String, use_transition: bool = true): func change_scene(scene_name: String, use_transition: bool = true):
# 防止重复切换 # 防止重复切换
if is_changing_scene: if is_changing_scene:
print("场景切换中,忽略新的切换请求") push_warning("SceneManager: 场景切换中,忽略新的切换请求")
return false return false
# 检查场景是否存在 # 检查场景是否存在
if not scene_paths.has(scene_name): if not scene_paths.has(scene_name):
print("错误: 未找到场景 ", scene_name) push_error("SceneManager: 未找到场景 %s" % scene_name)
return false return false
var scene_path = scene_paths[scene_name] var scene_path = scene_paths[scene_name]
print("开始切换场景: ", current_scene_name, " -> ", scene_name)
# 设置切换状态 # 设置切换状态
is_changing_scene = true is_changing_scene = true
@@ -109,7 +106,7 @@ func change_scene(scene_name: String, use_transition: bool = true):
# 执行场景切换 # 执行场景切换
var error = get_tree().change_scene_to_file(scene_path) var error = get_tree().change_scene_to_file(scene_path)
if error != OK: if error != OK:
print("场景切换失败: ", error) push_error("SceneManager: 场景切换失败 %s -> %s, 错误码: %d" % [current_scene_name, scene_name, error])
is_changing_scene = false is_changing_scene = false
return false return false
@@ -122,7 +119,6 @@ func change_scene(scene_name: String, use_transition: bool = true):
if use_transition: if use_transition:
await hide_transition() await hide_transition()
print("场景切换完成: ", scene_name)
return true return true
# ============ 查询方法 ============ # ============ 查询方法 ============
@@ -134,6 +130,72 @@ func change_scene(scene_name: String, use_transition: bool = true):
func get_current_scene_name() -> String: func get_current_scene_name() -> String:
return current_scene_name return current_scene_name
# ============ 场景位置和出生点管理 ============
# 设置下一个场景的初始位置
#
# 参数:
# pos: Vector2 - 玩家在下一个场景的初始位置
#
# 功能:
# - 用于场景切换时传递玩家位置信息
# - 配合 DoorTeleport 等传送机制使用
#
# 使用示例:
# SceneManager.set_next_scene_position(Vector2(100, 200))
# SceneManager.change_scene("room")
func set_next_scene_position(pos: Vector2) -> void:
_next_scene_position = pos
# 获取并清除下一个场景的初始位置
#
# 返回值:
# Variant - Vector2 位置或 null如果未设置
#
# 功能:
# - 获取预设的场景初始位置
# - 获取后自动清除,避免影响后续场景切换
#
# 注意事项:
# - 此方法会清除存储的位置,只能获取一次
# - 如果未设置位置,返回 null
func get_next_scene_position() -> Variant:
var pos = _next_scene_position
_next_scene_position = null
return pos
# 设置下一个场景的出生点名称
#
# 参数:
# spawn_name: String - 出生点节点的名称
#
# 功能:
# - 指定玩家在下一个场景应该出现在哪个出生点
# - 配合场景中的 Marker2D 出生点使用
#
# 使用示例:
# SceneManager.set_next_spawn_name("DoorExit")
# SceneManager.change_scene("square")
func set_next_spawn_name(spawn_name: String) -> void:
_next_spawn_name = spawn_name
# 获取并清除下一个场景的出生点名称
#
# 返回值:
# String - 出生点名称(如果未设置则返回空字符串)
#
# 功能:
# - 获取预设的出生点名称
# - 获取后自动清除,避免影响后续场景切换
#
# 注意事项:
# - 此方法会清除存储的名称,只能获取一次
# - 如果未设置名称,返回空字符串
func get_next_spawn_name() -> String:
var name = _next_spawn_name
_next_spawn_name = ""
return name
# ============ 场景注册方法 ============ # ============ 场景注册方法 ============
# 注册新场景 # 注册新场景
@@ -150,7 +212,6 @@ func get_current_scene_name() -> String:
# SceneManager.register_scene("boss_battle", "res://scenes/boss/boss_battle.tscn") # SceneManager.register_scene("boss_battle", "res://scenes/boss/boss_battle.tscn")
func register_scene(scene_name: String, scene_path: String): func register_scene(scene_name: String, scene_path: String):
scene_paths[scene_name] = scene_path scene_paths[scene_name] = scene_path
print("注册场景: ", scene_name, " -> ", scene_path)
# ============ 过渡效果方法 ============ # ============ 过渡效果方法 ============
@@ -166,8 +227,7 @@ func register_scene(scene_name: String, scene_path: String):
# #
# TODO: 实现淡入淡出、滑动等过渡效果 # TODO: 实现淡入淡出、滑动等过渡效果
func show_transition(): func show_transition():
# TODO: 实现场景切换过渡效果 # TODO: 实现场景切换过渡效果(当前仅占位延时)
print("显示场景切换过渡效果")
await get_tree().create_timer(0.2).timeout await get_tree().create_timer(0.2).timeout
# 隐藏场景切换过渡效果 # 隐藏场景切换过渡效果
@@ -182,6 +242,5 @@ func show_transition():
# #
# TODO: 实现与show_transition()对应的隐藏效果 # TODO: 实现与show_transition()对应的隐藏效果
func hide_transition(): func hide_transition():
# TODO: 隐藏场景切换过渡效果 # TODO: 隐藏场景切换过渡效果(当前仅占位延时)
print("隐藏场景切换过渡效果")
await get_tree().create_timer(0.2).timeout await get_tree().create_timer(0.2).timeout

View File

@@ -40,7 +40,6 @@ var toast_counter: int = 0 # Toast计数器用于生成
# container: Control - Toast消息的容器节点 # container: Control - Toast消息的容器节点
func setup(container: Control): func setup(container: Control):
toast_container = container toast_container = container
print("ToastManager 初始化完成")
# ============ 公共方法 ============ # ============ 公共方法 ============
@@ -51,10 +50,9 @@ func setup(container: Control):
# is_success: bool - 是否为成功消息(影响颜色) # is_success: bool - 是否为成功消息(影响颜色)
func show_toast(message: String, is_success: bool = true): func show_toast(message: String, is_success: bool = true):
if toast_container == null: if toast_container == null:
print("错误: toast_container 节点不存在") push_error("ToastManager: toast_container 节点不存在")
return return
print("显示Toast消息: ", message, " 成功: ", is_success)
_create_toast_instance(message, is_success) _create_toast_instance(message, is_success)
# 清理所有Toast # 清理所有Toast
@@ -123,21 +121,17 @@ func _create_toast_instance(message: String, is_success: bool):
# 平台特定的字体处理 # 平台特定的字体处理
if is_web: if is_web:
print("Web平台Toast字体处理")
# Web平台使用主题文件 # Web平台使用主题文件
var chinese_theme = load("res://assets/ui/chinese_theme.tres") var chinese_theme = load("res://assets/ui/chinese_theme.tres")
if chinese_theme: if chinese_theme:
text_label.theme = chinese_theme text_label.theme = chinese_theme
print("Web平台应用中文主题")
else: else:
print("Web平台中文主题加载失败") push_warning("ToastManager: Web平台中文主题加载失败,使用默认字体")
else: else:
print("桌面平台Toast字体处理")
# 桌面平台直接加载中文字体 # 桌面平台直接加载中文字体
var desktop_chinese_font = load("res://assets/fonts/msyh.ttc") var desktop_chinese_font = load("res://assets/fonts/msyh.ttc")
if desktop_chinese_font: if desktop_chinese_font:
text_label.add_theme_font_override("font", desktop_chinese_font) text_label.add_theme_font_override("font", desktop_chinese_font)
print("桌面平台使用中文字体")
text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
text_label.custom_minimum_size = Vector2(280, 0) text_label.custom_minimum_size = Vector2(280, 0)

View File

@@ -0,0 +1,429 @@
extends Node
# ============================================================================
# WebSocketManager.gd - WebSocket 连接生命周期管理(原生 WebSocket 版本)
# ============================================================================
# 管理 WebSocket 连接状态、自动重连和错误恢复
#
# 核心职责:
# - 连接状态管理(断开、连接中、已连接、重连中)
# - 自动重连机制(指数退避)
# - 连接错误恢复
# - WebSocket 消息发送/接收
# ============================================================================
# 使用方式:
# WebSocketManager.connect_to_game_server()
# WebSocketManager.connection_state_changed.connect(_on_state_changed)
#
# 注意事项:
# - 作为自动加载单例,全局可访问
# - 自动处理连接断开和重连
# - 通过信号通知连接状态变化
# ============================================================================
class_name ChatWebSocketManager
# ============================================================================
# 信号定义
# ============================================================================
# 连接状态变化信号
# 参数:
# new_state: ConnectionState - 新的连接状态
signal connection_state_changed(new_state: ConnectionState)
# 连接丢失信号
signal connection_lost()
# 重连成功信号
signal reconnection_succeeded()
# 重连失败信号
# 参数:
# attempt: int - 当前重连尝试次数
# max_attempts: int - 最大重连次数
signal reconnection_failed(attempt: int, max_attempts: int)
# WebSocket 消息接收信号
# 参数:
# message: String - 接收到的消息内容JSON 字符串)
signal data_received(message: String)
# ============================================================================
# 枚举定义
# ============================================================================
# 连接状态枚举
enum ConnectionState {
DISCONNECTED, # 未连接
CONNECTING, # 连接中
CONNECTED, # 已连接
RECONNECTING, # 重连中
ERROR # 错误状态
}
# ============================================================================
# 常量定义
# ============================================================================
# WebSocket 服务器 URL原生 WebSocket
const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game"
# 默认最大重连次数
const DEFAULT_MAX_RECONNECT_ATTEMPTS: int = 5
# 默认重连基础延迟(秒)
const DEFAULT_RECONNECT_BASE_DELAY: float = 3.0
# 最大重连延迟(秒)
const MAX_RECONNECT_DELAY: float = 30.0
# ============================================================================
# 成员变量
# ============================================================================
# WebSocket peer
var _websocket_peer: WebSocketPeer = WebSocketPeer.new()
# 当前连接状态
var _connection_state: ConnectionState = ConnectionState.DISCONNECTED
# 自动重连启用标志
var _auto_reconnect_enabled: bool = true
# 最大重连次数
var _max_reconnect_attempts: int = DEFAULT_MAX_RECONNECT_ATTEMPTS
# 重连基础延迟
var _reconnect_base_delay: float = DEFAULT_RECONNECT_BASE_DELAY
# 当前重连尝试次数
var _reconnect_attempt: int = 0
# 重连定时器
var _reconnect_timer: Timer = Timer.new()
# 是否为正常关闭(非异常断开)
var _clean_close: bool = true
# 心跳定时器
var _heartbeat_timer: Timer = Timer.new()
# 心跳间隔(秒)
const HEARTBEAT_INTERVAL: float = 30.0
# ============================================================================
# 生命周期方法
# ============================================================================
# 初始化
func _ready() -> void:
# 设置重连定时器
_setup_reconnect_timer()
# 设置心跳定时器
_setup_heartbeat_timer()
# 启动处理循环
set_process(true)
# 处理每帧
func _process(_delta: float) -> void:
# 检查 WebSocket 状态变化
_check_websocket_state()
var state: WebSocketPeer.State = _websocket_peer.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
# 接收数据
_websocket_peer.poll()
# 处理收到的数据
while _websocket_peer.get_available_packet_count() > 0:
var packet: PackedByteArray = _websocket_peer.get_packet()
var message: String = packet.get_string_from_utf8()
# 发射消息接收信号
data_received.emit(message)
# 清理
func _exit_tree() -> void:
_disconnect()
if is_instance_valid(_reconnect_timer):
_reconnect_timer.stop()
_reconnect_timer.queue_free()
if is_instance_valid(_heartbeat_timer):
_heartbeat_timer.stop()
_heartbeat_timer.queue_free()
# ============================================================================
# 公共 API - 连接管理
# ============================================================================
# 连接到游戏服务器
func connect_to_game_server() -> void:
if _connection_state == ConnectionState.CONNECTED or _connection_state == ConnectionState.CONNECTING:
push_warning("已经在连接或已连接状态")
return
_set_connection_state(ConnectionState.CONNECTING)
_clean_close = true
_reconnect_attempt = 0
var err: Error = _websocket_peer.connect_to_url(WEBSOCKET_URL)
if err != OK:
push_error("WebSocketManager: 连接失败 - %s" % error_string(err))
_set_connection_state(ConnectionState.ERROR)
return
# 启动心跳
_start_heartbeat()
# 断开 WebSocket 连接
func disconnect_websocket() -> void:
_disconnect()
# 断开连接(内部方法)
func _disconnect() -> void:
_clean_close = true
# 停止重连定时器
_reconnect_timer.stop()
# 停止心跳
_heartbeat_timer.stop()
# 关闭 WebSocket
if _websocket_peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
_websocket_peer.close()
_set_connection_state(ConnectionState.DISCONNECTED)
# 检查 WebSocket 是否已连接
#
# 返回值:
# bool - WebSocket 是否已连接
func is_websocket_connected() -> bool:
return _connection_state == ConnectionState.CONNECTED
# 获取当前连接状态
#
# 返回值:
# ConnectionState - 当前连接状态
func get_connection_state() -> ConnectionState:
return _connection_state
# ============================================================================
# 公共 API - 消息发送
# ============================================================================
# 发送 WebSocket 消息
#
# 参数:
# message: String - 要发送的消息内容JSON 字符串)
#
# 返回值:
# Error - 错误码OK 表示成功
func send_message(message: String) -> Error:
if _websocket_peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
push_warning("WebSocketManager: 未连接,无法发送消息")
return ERR_UNCONFIGURED
var err: Error = _websocket_peer.send_text(message)
if err != OK:
push_error("WebSocketManager: 发送消息失败 - %s" % error_string(err))
return err
return OK
# ============================================================================
# 公共 API - 自动重连
# ============================================================================
# 启用/禁用自动重连
#
# 参数:
# enabled: bool - 是否启用自动重连
# max_attempts: int - 最大重连次数(默认 5
# base_delay: float - 基础重连延迟,秒(默认 3.0
#
# 使用示例:
# WebSocketManager.enable_auto_reconnect(true, 5, 3.0)
func enable_auto_reconnect(enabled: bool, max_attempts: int = DEFAULT_MAX_RECONNECT_ATTEMPTS, base_delay: float = DEFAULT_RECONNECT_BASE_DELAY) -> void:
_auto_reconnect_enabled = enabled
_max_reconnect_attempts = max_attempts
_reconnect_base_delay = base_delay
# 获取重连信息
#
# 返回值:
# Dictionary - 重连信息 {enabled, attempt, max_attempts, delay}
func get_reconnect_info() -> Dictionary:
return {
"enabled": _auto_reconnect_enabled,
"attempt": _reconnect_attempt,
"max_attempts": _max_reconnect_attempts,
"next_delay": _calculate_reconnect_delay() if _connection_state == ConnectionState.RECONNECTING else 0.0
}
# ============================================================================
# 内部方法 - 连接状态管理
# ============================================================================
# 设置连接状态
func _set_connection_state(new_state: ConnectionState) -> void:
if _connection_state == new_state:
return
_connection_state = new_state
# 发射信号
connection_state_changed.emit(new_state)
# ============================================================================
# 内部方法 - WebSocket 状态监控
# ============================================================================
# 检查 WebSocket 状态变化
func _check_websocket_state() -> void:
# 必须先 poll 才能获取最新状态
_websocket_peer.poll()
var state: WebSocketPeer.State = _websocket_peer.get_ready_state()
match state:
WebSocketPeer.STATE_CONNECTING:
# 正在连接
if _connection_state != ConnectionState.CONNECTING and _connection_state != ConnectionState.RECONNECTING:
_set_connection_state(ConnectionState.CONNECTING)
WebSocketPeer.STATE_OPEN:
# 连接成功
if _connection_state != ConnectionState.CONNECTED:
_on_websocket_connected()
WebSocketPeer.STATE_CLOSING:
# 正在关闭
pass
WebSocketPeer.STATE_CLOSED:
# 连接关闭
var code: int = _websocket_peer.get_close_code()
_on_websocket_closed(code != 0) # code=0 表示正常关闭
# WebSocket 连接成功处理
func _on_websocket_connected() -> void:
# 如果是重连,发射重连成功信号
if _connection_state == ConnectionState.RECONNECTING:
_reconnect_attempt = 0
reconnection_succeeded.emit()
_set_connection_state(ConnectionState.CONNECTED)
# WebSocket 连接关闭处理
func _on_websocket_closed(clean_close: bool) -> void:
_clean_close = clean_close
# 如果是异常断开且启用了自动重连
if not clean_close and _auto_reconnect_enabled:
connection_lost.emit()
_attempt_reconnect()
else:
_set_connection_state(ConnectionState.DISCONNECTED)
# ============================================================================
# 内部方法 - 重连机制
# ============================================================================
# 设置重连定时器
func _setup_reconnect_timer() -> void:
_reconnect_timer = Timer.new()
_reconnect_timer.one_shot = true
_reconnect_timer.autostart = false
add_child(_reconnect_timer)
_reconnect_timer.timeout.connect(_on_reconnect_timeout)
# 尝试重连
func _attempt_reconnect() -> void:
# 检查是否超过最大重连次数
if _reconnect_attempt >= _max_reconnect_attempts:
push_error("WebSocketManager: 达到最大重连次数 (%d),停止重连" % _max_reconnect_attempts)
reconnection_failed.emit(_reconnect_attempt, _max_reconnect_attempts)
_set_connection_state(ConnectionState.ERROR)
return
_reconnect_attempt += 1
_set_connection_state(ConnectionState.RECONNECTING)
# 计算重连延迟(指数退避)
var delay: float = _calculate_reconnect_delay()
# 启动重连定时器
_reconnect_timer.start(delay)
# 计算重连延迟(指数退避)
func _calculate_reconnect_delay() -> float:
# 指数退避: base_delay * 2^(attempt-1)
var delay: float = _reconnect_base_delay * pow(2.0, _reconnect_attempt - 1)
# 限制最大延迟
return min(delay, MAX_RECONNECT_DELAY)
# 重连定时器超时处理
func _on_reconnect_timeout() -> void:
_clean_close = false
connect_to_game_server()
# ============================================================================
# 内部方法 - 心跳机制
# ============================================================================
# 设置心跳定时器
func _setup_heartbeat_timer() -> void:
_heartbeat_timer = Timer.new()
_heartbeat_timer.wait_time = HEARTBEAT_INTERVAL
_heartbeat_timer.one_shot = false
_heartbeat_timer.autostart = false
add_child(_heartbeat_timer)
_heartbeat_timer.timeout.connect(_on_heartbeat)
# 启动心跳
func _start_heartbeat() -> void:
_heartbeat_timer.start()
# 停止心跳
func _stop_heartbeat() -> void:
_heartbeat_timer.stop()
# 心跳超时处理
func _on_heartbeat() -> void:
# 不发送心跳,避免服务器返回 "消息格式错误"
# 如果需要心跳,服务器应该支持特定格式
pass
# ============================================================================
# 工具方法
# ============================================================================
# 获取连接状态描述
#
# 返回值:
# String - 连接状态描述
func get_state_description() -> String:
match _connection_state:
ConnectionState.DISCONNECTED:
return "未连接"
ConnectionState.CONNECTING:
return "连接中"
ConnectionState.CONNECTED:
return "已连接"
ConnectionState.RECONNECTING:
return "重连中 (%d/%d)" % [_reconnect_attempt, _max_reconnect_attempts]
ConnectionState.ERROR:
return "错误"
_:
return "未知状态"

View File

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

View File

@@ -27,13 +27,6 @@ extends Node
# 结构: {event_name: [{"callback": Callable, "target": Node}, ...]} # 结构: {event_name: [{"callback": Callable, "target": Node}, ...]}
var event_listeners: Dictionary = {} var event_listeners: Dictionary = {}
# ============ 生命周期方法 ============
# 初始化事件系统
# 在节点准备就绪时调用
func _ready():
print("EventSystem 初始化完成")
# ============ 事件监听器管理 ============ # ============ 事件监听器管理 ============
# 注册事件监听器 # 注册事件监听器
@@ -59,6 +52,11 @@ func connect_event(event_name: String, callback: Callable, target: Node = null):
if not event_listeners.has(event_name): if not event_listeners.has(event_name):
event_listeners[event_name] = [] event_listeners[event_name] = []
# 避免重复注册同一个监听器
for listener in event_listeners[event_name]:
if listener.callback == callback and listener.target == target:
return
# 创建监听器信息 # 创建监听器信息
var listener_info = { var listener_info = {
"callback": callback, "callback": callback,
@@ -67,7 +65,6 @@ func connect_event(event_name: String, callback: Callable, target: Node = null):
# 添加到监听器列表 # 添加到监听器列表
event_listeners[event_name].append(listener_info) event_listeners[event_name].append(listener_info)
print("注册事件监听器: ", event_name, " -> ", callback)
# 移除事件监听器 # 移除事件监听器
# #
@@ -93,7 +90,6 @@ func disconnect_event(event_name: String, callback: Callable, target: Node = nul
# 匹配callback和target # 匹配callback和target
if listener.callback == callback and listener.target == target: if listener.callback == callback and listener.target == target:
listeners.remove_at(i) listeners.remove_at(i)
print("移除事件监听器: ", event_name, " -> ", callback)
break break
# ============ 事件发送 ============ # ============ 事件发送 ============
@@ -117,13 +113,11 @@ func disconnect_event(event_name: String, callback: Callable, target: Node = nul
# - 事件发送是同步的,所有监听器会立即执行 # - 事件发送是同步的,所有监听器会立即执行
# - 如果监听器执行出错,不会影响其他监听器 # - 如果监听器执行出错,不会影响其他监听器
func emit_event(event_name: String, data: Variant = null): func emit_event(event_name: String, data: Variant = null):
print("发送事件: ", event_name, " 数据: ", data)
# 检查是否有监听器 # 检查是否有监听器
if not event_listeners.has(event_name): if not event_listeners.has(event_name):
return return
var listeners = event_listeners[event_name] var listeners = event_listeners[event_name].duplicate()
for listener_info in listeners: for listener_info in listeners:
var target = listener_info.target var target = listener_info.target
var callback = listener_info.callback var callback = listener_info.callback
@@ -161,7 +155,6 @@ func cleanup_invalid_listeners():
# 如果目标节点无效,移除监听器 # 如果目标节点无效,移除监听器
if target != null and not is_instance_valid(target): if target != null and not is_instance_valid(target):
listeners.remove_at(i) listeners.remove_at(i)
print("清理无效监听器: ", event_name)
# ============ 查询方法 ============ # ============ 查询方法 ============
@@ -192,4 +185,3 @@ func get_listener_count(event_name: String) -> int:
# - 使用前请确保所有模块都能正确处理监听器丢失 # - 使用前请确保所有模块都能正确处理监听器丢失
func clear_all_listeners(): func clear_all_listeners():
event_listeners.clear() event_listeners.clear()
print("清空所有事件监听器")

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

@@ -0,0 +1,144 @@
# ============================================================================
# 网格系统 - 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) -> Dictionary:
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)
return {
"world_position": world_pos,
"grid_position": grid_pos,
"snapped_position": snapped_pos,
"center_position": center_pos
}

View File

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

View File

@@ -39,7 +39,7 @@ class_name StringUtils
# #
# 使用示例: # 使用示例:
# if StringUtils.is_valid_email("user@example.com"): # if StringUtils.is_valid_email("user@example.com"):
# print("邮箱格式正确") # # 邮箱格式正确
static func is_valid_email(email: String) -> bool: static func is_valid_email(email: String) -> bool:
var regex = RegEx.new() var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
@@ -60,7 +60,7 @@ static func is_valid_email(email: String) -> bool:
# #
# 使用示例: # 使用示例:
# if StringUtils.is_valid_username("user_123"): # if StringUtils.is_valid_username("user_123"):
# print("用户名格式正确") # # 用户名格式正确
static func is_valid_username(username: String) -> bool: static func is_valid_username(username: String) -> bool:
# 检查长度 # 检查长度
if username.is_empty() or username.length() > 50: if username.is_empty() or username.length() > 50:
@@ -92,7 +92,7 @@ static func is_valid_username(username: String) -> bool:
# 使用示例: # 使用示例:
# var result = StringUtils.validate_password_strength("MyPass123!") # var result = StringUtils.validate_password_strength("MyPass123!")
# if result.valid: # if result.valid:
# print("密码强度: ", result.message) # # 可按需展示 result.message
static func validate_password_strength(password: String) -> Dictionary: static func validate_password_strength(password: String) -> Dictionary:
var result = {"valid": false, "message": "", "strength": 0} var result = {"valid": false, "message": "", "strength": 0}

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c5ujr1fj14n58"
path="res://.godot/imported/Spawn Fixed.mp3-cdebcc11ab16736c9b4a0906adccb431.mp3str"
[deps]
source_file="res://assets/audio/music/Spawn Fixed.mp3"
dest_files=["res://.godot/imported/Spawn Fixed.mp3-cdebcc11ab16736c9b4a0906adccb431.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://brko2ik6t6ib5"
path="res://.godot/imported/npc_286_241.png-dfe6daef11d0f27f7902e69d6057828f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/characters/npc_286_241.png"
dest_files=["res://.godot/imported/npc_286_241.png-dfe6daef11d0f27f7902e69d6057828f.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: 43 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://krfed1r4qmnp"
path="res://.godot/imported/payer_44_30.png-100395b4756c93dec9ce9baa5c5df0f3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/characters/payer_44_30.png"
dest_files=["res://.godot/imported/payer_44_30.png-100395b4756c93dec9ce9baa5c5df0f3.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: 43 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cghab1hkx5lg5"
path="res://.godot/imported/player_spritesheet.png-7e76f97a17946ec512358d45f2527da8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/characters/player_spritesheet.png"
dest_files=["res://.godot/imported/player_spritesheet.png-7e76f97a17946ec512358d45f2527da8.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: 856 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyimi462hj6r2"
path="res://.godot/imported/player_spritesheet_backup.png-6d8c966a56b0ced39cf1ecf0c772847f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/characters/player_spritesheet_backup.png"
dest_files=["res://.godot/imported/player_spritesheet_backup.png-6d8c966a56b0ced39cf1ecf0c772847f.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://dw7lapxn4m8ry"
path="res://.godot/imported/NoticeBoard.png-038eefee12f116fb9502ed755594cede.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/materials/NoticeBoard.png"
dest_files=["res://.godot/imported/NoticeBoard.png-038eefee12f116fb9502ed755594cede.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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c5y8qxm3jn2wv"
path="res://.godot/imported/WelcomeBoard.png-bcff7f9bf968cb5d7630e2ad47f2fb42.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/materials/WelcomeBoard.png"
dest_files=["res://.godot/imported/WelcomeBoard.png-bcff7f9bf968cb5d7630e2ad47f2fb42.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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c7jx40leuy6q1"
path="res://.godot/imported/board.png-4f7a101e7a1b1cbdc8a75666c78e8907.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/board.png"
dest_files=["res://.godot/imported/board.png-4f7a101e7a1b1cbdc8a75666c78e8907.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.6 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://7j3n0nhg8atb"
path="res://.godot/imported/community.png-a8c4bd53b7eaad8a751801ba0eb4ea69.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/community.png"
dest_files=["res://.godot/imported/community.png-a8c4bd53b7eaad8a751801ba0eb4ea69.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: 458 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cle66our01dq1"
path="res://.godot/imported/community_512_512.png-fa162180b6884ce89074ec9b8b445a11.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/community_512_512.png"
dest_files=["res://.godot/imported/community_512_512.png-fa162180b6884ce89074ec9b8b445a11.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: 28 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bxk7qx15ks23n"
path="res://.godot/imported/deck_256_111.png-1f5516606f281e4ce47eda14a0f195f6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_256_111.png"
dest_files=["res://.godot/imported/deck_256_111.png-1f5516606f281e4ce47eda14a0f195f6.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: 29 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cuoondqo7wpvm"
path="res://.godot/imported/deck_256_93.png-53cc7596920e943ad680f551cecde9c5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_256_93.png"
dest_files=["res://.godot/imported/deck_256_93.png-53cc7596920e943ad680f551cecde9c5.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.5 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dth4pwye1huv1"
path="res://.godot/imported/deck_2784_1536.png-7209e3c01bcb29b96850c5bfe2118eb5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_2784_1536.png"
dest_files=["res://.godot/imported/deck_2784_1536.png-7209e3c01bcb29b96850c5bfe2118eb5.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: 57 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3w3fncsm32oi"
path="res://.godot/imported/deck_384_167.png-f28a21a5574e4d3fa7c0565d6c292757.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_384_167.png"
dest_files=["res://.godot/imported/deck_384_167.png-f28a21a5574e4d3fa7c0565d6c292757.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: 83 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://j0twhfkpj15i"
path="res://.godot/imported/deck_512_164.png-c98703495d73d104671911f6dada9aca.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_512_164.png"
dest_files=["res://.godot/imported/deck_512_164.png-c98703495d73d104671911f6dada9aca.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: 37 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blre1srim52hs"
path="res://.godot/imported/deck_512_282.png-ec2cdd543ebb499e3623cd89dff86aa4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_512_282.png"
dest_files=["res://.godot/imported/deck_512_282.png-ec2cdd543ebb499e3623cd89dff86aa4.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: 570 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drdfggxi5ecw7"
path="res://.godot/imported/deck_512_512.png-14cca0360b4e43a84ff23febdf688dce.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/deck_512_512.png"
dest_files=["res://.godot/imported/deck_512_512.png-14cca0360b4e43a84ff23febdf688dce.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: 27 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ci5myym3wxvej"
path="res://.godot/imported/e2f5aff78fae12f979d3456eca0896b4.jpg-09ffb25e15f901ba90adc883741736d3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/e2f5aff78fae12f979d3456eca0896b4.jpg"
dest_files=["res://.godot/imported/e2f5aff78fae12f979d3456eca0896b4.jpg-09ffb25e15f901ba90adc883741736d3.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: 78 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dujutnr03apoj"
path="res://.godot/imported/fountain_256_192.png-6fb4e69b74642a426b29631ef7156f53.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/fountain_256_192.png"
dest_files=["res://.godot/imported/fountain_256_192.png-6fb4e69b74642a426b29631ef7156f53.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: 9.4 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://h1kqvkvshfxo"
path="res://.godot/imported/grass_128_128.png-f99428f7721484fc94b70ea8f73a140d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/grass_128_128.png"
dest_files=["res://.godot/imported/grass_128_128.png-f99428f7721484fc94b70ea8f73a140d.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: 28 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwlnclqw6lsa7"
path="res://.godot/imported/grass_256_256.png-480baf97b2b792db085b32018ac813a5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/grass_256_256.png"
dest_files=["res://.godot/imported/grass_256_256.png-480baf97b2b792db085b32018ac813a5.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.7 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ccqxsarxnnf4e"
path="res://.godot/imported/ground.png-2205e043de9d3b8a38f01788dff375d3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/ground.png"
dest_files=["res://.godot/imported/ground.png-2205e043de9d3b8a38f01788dff375d3.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: 71 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dtev6yddbjtvp"
path="res://.godot/imported/house_256_192.png-939b43001a6826dd2fb03b0e500d6b91.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/house_256_192.png"
dest_files=["res://.godot/imported/house_256_192.png-939b43001a6826dd2fb03b0e500d6b91.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: 125 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bxmbnywn7pd35"
path="res://.godot/imported/house_384_256.png-9c21ec19000d397cf04e3d4c46d9fd67.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/house_384_256.png"
dest_files=["res://.godot/imported/house_384_256.png-9c21ec19000d397cf04e3d4c46d9fd67.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: 145 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drl6vecqinsgw"
path="res://.godot/imported/house_384_288.png-2c4bb2980ec70d9c801e9d8f5c9da5e2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/house_384_288.png"
dest_files=["res://.godot/imported/house_384_288.png-2c4bb2980ec70d9c801e9d8f5c9da5e2.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: 9.4 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c7v86fkrcb4go"
path="res://.godot/imported/river.png-5a4acbf78dd4e08f27a3192f37afc708.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/river.png"
dest_files=["res://.godot/imported/river.png-5a4acbf78dd4e08f27a3192f37afc708.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: 44 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://df3klfat72qro"
path="res://.godot/imported/river2_256_256.png-4b34411e3a9844aea328c2b5fdd9f6d7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/river2_256_256.png"
dest_files=["res://.godot/imported/river2_256_256.png-4b34411e3a9844aea328c2b5fdd9f6d7.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: 207 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://5r75q24ww18f"
path="res://.godot/imported/river2_512_512.png-af87f8da62bcadc69167d888dc2932d0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/river2_512_512.png"
dest_files=["res://.godot/imported/river2_512_512.png-af87f8da62bcadc69167d888dc2932d0.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: 28 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://devfvbybifga6"
path="res://.godot/imported/river_256_256.png-9a57f752ac6003da70cffda69b91371a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/river_256_256.png"
dest_files=["res://.godot/imported/river_256_256.png-9a57f752ac6003da70cffda69b91371a.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: 76 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b4wt8paqrevg2"
path="res://.godot/imported/river_512_512.png-218f1dc6fae57e80762824a68b0c08f6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/river_512_512.png"
dest_files=["res://.godot/imported/river_512_512.png-218f1dc6fae57e80762824a68b0c08f6.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: 255 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bjcij2ncikeyw"
path="res://.godot/imported/room_512_384.png-339d4ab4d8dc5972ef1c8a09d0380694.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/room_512_384.png"
dest_files=["res://.godot/imported/room_512_384.png-339d4ab4d8dc5972ef1c8a09d0380694.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: 569 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://icto1uyw4hj1"
path="res://.godot/imported/standard_brick.png.png-f806a78b5dfedf81aa2e413ced30aa6a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/standard_brick.png.png"
dest_files=["res://.godot/imported/standard_brick.png.png-f806a78b5dfedf81aa2e413ced30aa6a.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: 7.1 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://baa5wkuyqouh6"
path="res://.godot/imported/standard_brick_128_128.jpg-0dc76f792db60d64e5610aa75364c4ef.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/standard_brick_128_128.jpg"
dest_files=["res://.godot/imported/standard_brick_128_128.jpg-0dc76f792db60d64e5610aa75364c4ef.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.9 MiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://balpojbve2n4f"
path="res://.godot/imported/water.png-28a245d0248e5e7513f6a266dcca901f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/environment/water.png"
dest_files=["res://.godot/imported/water.png-28a245d0248e5e7513f6a266dcca901f.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

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