Compare commits
40 Commits
feature/wh
...
6b03d623a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b03d623a5 | |||
| f981ef18b1 | |||
| 558a0ff9bc | |||
| dc403179f7 | |||
| dde92737b6 | |||
| 7e65137922 | |||
| 473c9f4692 | |||
| 9d5536fd32 | |||
| 3eef8c0814 | |||
| fa38b75d8f | |||
| 64b9931eac | |||
| 22f4ec5d76 | |||
| 23d63a9dda | |||
| 0b0bdbacfa | |||
| 8123bc6b22 | |||
| 64850c2cae | |||
| d96abbb8b9 | |||
| 88a8eaad02 | |||
| 26479636ec | |||
| 3d1973f284 | |||
| ca6bf36be3 | |||
| f24a01dbca | |||
| 19794d896e | |||
| 7fcb41d576 | |||
|
|
326ab7ce5c | ||
|
|
603e7d9fc6 | ||
|
|
e989b4adf1 | ||
|
|
6e70aac0b9 | ||
| 625fe0ff6c | |||
|
|
0e5b9f947b | ||
|
|
749e2c257b | ||
|
|
e335a35f6c | ||
|
|
136e1344a0 | ||
|
|
25a21f92be | ||
|
|
9c2e3bf15a | ||
|
|
9e288dbb62 | ||
|
|
c8e73bec59 | ||
|
|
16f24ab26f | ||
|
|
414225e8c1 | ||
|
|
fb7cba4088 |
105
.claude/skills/godot-cli-test-runner/SKILL.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
name: godot-cli-test-runner
|
||||||
|
description: Run Godot CLI commands for this project with emphasis on headless test execution, script runs, scene runs, and export/debug operations. Use when the user asks to run Godot tests or commands (for example “Godot 跑测试”, “执行 Godot 命令”, “检查 Godot 参数”), troubleshoot CLI failures, or request reusable terminal/CI command templates.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot CLI Test Runner
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Use deterministic Godot CLI workflows for Windows terminal and CI-style execution. Prefer `--headless`, explicit `--path`, and `--log-file` for reproducible diagnostics.
|
||||||
|
|
||||||
|
## Quick Decision
|
||||||
|
1. Parse-only check script syntax: `--headless --path . --script <file> --check-only`.
|
||||||
|
2. If test logic depends on autoload singletons (for example `SceneManager`, `LocationManager`, `EventSystem`), do not use direct `--script` as primary validation; use scene/project context first.
|
||||||
|
3. For isolated script tests without autoload dependencies, run `--headless --path . --script <file>`.
|
||||||
|
4. Export build by using `--export-release` / `--export-debug` with an existing preset.
|
||||||
|
5. Diagnose CLI behavior by adding `--verbose` and always writing `--log-file`.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Resolve executable and project path
|
||||||
|
1. Prefer `godot` from PATH.
|
||||||
|
2. If not available, use explicit exe path (for this machine typically `D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64_console.exe` or `D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe`).
|
||||||
|
3. Run from repository root and always pass `--path .` unless intentionally targeting another project.
|
||||||
|
|
||||||
|
### 2. Preflight checks
|
||||||
|
1. Confirm engine version: `godot --version`.
|
||||||
|
2. Confirm options when needed: `godot --help`.
|
||||||
|
3. Confirm project exists: ensure `project.godot` is present under `--path`.
|
||||||
|
4. Read `project.godot` `[autoload]` and check whether the target test script references those singleton names.
|
||||||
|
5. Prepare a log output path (for example `.godot/test_xxx.log`) and pass `--log-file`.
|
||||||
|
|
||||||
|
### 3. Execute task type
|
||||||
|
1. Autoload-dependent validation (preferred when script references global singleton names):
|
||||||
|
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
|
||||||
|
2. Scene-specific validation:
|
||||||
|
`godot --headless --path . --scene res://scenes/Maps/square.tscn --quit-after 90 --log-file .godot/smoke_square.log`
|
||||||
|
3. Script test (only for isolated logic or known SceneTree tests):
|
||||||
|
`godot --headless --path . --script tests/unit/test_xxx.gd --log-file .godot/test_xxx.log`
|
||||||
|
4. Script syntax only:
|
||||||
|
`godot --headless --path . --script tests/unit/test_xxx.gd --check-only --log-file .godot/check_xxx.log`
|
||||||
|
5. Export:
|
||||||
|
`godot --headless --path . --export-release "Web" web_assets/index.html --log-file .godot/export_web_release.log`
|
||||||
|
|
||||||
|
### 4. Capture and report results
|
||||||
|
1. Report exit code, key stdout/stderr lines, and failed command.
|
||||||
|
2. For failures, include one retry variant (for example add `--verbose`, switch explicit exe path, or switch from `--script` to `--scene` context).
|
||||||
|
3. Keep output concise and actionable.
|
||||||
|
4. If `--script` fails with missing singleton identifiers, mark it as context mismatch first, not business regression.
|
||||||
|
|
||||||
|
## Command Templates
|
||||||
|
|
||||||
|
### Windows (explicit exe)
|
||||||
|
```powershell
|
||||||
|
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64_console.exe" --headless --path . --log-file .godot\test_websocket_close_code.log --script tests/unit/test_websocket_close_code.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic (PATH)
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --log-file .godot/test_websocket_close_code.log --script tests/unit/test_websocket_close_code.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
### With extra app args
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --log-file .godot/test_runner.log --script tests/unit/test_runner.gd -- --case websocket --timeout 30
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimal Runnable Examples
|
||||||
|
Run from repository root (`--path .`).
|
||||||
|
|
||||||
|
1. Run one scene-level smoke test (autoload-safe):
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run one test script (isolated logic):
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --script tests/unit/test_websocket_close_code.gd --log-file .godot/test_websocket_close_code.log
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run one scene:
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --scene res://scenes/SomeScene.tscn --quit-after 90 --log-file .godot/smoke_scene.log
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Parse script only (syntax check):
|
||||||
|
```powershell
|
||||||
|
godot --headless --path . --script tests/unit/test_websocket_close_code.gd --check-only --log-file .godot/check_websocket.log
|
||||||
|
```
|
||||||
|
|
||||||
|
If `godot` is not in PATH, replace `godot` with explicit exe call:
|
||||||
|
```powershell
|
||||||
|
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe" <same arguments>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option Summary Reference
|
||||||
|
Use `references/godot-cli-commands.md` for categorized option summary and quick recipes based on `godot --help` output.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
1. Prefer non-interactive commands.
|
||||||
|
2. Prefer `--headless` for tests and scripts.
|
||||||
|
3. For this environment, include `--log-file` for reproducible logs and to avoid console build logging issues.
|
||||||
|
4. Avoid assuming GUT addon exists; check `addons/gut/gut_cmdline.gd` before using GUT command.
|
||||||
|
5. Use `--check-only` when user requests parse/syntax validation only.
|
||||||
|
6. For long-running runs, include `--quit-after` when appropriate.
|
||||||
|
7. Do not classify missing autoload singleton errors in `--script` mode as product regressions until scene/project-context validation is also run.
|
||||||
4
.claude/skills/godot-cli-test-runner/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Godot CLI Test Runner"
|
||||||
|
short_description: "Run Godot tests and command-line workflows"
|
||||||
|
default_prompt: "Use this skill to run Godot headless tests with autoload-aware strategy, check CLI options, and execute Godot commands with reproducible --log-file diagnostics."
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Godot CLI Commands Summary
|
||||||
|
|
||||||
|
This reference summarizes the provided `godot --help` output for fast command selection.
|
||||||
|
|
||||||
|
## 1. Core inspection
|
||||||
|
- Help: `godot --help`
|
||||||
|
- Version: `godot --version`
|
||||||
|
- Verbose logs: `godot --verbose`
|
||||||
|
- Quiet mode: `godot --quiet`
|
||||||
|
|
||||||
|
## 2. Project targeting and run mode
|
||||||
|
- Point to project directory: `godot --path .`
|
||||||
|
- Run specific scene: `godot --path . --scene res://scenes/MainScene.tscn`
|
||||||
|
- Headless mode: `godot --headless --path . ...`
|
||||||
|
- Quit quickly: `godot --path . --quit`
|
||||||
|
- Quit after N frames: `godot --path . --quit-after 120`
|
||||||
|
|
||||||
|
## 3. Script execution and tests
|
||||||
|
- Run script: `godot --headless --path . --script tests/unit/test_xxx.gd`
|
||||||
|
- Parse-only script check: `godot --headless --path . --script tests/unit/test_xxx.gd --check-only`
|
||||||
|
- Pass custom user args to script:
|
||||||
|
`godot --headless --path . --script tests/unit/test_runner.gd -- --case websocket`
|
||||||
|
- Autoload-safe smoke test (preferred when test uses singleton globals):
|
||||||
|
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
|
||||||
|
|
||||||
|
## 4. Debug and diagnostics
|
||||||
|
- Local debugger: `godot --debug --path .`
|
||||||
|
- Remote debug: `godot --remote-debug tcp://127.0.0.1:6007 --path .`
|
||||||
|
- Print FPS: `godot --path . --print-fps`
|
||||||
|
- Log to file: `godot --path . --log-file logs/godot.log`
|
||||||
|
- Disable VSync for profiling: `godot --path . --disable-vsync`
|
||||||
|
|
||||||
|
## 5. Display and runtime controls
|
||||||
|
- Fullscreen: `godot --path . --fullscreen`
|
||||||
|
- Windowed: `godot --path . --windowed`
|
||||||
|
- Resolution: `godot --path . --resolution 1920x1080`
|
||||||
|
- Max FPS: `godot --path . --max-fps 60`
|
||||||
|
- Fixed FPS: `godot --path . --fixed-fps 60`
|
||||||
|
- Time scale: `godot --path . --time-scale 0.5`
|
||||||
|
|
||||||
|
## 6. Export operations (editor build only)
|
||||||
|
- Release export:
|
||||||
|
`godot --path . --export-release "Web" build/web/index.html`
|
||||||
|
- Debug export:
|
||||||
|
`godot --path . --export-debug "Web" build/web/index.html`
|
||||||
|
- Pack export:
|
||||||
|
`godot --path . --export-pack "Web" build/web/game.pck`
|
||||||
|
- Check preset syntax only:
|
||||||
|
`godot --path . --export-debug "Web" --check-only`
|
||||||
|
|
||||||
|
## 7. Common quick recipes
|
||||||
|
- Run a unit test script (isolated logic):
|
||||||
|
`godot --headless --path . --script tests/unit/test_websocket_close_code.gd --log-file .godot/test_websocket_close_code.log`
|
||||||
|
- Validate script syntax without running:
|
||||||
|
`godot --headless --path . --script tests/unit/test_websocket_close_code.gd --check-only --log-file .godot/check_websocket.log`
|
||||||
|
- Run game with verbose logs:
|
||||||
|
`godot --verbose --path . --log-file .godot/main_verbose.log`
|
||||||
|
- Run scene and auto-exit after startup checks:
|
||||||
|
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
|
||||||
|
|
||||||
|
## 8. Windows explicit executable pattern
|
||||||
|
When `godot` is not in PATH, call the executable directly:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe" --headless --path . --script tests/unit/test_websocket_close_code.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Notes for AI execution
|
||||||
|
- Prefer `--headless` for tests and scripts in terminal/CI.
|
||||||
|
- Always include `--path .` for reproducibility.
|
||||||
|
- Use `--check-only` for parse checks when execution is not needed.
|
||||||
|
- If a script depends on autoload singleton names from `project.godot` (`SceneManager`, `LocationManager`, `EventSystem`, etc.), validate in scene/project context before concluding regression.
|
||||||
|
- Prefer `--log-file` for reliable diagnostics and environment-specific logging issues.
|
||||||
|
- Add `--verbose` when failure context is insufficient.
|
||||||
3
.gitignore
vendored
@@ -56,3 +56,6 @@ coverage/
|
|||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
node_modules/
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
*.uid
|
||||||
|
*.import
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"api_base_url": "https://whaletownend.xinghangee.icu",
|
"api_base_url": "https://whaletownend.xinghangee.icu",
|
||||||
|
"location_ws_url": "wss://whaletownend.xinghangee.icu/game",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"retry_count": 3
|
"retry_count": 3
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"ui": {
|
"ui": {
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
"register": "注册",
|
"register": "注册",
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"confirm_password": "确认密码",
|
"confirm_password": "确认密码",
|
||||||
"verification_code": "验证码",
|
"verification_code": "验证码",
|
||||||
"send_code": "发送验证码",
|
"send_code": "发送验证码",
|
||||||
"forgot_password": "忘记密码",
|
"forgot_password": "忘记密码",
|
||||||
"enter_town": "进入小镇",
|
"enter_town": "进入小镇",
|
||||||
"logout": "退出登录"
|
"logout": "退出登录"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"login_success": "登录成功!正在进入鲸鱼镇...",
|
"login_success": "登录成功!正在进入鲸鱼镇...",
|
||||||
"register_success": "注册成功!欢迎加入鲸鱼镇",
|
"register_success": "注册成功!欢迎加入鲸鱼镇",
|
||||||
"network_error": "网络连接失败,请检查网络连接",
|
"network_error": "网络连接失败,请检查网络连接",
|
||||||
"invalid_username": "用户名只能包含字母、数字和下划线",
|
"invalid_username": "用户名只能包含字母、数字和下划线",
|
||||||
"invalid_email": "请输入有效的邮箱地址",
|
"invalid_email": "请输入有效的邮箱地址",
|
||||||
"password_too_short": "密码长度至少8位",
|
"password_too_short": "密码长度至少8位",
|
||||||
"password_mismatch": "两次输入的密码不一致",
|
"password_mismatch": "两次输入的密码不一致",
|
||||||
"verification_code_sent": "验证码已发送到您的邮箱,请查收"
|
"verification_code_sent": "验证码已发送到您的邮箱,请查收"
|
||||||
},
|
},
|
||||||
"game": {
|
"game": {
|
||||||
"level": "等级",
|
"level": "等级",
|
||||||
"coins": "金币",
|
"coins": "金币",
|
||||||
"experience": "经验",
|
"experience": "经验",
|
||||||
"energy": "体力",
|
"energy": "体力",
|
||||||
"explore": "探索小镇",
|
"explore": "探索小镇",
|
||||||
"inventory": "背包",
|
"inventory": "背包",
|
||||||
"shop": "商店",
|
"shop": "商店",
|
||||||
"friends": "好友",
|
"friends": "好友",
|
||||||
"settings": "设置"
|
"settings": "设置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ WhaleTown/ # 🐋 项目根目录
|
|||||||
│ ├── performance/ # ⚡ 性能测试(帧率、内存优化)
|
│ ├── performance/ # ⚡ 性能测试(帧率、内存优化)
|
||||||
│ └── api/ # 🌐 API接口测试
|
│ └── api/ # 🌐 API接口测试
|
||||||
└── 🌐 web_assets/ # 🌍 Web导出资源
|
└── 🌐 web_assets/ # 🌍 Web导出资源
|
||||||
├── html/ # 📄 HTML模板文件
|
├── html/ # 📄 HTML模板文件
|
||||||
├── css/ # 🎨 样式文件
|
├── css/ # 🎨 样式文件
|
||||||
└── js/ # 📜 JavaScript脚本
|
└── js/ # 📜 JavaScript脚本
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🔧 核心架构说明
|
### 🔧 核心架构说明
|
||||||
|
|||||||
@@ -57,4 +57,15 @@ 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"
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://qn0imbklx1m0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dybcuscku7tyl
|
|
||||||
@@ -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,191 @@ 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()
|
||||||
|
_connect_network_signals()
|
||||||
|
|
||||||
# 清理资源
|
# 清理资源
|
||||||
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()
|
||||||
|
|
||||||
|
_disconnect_network_signals()
|
||||||
|
|
||||||
|
# ============ 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,19 +596,21 @@ 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)
|
||||||
|
|
||||||
if result.should_show_toast:
|
if result.should_show_toast:
|
||||||
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)
|
||||||
@@ -441,19 +618,21 @@ 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)
|
||||||
|
|
||||||
if result.should_show_toast:
|
if result.should_show_toast:
|
||||||
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)
|
||||||
else:
|
else:
|
||||||
@@ -499,7 +678,7 @@ func _on_send_login_code_response(success: bool, data: Dictionary, error_info: D
|
|||||||
func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary):
|
func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary):
|
||||||
button_state_changed.emit("forgot_password_btn", false, "忘记密码")
|
button_state_changed.emit("forgot_password_btn", false, "忘记密码")
|
||||||
|
|
||||||
var result = ResponseHandler.handle_send_login_code_response(success, data, error_info)
|
var result = ResponseHandler.handle_forgot_password_response(success, data, error_info)
|
||||||
|
|
||||||
if result.should_show_toast:
|
if result.should_show_toast:
|
||||||
show_toast_message.emit(result.message, result.success)
|
show_toast_message.emit(result.message, result.success)
|
||||||
@@ -538,10 +717,10 @@ func _can_send_verification_code(email: String) -> bool:
|
|||||||
if not email_data.sent:
|
if not email_data.sent:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
var current_time = Time.get_time_dict_from_system()
|
var current_timestamp: int = int(Time.get_unix_time_from_system())
|
||||||
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
|
var sent_timestamp: int = int(email_data.get("time", 0))
|
||||||
|
|
||||||
return (current_timestamp - email_data.time) >= code_cooldown
|
return float(current_timestamp - sent_timestamp) >= code_cooldown
|
||||||
|
|
||||||
# 获取剩余冷却时间
|
# 获取剩余冷却时间
|
||||||
func get_remaining_cooldown_time(email: String) -> int:
|
func get_remaining_cooldown_time(email: String) -> int:
|
||||||
@@ -549,15 +728,15 @@ func get_remaining_cooldown_time(email: String) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
var email_data = verification_codes_sent[email]
|
var email_data = verification_codes_sent[email]
|
||||||
var current_time = Time.get_time_dict_from_system()
|
var current_timestamp: int = int(Time.get_unix_time_from_system())
|
||||||
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
|
var sent_timestamp: int = int(email_data.get("time", 0))
|
||||||
|
var remaining: int = int(code_cooldown - float(current_timestamp - sent_timestamp))
|
||||||
|
|
||||||
return int(code_cooldown - (current_timestamp - email_data.time))
|
return maxi(0, remaining)
|
||||||
|
|
||||||
# 记录验证码发送状态
|
# 记录验证码发送状态
|
||||||
func _record_verification_code_sent(email: String):
|
func _record_verification_code_sent(email: String):
|
||||||
var current_time = Time.get_time_dict_from_system()
|
var current_timestamp: int = int(Time.get_unix_time_from_system())
|
||||||
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
|
|
||||||
|
|
||||||
if not verification_codes_sent.has(email):
|
if not verification_codes_sent.has(email):
|
||||||
verification_codes_sent[email] = {}
|
verification_codes_sent[email] = {}
|
||||||
@@ -582,8 +761,32 @@ func _has_sent_verification_code(email: String) -> bool:
|
|||||||
func _is_valid_identifier(identifier: String) -> bool:
|
func _is_valid_identifier(identifier: String) -> bool:
|
||||||
return StringUtils.is_valid_email(identifier) or _is_valid_phone(identifier)
|
return StringUtils.is_valid_email(identifier) or _is_valid_phone(identifier)
|
||||||
|
|
||||||
|
# 连接/断开 NetworkManager 请求信号,用于回收 active_request_ids
|
||||||
|
func _connect_network_signals() -> void:
|
||||||
|
if not NetworkManager.request_completed.is_connected(_on_network_request_completed):
|
||||||
|
NetworkManager.request_completed.connect(_on_network_request_completed)
|
||||||
|
if not NetworkManager.request_failed.is_connected(_on_network_request_failed):
|
||||||
|
NetworkManager.request_failed.connect(_on_network_request_failed)
|
||||||
|
|
||||||
|
func _disconnect_network_signals() -> void:
|
||||||
|
if NetworkManager.request_completed.is_connected(_on_network_request_completed):
|
||||||
|
NetworkManager.request_completed.disconnect(_on_network_request_completed)
|
||||||
|
if NetworkManager.request_failed.is_connected(_on_network_request_failed):
|
||||||
|
NetworkManager.request_failed.disconnect(_on_network_request_failed)
|
||||||
|
|
||||||
|
func _on_network_request_completed(request_id: String, _success: bool, _data: Dictionary) -> void:
|
||||||
|
_remove_active_request_id(request_id)
|
||||||
|
|
||||||
|
func _on_network_request_failed(request_id: String, _error_type: String, _message: String) -> void:
|
||||||
|
_remove_active_request_id(request_id)
|
||||||
|
|
||||||
|
func _remove_active_request_id(request_id: String) -> void:
|
||||||
|
var index: int = active_request_ids.find(request_id)
|
||||||
|
if index != -1:
|
||||||
|
active_request_ids.remove_at(index)
|
||||||
|
|
||||||
# 验证手机号格式
|
# 验证手机号格式
|
||||||
func _is_valid_phone(phone: String) -> bool:
|
func _is_valid_phone(phone: String) -> bool:
|
||||||
var regex = RegEx.new()
|
var regex = RegEx.new()
|
||||||
regex.compile("^\\+?[1-9]\\d{1,14}$")
|
regex.compile("^\\+?[1-9]\\d{1,14}$")
|
||||||
return regex.search(phone) != null
|
return regex.search(phone) != null
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://bpdyraefv0yta
|
|
||||||
753
_Core/managers/ChatManager.gd
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
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] = []
|
||||||
|
|
||||||
|
# 空消息类型告警限频(避免日志刷屏)
|
||||||
|
const EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: float = 10.0
|
||||||
|
var _last_empty_message_type_warning_at: float = -1000.0
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 生命周期方法
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 初始化
|
||||||
|
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_variant: Variant = json.data
|
||||||
|
if not (data_variant is Dictionary):
|
||||||
|
push_warning("ChatManager: 收到非对象消息,已忽略")
|
||||||
|
return
|
||||||
|
|
||||||
|
var data: Dictionary = data_variant
|
||||||
|
|
||||||
|
# 兼容不同后端字段命名:t / type
|
||||||
|
var message_type: String = str(data.get("t", data.get("type", ""))).strip_edges()
|
||||||
|
if message_type.is_empty():
|
||||||
|
_warn_empty_message_type_limited(data)
|
||||||
|
return
|
||||||
|
|
||||||
|
match message_type:
|
||||||
|
"connected":
|
||||||
|
pass
|
||||||
|
"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)
|
||||||
|
"error":
|
||||||
|
_handle_error_response(data)
|
||||||
|
_:
|
||||||
|
push_warning("ChatManager: 未处理的消息类型 %s" % message_type)
|
||||||
|
|
||||||
|
func _warn_empty_message_type_limited(data: Dictionary) -> void:
|
||||||
|
var now: float = Time.get_unix_time_from_system()
|
||||||
|
if now - _last_empty_message_type_warning_at < EMPTY_MESSAGE_TYPE_WARNING_INTERVAL:
|
||||||
|
return
|
||||||
|
|
||||||
|
_last_empty_message_type_warning_at = now
|
||||||
|
var payload_preview: String = JSON.stringify(data)
|
||||||
|
if payload_preview.length() > 180:
|
||||||
|
payload_preview = payload_preview.substr(0, 180) + "..."
|
||||||
|
push_warning("ChatManager: 收到未带消息类型的消息,已忽略 payload=%s" % payload_preview)
|
||||||
|
|
||||||
|
# 处理登录成功
|
||||||
|
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()
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
# 获取当前登录用户
|
# 获取当前登录用户
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://cd8fn73ysjxh8
|
|
||||||
220
_Core/managers/LocationManager.gd
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LocationManager.gd - 位置同步管理器
|
||||||
|
# ============================================================================
|
||||||
|
# 负责与后端 WebSocket 服务进行位置同步和多人会话管理
|
||||||
|
#
|
||||||
|
# 协议文档: new_docs/game_architecture_design.md
|
||||||
|
# 后端地址默认值: wss://whaletownend.xinghangee.icu/game
|
||||||
|
# 可通过以下方式覆盖:
|
||||||
|
# 1) 环境变量 WHALETOWN_LOCATION_WS_URL
|
||||||
|
# 2) Config/game_config.json 或 config/game_config.json 中的
|
||||||
|
# network.location_ws_url / network.game_ws_url
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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 DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game"
|
||||||
|
const WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL"
|
||||||
|
const PING_INTERVAL = 25.0 # 秒
|
||||||
|
|
||||||
|
var _socket: WebSocketPeer
|
||||||
|
var _connected: bool = false
|
||||||
|
var _ping_timer: float = 0.0
|
||||||
|
var _auth_token: String = ""
|
||||||
|
var _connection_error_reported: bool = false
|
||||||
|
var _is_connecting: bool = false
|
||||||
|
var _ws_url: String = DEFAULT_WS_URL
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
_socket = WebSocketPeer.new()
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络
|
||||||
|
_ws_url = _resolve_ws_url()
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
_socket.poll()
|
||||||
|
|
||||||
|
var state = _socket.get_ready_state()
|
||||||
|
|
||||||
|
if state == WebSocketPeer.STATE_OPEN:
|
||||||
|
_connection_error_reported = false
|
||||||
|
_is_connecting = false
|
||||||
|
|
||||||
|
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()
|
||||||
|
elif _is_connecting and not _connection_error_reported:
|
||||||
|
var close_code := _socket.get_close_code()
|
||||||
|
var close_reason := _socket.get_close_reason()
|
||||||
|
push_warning(
|
||||||
|
"LocationManager: WebSocket 握手失败,close_code=%d, reason=%s" % [close_code, close_reason]
|
||||||
|
)
|
||||||
|
connection_error.emit()
|
||||||
|
_connection_error_reported = true
|
||||||
|
_is_connecting = false
|
||||||
|
|
||||||
|
func connect_to_server():
|
||||||
|
var state: WebSocketPeer.State = _socket.get_ready_state()
|
||||||
|
if state == WebSocketPeer.STATE_OPEN or state == WebSocketPeer.STATE_CONNECTING:
|
||||||
|
return
|
||||||
|
|
||||||
|
_connection_error_reported = false
|
||||||
|
_is_connecting = true
|
||||||
|
var err = _socket.connect_to_url(_ws_url)
|
||||||
|
if err != OK:
|
||||||
|
push_error("LocationManager: WebSocket 连接请求失败,url=%s, 错误码: %d" % [_ws_url, err])
|
||||||
|
connection_error.emit()
|
||||||
|
_connection_error_reported = true
|
||||||
|
_is_connecting = false
|
||||||
|
else:
|
||||||
|
# Godot WebSocket connect is non-blocking, wait for state change in _process
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _resolve_ws_url() -> String:
|
||||||
|
var env_url: String = OS.get_environment(WS_URL_ENV_KEY).strip_edges()
|
||||||
|
if not env_url.is_empty():
|
||||||
|
return env_url
|
||||||
|
|
||||||
|
for config_path in ["res://Config/game_config.json", "res://config/game_config.json"]:
|
||||||
|
var config_url: String = _load_ws_url_from_config(config_path)
|
||||||
|
if not config_url.is_empty():
|
||||||
|
return config_url
|
||||||
|
|
||||||
|
return DEFAULT_WS_URL
|
||||||
|
|
||||||
|
func _load_ws_url_from_config(config_path: String) -> String:
|
||||||
|
if not FileAccess.file_exists(config_path):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var content: String = FileAccess.get_file_as_string(config_path)
|
||||||
|
if content.is_empty():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var json := JSON.new()
|
||||||
|
if json.parse(content) != OK:
|
||||||
|
push_warning("LocationManager: 读取配置失败 %s - %s" % [config_path, json.get_error_message()])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var data_variant: Variant = json.data
|
||||||
|
if not (data_variant is Dictionary):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var root: Dictionary = data_variant
|
||||||
|
var network_variant: Variant = root.get("network", {})
|
||||||
|
if not (network_variant is Dictionary):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var network_config: Dictionary = network_variant
|
||||||
|
var ws_url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges()
|
||||||
|
return ws_url
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -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)
|
||||||
@@ -638,8 +602,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:
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://dr7v30wheetca
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://nseguk2ytiw6
|
|
||||||
@@ -36,27 +36,21 @@ 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)
|
||||||
|
|
||||||
# 场景路径映射表
|
# 场景路径映射表
|
||||||
# 将场景名称映射到实际的文件路径
|
# 将场景名称映射到实际的文件路径
|
||||||
# 便于统一管理和修改场景路径
|
# 便于统一管理和修改场景路径
|
||||||
var scene_paths: Dictionary = {
|
var scene_paths: Dictionary = {
|
||||||
"main": "res://scenes/MainScene.tscn", # 主场景 - 游戏入口
|
"main": "res://scenes/MainScene.tscn", # 主场景 - 游戏入口
|
||||||
"auth": "res://scenes/ui/LoginWindow.tscn", # 认证场景 - 登录窗口
|
"square": "res://scenes/Maps/square.tscn", # 广场地图
|
||||||
"game": "res://scenes/maps/game_scene.tscn", # 游戏场景 - 主要游戏内容
|
"room": "res://scenes/Maps/room.tscn", # 房间地图
|
||||||
"battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统
|
"fountain": "res://scenes/Maps/fountain.tscn", # 喷泉地图
|
||||||
"inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面
|
"datawhale_home": "res://scenes/Maps/datawhale_home.tscn", # 数据鲸鱼之家
|
||||||
"shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面
|
"community": "res://scenes/Maps/community.tscn" # 社区地图
|
||||||
"settings": "res://scenes/ui/SettingsWindow.tscn" # 设置界面
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============ 生命周期方法 ============
|
|
||||||
|
|
||||||
# 初始化场景管理器
|
|
||||||
# 在节点准备就绪时调用
|
|
||||||
func _ready():
|
|
||||||
print("SceneManager 初始化完成")
|
|
||||||
|
|
||||||
# ============ 场景切换方法 ============
|
# ============ 场景切换方法 ============
|
||||||
|
|
||||||
# 切换到指定场景
|
# 切换到指定场景
|
||||||
@@ -77,8 +71,6 @@ func _ready():
|
|||||||
#
|
#
|
||||||
# 使用示例:
|
# 使用示例:
|
||||||
# var success = SceneManager.change_scene("main", true)
|
# var success = SceneManager.change_scene("main", true)
|
||||||
# if success:
|
|
||||||
# print("场景切换成功")
|
|
||||||
#
|
#
|
||||||
# 注意事项:
|
# 注意事项:
|
||||||
# - 场景切换是异步操作
|
# - 场景切换是异步操作
|
||||||
@@ -87,16 +79,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 +100,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,18 +113,83 @@ 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
|
||||||
|
|
||||||
# ============ 查询方法 ============
|
# ============ 查询方法 ============
|
||||||
|
|
||||||
# 获取当前场景名称
|
# 获取当前场景名称
|
||||||
#
|
#
|
||||||
# 返回值:
|
# 返回值:
|
||||||
# String - 当前场景的名称
|
# String - 当前场景的名称
|
||||||
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 spawn_name: String = _next_spawn_name
|
||||||
|
_next_spawn_name = ""
|
||||||
|
return spawn_name
|
||||||
|
|
||||||
# ============ 场景注册方法 ============
|
# ============ 场景注册方法 ============
|
||||||
|
|
||||||
# 注册新场景
|
# 注册新场景
|
||||||
@@ -150,7 +206,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 +221,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 +236,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
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://d3l286ti5gqhw
|
|
||||||
@@ -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)
|
||||||
@@ -231,4 +225,4 @@ func _rearrange_toasts():
|
|||||||
if is_instance_valid(toast):
|
if is_instance_valid(toast):
|
||||||
var tween = toast_container.create_tween()
|
var tween = toast_container.create_tween()
|
||||||
tween.tween_property(toast, "position:y", current_y, 0.2)
|
tween.tween_property(toast, "position:y", current_y, 0.2)
|
||||||
current_y += toast.size.y + 15
|
current_y += toast.size.y + 15
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://buk7d21cag262
|
|
||||||
460
_Core/managers/WebSocketManager.gd
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
# 当前 CLOSED 状态是否已经处理过(防止每帧重复处理 close 事件)
|
||||||
|
var _closed_state_handled: bool = false
|
||||||
|
|
||||||
|
# 心跳定时器
|
||||||
|
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(is_reconnect_attempt: bool = false) -> void:
|
||||||
|
if _connection_state == ConnectionState.CONNECTED or _connection_state == ConnectionState.CONNECTING:
|
||||||
|
push_warning("已经在连接或已连接状态")
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_reconnect_attempt:
|
||||||
|
_set_connection_state(ConnectionState.RECONNECTING)
|
||||||
|
else:
|
||||||
|
_set_connection_state(ConnectionState.CONNECTING)
|
||||||
|
_clean_close = true
|
||||||
|
_closed_state_handled = false
|
||||||
|
# 仅在首次/手动连接时重置重连计数,重连流程保持累计尝试次数
|
||||||
|
if not is_reconnect_attempt:
|
||||||
|
_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:
|
||||||
|
_closed_state_handled = false
|
||||||
|
|
||||||
|
# 正在连接
|
||||||
|
if _connection_state != ConnectionState.CONNECTING and _connection_state != ConnectionState.RECONNECTING:
|
||||||
|
_set_connection_state(ConnectionState.CONNECTING)
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_OPEN:
|
||||||
|
_closed_state_handled = false
|
||||||
|
|
||||||
|
# 连接成功
|
||||||
|
if _connection_state != ConnectionState.CONNECTED:
|
||||||
|
_on_websocket_connected()
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_CLOSING:
|
||||||
|
_closed_state_handled = false
|
||||||
|
|
||||||
|
# 正在关闭
|
||||||
|
pass
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_CLOSED:
|
||||||
|
if _closed_state_handled:
|
||||||
|
return
|
||||||
|
|
||||||
|
_closed_state_handled = true
|
||||||
|
|
||||||
|
# 仅在连接生命周期中发生的关闭才触发关闭处理
|
||||||
|
var should_handle_close: bool = (
|
||||||
|
_connection_state == ConnectionState.CONNECTED
|
||||||
|
or _connection_state == ConnectionState.CONNECTING
|
||||||
|
or _connection_state == ConnectionState.RECONNECTING
|
||||||
|
)
|
||||||
|
if should_handle_close:
|
||||||
|
var close_code: int = _websocket_peer.get_close_code()
|
||||||
|
_on_websocket_closed(_is_clean_close_code(close_code))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 判断关闭码是否为干净关闭
|
||||||
|
# Godot 中 close_code == -1 表示非干净关闭(异常断开)
|
||||||
|
func _is_clean_close_code(close_code: int) -> bool:
|
||||||
|
return close_code != -1
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 重连机制
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置重连定时器
|
||||||
|
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(true)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 心跳机制
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置心跳定时器
|
||||||
|
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 "未知状态"
|
||||||
@@ -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("清空所有事件监听器")
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://bfheblucmti24
|
|
||||||
@@ -131,13 +131,14 @@ static func get_grid_rect(grid_pos: Vector2i) -> Rect2:
|
|||||||
var world_pos = grid_to_world(grid_pos)
|
var world_pos = grid_to_world(grid_pos)
|
||||||
return Rect2(world_pos, Vector2(GRID_SIZE, GRID_SIZE))
|
return Rect2(world_pos, Vector2(GRID_SIZE, GRID_SIZE))
|
||||||
|
|
||||||
# 打印网格信息(调试用)
|
# 获取网格信息(调试辅助)
|
||||||
static func print_grid_info(world_pos: Vector2) -> void:
|
static func print_grid_info(world_pos: Vector2) -> Dictionary:
|
||||||
var grid_pos = world_to_grid(world_pos)
|
var grid_pos = world_to_grid(world_pos)
|
||||||
var snapped_pos = snap_to_grid(world_pos)
|
var snapped_pos = snap_to_grid(world_pos)
|
||||||
var center_pos = grid_to_world_center(grid_pos)
|
var center_pos = grid_to_world_center(grid_pos)
|
||||||
|
return {
|
||||||
print("世界坐标: ", world_pos)
|
"world_position": world_pos,
|
||||||
print("网格坐标: ", grid_pos)
|
"grid_position": grid_pos,
|
||||||
print("吸附位置: ", snapped_pos)
|
"snapped_position": snapped_pos,
|
||||||
print("网格中心: ", center_pos)
|
"center_position": center_pos
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://dceqpffgti4jb
|
|
||||||
@@ -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}
|
||||||
|
|
||||||
@@ -383,4 +383,4 @@ static func get_relative_time_until(utc_time_str: String) -> String:
|
|||||||
if minutes > 0:
|
if minutes > 0:
|
||||||
return "%d小时%d分钟后" % [hours, minutes]
|
return "%d小时%d分钟后" % [hours, minutes]
|
||||||
else:
|
else:
|
||||||
return "%d小时后" % hours
|
return "%d小时后" % hours
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://bu8onmk6q8wic
|
|
||||||
BIN
assets/audio/music/Spawn Fixed.mp3
Normal file
BIN
assets/characters/crayfish_npc_256_256.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/characters/npc_286_241.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/characters/payer_44_30.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/characters/player_spritesheet.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/characters/player_spritesheet_backup.png
Normal file
|
After Width: | Height: | Size: 856 KiB |
@@ -1,41 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="font_data_dynamic"
|
|
||||||
type="FontFile"
|
|
||||||
uid="uid://ce7ujbeobblyr"
|
|
||||||
path="res://.godot/imported/msyh.ttc-1f7944f6d1cff8092894a3525ec5156c.fontdata"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/fonts/msyh.ttc"
|
|
||||||
dest_files=["res://.godot/imported/msyh.ttc-1f7944f6d1cff8092894a3525ec5156c.fontdata"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
Rendering=null
|
|
||||||
antialiasing=1
|
|
||||||
generate_mipmaps=false
|
|
||||||
disable_embedded_bitmaps=true
|
|
||||||
multichannel_signed_distance_field=false
|
|
||||||
msdf_pixel_range=8
|
|
||||||
msdf_size=48
|
|
||||||
allow_system_fallback=true
|
|
||||||
force_autohinter=false
|
|
||||||
modulate_color_glyphs=false
|
|
||||||
hinting=1
|
|
||||||
subpixel_positioning=4
|
|
||||||
keep_rounding_remainders=true
|
|
||||||
oversampling=0.0
|
|
||||||
Fallbacks=null
|
|
||||||
fallbacks=[]
|
|
||||||
Compress=null
|
|
||||||
compress=false
|
|
||||||
preload=[{
|
|
||||||
"chars": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+-=[]{}|;':\",./<>?`~一二三四五六七八九十百千万亿用户名密码登录注册验证码邮箱小镇鲸鱼欢迎来到开始你的之旅请输入不能为空获取发送忘记返回居民身份确认再次已被使用换个等待分钟后试稍后正在创建账户测试模式生成查看控制台网络连接失败系统维护中升级稍后再试频繁联系管理员禁用审核先邮箱后使用成功进入镇错误或过期未找到存在",
|
|
||||||
"glyphs": [],
|
|
||||||
"name": "Web预加载",
|
|
||||||
"size": Vector2i(16, 0)
|
|
||||||
}]
|
|
||||||
language_support={}
|
|
||||||
script_support={}
|
|
||||||
opentype_features={}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://bwy5r7soxi76a"
|
|
||||||
path="res://.godot/imported/icon144.png-ae9d1f30a88beaab449c2cad89283dd3.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon144.png"
|
|
||||||
dest_files=["res://.godot/imported/icon144.png-ae9d1f30a88beaab449c2cad89283dd3.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://bqg5e8qn1j74u"
|
|
||||||
path="res://.godot/imported/icon16.png-3099ad8a609f90c382508b9c073ffd76.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon16.png"
|
|
||||||
dest_files=["res://.godot/imported/icon16.png-3099ad8a609f90c382508b9c073ffd76.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://drpllpsjdiaex"
|
|
||||||
path="res://.godot/imported/icon180.png-20a9d7b98bfb315dd470e3635f315a17.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon180.png"
|
|
||||||
dest_files=["res://.godot/imported/icon180.png-20a9d7b98bfb315dd470e3635f315a17.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://dt24j6p0cijqo"
|
|
||||||
path="res://.godot/imported/icon32.png-9a0aceb23d191139c34540a188bf8c91.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon32.png"
|
|
||||||
dest_files=["res://.godot/imported/icon32.png-9a0aceb23d191139c34540a188bf8c91.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://dt817lem3dwee"
|
|
||||||
path="res://.godot/imported/icon512.png-1c1c4b424489de87a542c89bec6eb15b.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon512.png"
|
|
||||||
dest_files=["res://.godot/imported/icon512.png-1c1c4b424489de87a542c89bec6eb15b.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://ci42rd5qe6icl"
|
|
||||||
path="res://.godot/imported/icon64.png-da8a1a20e3bf4dcf06c8ff6c558caaff.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/icon64.png"
|
|
||||||
dest_files=["res://.godot/imported/icon64.png-da8a1a20e3bf4dcf06c8ff6c558caaff.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://cnw6e3wmy0ea4"
|
|
||||||
path="res://.godot/imported/image(1).png-c89cc92103e50aaba40bf38c797be77f.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/image(1).png"
|
|
||||||
dest_files=["res://.godot/imported/image(1).png-c89cc92103e50aaba40bf38c797be77f.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://c7v22i1hgo1x6"
|
|
||||||
path="res://.godot/imported/image.png-3f16548595ba9fb08c5e50ef3251d148.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/icon/image.png"
|
|
||||||
dest_files=["res://.godot/imported/image.png-3f16548595ba9fb08c5e50ef3251d148.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
|
|
||||||
BIN
assets/materials/NoticeBoard.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
assets/materials/WelcomeBoard.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
assets/sprites/environment/board.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/sprites/environment/community.png
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
BIN
assets/sprites/environment/community_512_512.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
BIN
assets/sprites/environment/deck_256_111.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sprites/environment/deck_256_93.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/sprites/environment/deck_2784_1536.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/sprites/environment/deck_384_167.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/sprites/environment/deck_512_164.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
assets/sprites/environment/deck_512_282.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/sprites/environment/deck_512_512.png
Normal file
|
After Width: | Height: | Size: 570 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
BIN
assets/sprites/environment/e2f5aff78fae12f979d3456eca0896b4.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
BIN
assets/sprites/environment/fountain_256_192.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
assets/sprites/environment/grass_128_128.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/sprites/environment/grass_256_256.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sprites/environment/ground.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/sprites/environment/house_256_192.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/sprites/environment/house_384_256.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
assets/sprites/environment/house_384_288.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
assets/sprites/environment/river.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/sprites/environment/river2_256_256.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/sprites/environment/river2_512_512.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
assets/sprites/environment/river_256_256.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sprites/environment/river_512_512.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/sprites/environment/room_512_384.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[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
|
|
||||||
BIN
assets/sprites/environment/standard_brick.png.png
Normal file
|
After Width: | Height: | Size: 569 KiB |
BIN
assets/sprites/environment/standard_brick_128_128.jpg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/sprites/environment/water.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://ignbtjvnp5k7"
|
|
||||||
path="res://.godot/imported/广场瓦片集.png-b224b40553b9f690e690f67a89e2b520.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/sprites/environment/广场瓦片集.png"
|
|
||||||
dest_files=["res://.godot/imported/广场瓦片集.png-b224b40553b9f690e690f67a89e2b520.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://dvsb51jintro"
|
|
||||||
path="res://.godot/imported/草地.png-2fa7f2346d7dc837788dd21e5693cec7.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/sprites/environment/草地.png"
|
|
||||||
dest_files=["res://.godot/imported/草地.png-2fa7f2346d7dc837788dd21e5693cec7.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
BIN
assets/ui/auth/Image (1).png
Normal file
|
After Width: | Height: | Size: 1000 KiB |
BIN
assets/ui/auth/Image (2).png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/ui/auth/Image (3).png
Normal file
|
After Width: | Height: | Size: 902 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://bx17oy8lvaca4"
|
|
||||||
path="res://.godot/imported/bg_auth_scene.png-818065fcc20397e855c75507c1313623.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/ui/auth/bg_auth_scene.png"
|
|
||||||
dest_files=["res://.godot/imported/bg_auth_scene.png-818065fcc20397e855c75507c1313623.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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://de4q4s1gxivtf"
|
|
||||||
path="res://.godot/imported/login_frame_smart_transparent.png-e5d0fd05b4713ddd3beae8223f2abb80.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/ui/auth/login_frame_smart_transparent.png"
|
|
||||||
dest_files=["res://.godot/imported/login_frame_smart_transparent.png-e5d0fd05b4713ddd3beae8223f2abb80.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
|
|
||||||
BIN
assets/ui/auth/微信图片_20251218232924_983_1425.png
Normal file
|
After Width: | Height: | Size: 992 KiB |
BIN
assets/ui/auth/清空内部组件的登录框架 (1).png
Normal file
|
After Width: | Height: | Size: 690 KiB |
BIN
assets/ui/auth/登录背景.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
assets/ui/auth/输入框.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/ui/chat/缩略框背景.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
assets/ui/chat/装饰.png
Normal file
|
After Width: | Height: | Size: 276 B |
BIN
assets/ui/chat/装饰2.png
Normal file
|
After Width: | Height: | Size: 281 B |
BIN
assets/ui/chat/输入框背景.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,40 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://gr7vud1lee4m"
|
|
||||||
path="res://.godot/imported/datawhale_logo.png-ddb5e2c04419eb84cfa8605bcbf64fbd.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/ui/datawhale_logo.png"
|
|
||||||
dest_files=["res://.godot/imported/datawhale_logo.png-ddb5e2c04419eb84cfa8605bcbf64fbd.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
|
|
||||||
7
assets/ui/world_text_theme.tres
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[gd_resource type="Theme" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="FontFile" uid="uid://ce7ujbeobblyr" path="res://assets/fonts/msyh.ttc" id="1_font"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
resource_local_to_scene = true
|
||||||
|
default_font = ExtResource("1_font")
|
||||||
29
claude.md
@@ -66,10 +66,10 @@
|
|||||||
```gdscript
|
```gdscript
|
||||||
extends GutTest
|
extends GutTest
|
||||||
func test_event_emission():
|
func test_event_emission():
|
||||||
var sender = Node.new()
|
var sender = Node.new()
|
||||||
watch_signals(EventSystem)
|
watch_signals(EventSystem)
|
||||||
EventSystem.emit_event(EventNames.PLAYER_MOVED, {})
|
EventSystem.emit_event(EventNames.PLAYER_MOVED, {})
|
||||||
assert_signal_emitted(EventSystem, "event_raised")
|
assert_signal_emitted(EventSystem, "event_raised")
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 8. Standard Development Workflow (MANDATORY)
|
## 🔄 8. Standard Development Workflow (MANDATORY)
|
||||||
@@ -152,10 +152,23 @@ class_name Player
|
|||||||
|
|
||||||
# 3. Lifecycle
|
# 3. Lifecycle
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
_move(delta)
|
_move(delta)
|
||||||
|
|
||||||
# 4. Private Methods
|
# 4. Private Methods
|
||||||
func _move(_delta: float) -> void:
|
func _move(_delta: float) -> void:
|
||||||
var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||||
velocity = dir * move_speed
|
velocity = dir * move_speed
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
```
|
||||||
|
## 10. 🔄 Plan Mode Protocol (MANDATORY)
|
||||||
|
- **Planning Phase**:
|
||||||
|
- Whenever using **Plan Mode** to outline a task, a TODO list MUST be outputted to `docs/ai_docs/plan/[feature_name].md`.
|
||||||
|
- **Execution & Reporting**:
|
||||||
|
- Every time a TODO item is completed, the corresponding `.md` document MUST be updated.
|
||||||
|
- After updating the document, report to the user with the following:
|
||||||
|
1. **Completed Items**: What was just finished.
|
||||||
|
2. **User Acceptance Rules**: Instructions on how the user can test/verify the current progress.
|
||||||
|
3. **Next Step**: The next TODO item to be tackled.
|
||||||
|
- **Strict Confirmation**:
|
||||||
|
- After reporting progress, **Claude MUST stop and wait**.
|
||||||
|
- Do NOT proceed to the next TODO until the user has replied with confirmation/approval.
|
||||||
2495
docs/api-documentation.md
Normal file
@@ -3,7 +3,6 @@
|
|||||||
name="Web"
|
name="Web"
|
||||||
platform="Web"
|
platform="Web"
|
||||||
runnable=true
|
runnable=true
|
||||||
advanced_options=false
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
@@ -11,6 +10,11 @@ include_filter=""
|
|||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="web_assets/index.html"
|
export_path="web_assets/index.html"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
seed=0
|
seed=0
|
||||||
@@ -40,6 +44,6 @@ progressive_web_app/orientation=0
|
|||||||
progressive_web_app/icon_144x144="uid://bwy5r7soxi76a"
|
progressive_web_app/icon_144x144="uid://bwy5r7soxi76a"
|
||||||
progressive_web_app/icon_180x180="uid://drpllpsjdiaex"
|
progressive_web_app/icon_180x180="uid://drpllpsjdiaex"
|
||||||
progressive_web_app/icon_512x512="uid://dt817lem3dwee"
|
progressive_web_app/icon_512x512="uid://dt817lem3dwee"
|
||||||
progressive_web_app/background_color=Color(0.19215687, 0.42352942, 1, 1)
|
progressive_web_app/background_color=Color(0.07450981, 0.28627452, 1, 1)
|
||||||
threads/emscripten_pool_size=8
|
threads/emscripten_pool_size=8
|
||||||
threads/godot_pool_size=4
|
threads/godot_pool_size=4
|
||||||
|
|||||||