Files
mtgodot-poc/project/intro_select_phase.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

131 lines
4.2 KiB
GDScript

# intro_select_phase.gd —— 40250 3D 角色选择大厅系统 1:1
# 严格对照:
# Client/Eternexus/root/introselect.py (SelectCharacterWindow)
# ClientVS22/source/UserInterface/InstanceBaseMotion.cpp (MOTION_INTRO_SELECTED)
# metin2/src/server/game/src/char.cpp
extends RefCounted
const SLOT_COUNT := 4 # 官方 4 槽位
const SLOT_ROTATION := [135.0, 225.0, 315.0, 45.0] # 官方 4 槽位旋转视角 (度数)
const JOB_WARRIOR := 0
const JOB_ASSASSIN := 1
const JOB_SURA := 2
const JOB_SHAMAN := 3
const JOB_NAMES := {
JOB_WARRIOR: "战士 (Warrior)",
JOB_ASSASSIN: "刺客 (Assassin)",
JOB_SURA: "修罗 (Sura)",
JOB_SHAMAN: "萨满 (Shaman)",
}
# 4 个角色槽位存储 (slot 0..3 -> CharacterData or null)
var character_slots: Array = [null, null, null, null]
var selected_slot_index: int = 0
var is_character_stepped_forward: bool = false
func _init() -> void:
character_slots = [null, null, null, null]
selected_slot_index = 0
is_character_stepped_forward = false
# 1. 注册/载入角色槽位数据
func set_character_slot(slot_idx: int, char_data: Dictionary) -> bool:
if slot_idx < 0 or slot_idx >= SLOT_COUNT:
return false
character_slots[slot_idx] = char_data.duplicate(true)
return true
# 2. 选中角色槽位与踏步迈出动画 (1:1 SelectCharacterWindow.SelectSlot)
func select_slot(slot_idx: int) -> Dictionary:
var res := {
"success": false,
"slot_idx": slot_idx,
"is_empty_slot": true,
"character": null,
"rotation_deg": 0.0,
"action": ""
}
if slot_idx < 0 or slot_idx >= SLOT_COUNT:
return res
selected_slot_index = slot_idx
res["rotation_deg"] = SLOT_ROTATION[slot_idx]
if character_slots[slot_idx] == null:
is_character_stepped_forward = false
res["is_empty_slot"] = true
res["action"] = "PROMPT_CREATE"
else:
is_character_stepped_forward = true # 触发 MOTION_INTRO_SELECTED 迈步出列动画
res["is_empty_slot"] = false
res["character"] = character_slots[slot_idx]
res["action"] = "READY_TO_START"
res["success"] = true
return res
# 3. 开始游戏进入世界 (1:1 SelectCharacterWindow.StartGame)
func start_game() -> Dictionary:
var res := { "success": false, "character": null, "error_code": "OK", "message": "" }
if character_slots[selected_slot_index] == null:
res["error_code"] = "EMPTY_SLOT"
res["message"] = "当前选中槽位为空,无法开始游戏。"
return res
res["success"] = true
res["character"] = character_slots[selected_slot_index]
res["message"] = "进入游戏世界..."
return res
# 4. 删除角色与 7 位安全防删 PIN 码校验 (1:1 SelectCharacterWindow.DeleteCharacter)
func delete_character(slot_idx: int, delete_pin_code: String) -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
if slot_idx < 0 or slot_idx >= SLOT_COUNT or character_slots[slot_idx] == null:
res["error_code"] = "CHARACTER_NOT_FOUND"
res["message"] = "未找到待删除的角色。"
return res
# 官方安全码格式验证 (必须为 7 位字符)
if delete_pin_code.length() != 7:
res["error_code"] = "INVALID_PIN_LENGTH"
res["message"] = "防误删安全码必须为 7 位数字。"
return res
var target_char: Dictionary = character_slots[slot_idx]
var expected_pin: String = str(target_char.get("delete_code", "1234567"))
if delete_pin_code != expected_pin:
res["error_code"] = "WRONG_DELETE_PIN"
res["message"] = "安全码校验失败,删除角色请求被拒绝。"
return res
# 删除成功
var char_name: String = target_char.get("name", "Unknown")
character_slots[slot_idx] = null
if selected_slot_index == slot_idx:
is_character_stepped_forward = false
res["success"] = true
res["message"] = "角色 %s 已永久删除。" % char_name
return res
# 5. 获取当前选中槽位的角色卡片详情
func get_selected_character_card() -> Dictionary:
if character_slots[selected_slot_index] == null:
return {}
var c: Dictionary = character_slots[selected_slot_index]
var job_idx: int = c.get("job", 0)
return {
"name": c.get("name", ""),
"level": c.get("level", 1),
"job_name": JOB_NAMES.get(job_idx, "未知职业"),
"horse_level": c.get("horse_level", 0),
"playtime_min": c.get("playtime_min", 0),
"stats": c.get("stats", { "con": 4, "int": 3, "str": 6, "dex": 3 })
}