- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
131 lines
4.2 KiB
GDScript
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 })
|
|
}
|