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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# gm_toolkit_system.gd —— Metin2 40250 单人单机内置 GM 控制台与命令解析器 1:1
|
||||
# 对照 40250 服务端 cmd_gm.cpp, cmd.cpp
|
||||
class_name GmToolkitSystem
|
||||
extends RefCounted
|
||||
|
||||
signal command_executed(cmd: String, success: bool, result_msg: String)
|
||||
signal player_warped(target_pos: Vector3, map_name: String)
|
||||
signal mob_spawned(vnum: int, count: int, pos: Vector3)
|
||||
|
||||
const MAP_PRESETS: Dictionary = {
|
||||
"village": Vector3(0.0, 0.0, 0.0),
|
||||
"sohan": Vector3(400.0, 0.0, 600.0), # 冰山 Mount Sohan
|
||||
"desert": Vector3(200.0, 0.0, 300.0), # 永壁沙漠 Yongbi Desert
|
||||
"temple": Vector3(100.0, 0.0, 500.0), # 黄龙神殿 Hwang Temple
|
||||
"flame": Vector3(600.0, 0.0, 200.0), # 烈焰原野 Doyyumhwan
|
||||
"catacomb": Vector3(500.0, 0.0, 500.0) # 地下墓穴 Devil's Catacomb
|
||||
}
|
||||
|
||||
# 解析并执行 GM 斜杠指令
|
||||
func execute_command(cmd_string: String, ctx: Dictionary) -> Dictionary:
|
||||
var trimmed := cmd_string.strip_edges()
|
||||
if not trimmed.begins_with("/"):
|
||||
return {"ok": false, "reason": "NOT_A_COMMAND", "msg": "指令必须以 '/' 开头!"}
|
||||
|
||||
var parts := trimmed.substr(1).split(" ", false)
|
||||
if parts.is_empty():
|
||||
return {"ok": false, "reason": "EMPTY_COMMAND"}
|
||||
|
||||
var action := parts[0].to_lower()
|
||||
var res: Dictionary = {}
|
||||
|
||||
match action:
|
||||
"item":
|
||||
res = _handle_item(parts, ctx)
|
||||
"level":
|
||||
res = _handle_level(parts, ctx)
|
||||
"gold":
|
||||
res = _handle_gold(parts, ctx)
|
||||
"set_skill", "skill":
|
||||
res = _handle_skill(parts, ctx)
|
||||
"warp", "map":
|
||||
res = _handle_warp(parts, ctx)
|
||||
"spawn_mob", "mob", "m":
|
||||
res = _handle_mob(parts, ctx)
|
||||
"help":
|
||||
res = _handle_help()
|
||||
_:
|
||||
res = {"ok": false, "reason": "UNKNOWN_COMMAND", "msg": "未知的 GM 指令【%s】!输入 /help 查看支持的指令列表。" % action}
|
||||
|
||||
command_executed.emit(action, res.get("ok", false), res.get("msg", ""))
|
||||
return res
|
||||
|
||||
# /item <vnum> [count]
|
||||
func _handle_item(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
if parts.size() < 2 or not parts[1].is_valid_int():
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /item <物品代码vnum> [数量count]"}
|
||||
|
||||
var vnum: int = int(parts[1])
|
||||
var count: int = 1
|
||||
if parts.size() >= 3 and parts[2].is_valid_int():
|
||||
count = maxi(1, int(parts[2]))
|
||||
|
||||
var inventory: Array = ctx.get("inventory", [])
|
||||
var empty_slot := -1
|
||||
for i in range(inventory.size()):
|
||||
if inventory[i] == null:
|
||||
empty_slot = i
|
||||
break
|
||||
|
||||
if empty_slot == -1:
|
||||
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法生成物品!"}
|
||||
|
||||
inventory[empty_slot] = {
|
||||
"vnum": vnum,
|
||||
"name": "道具_%d" % vnum,
|
||||
"count": count
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"vnum": vnum,
|
||||
"count": count,
|
||||
"slot": empty_slot,
|
||||
"msg": "【GM】已成功生成物品 [vnum: %d, count: %d] 至背包格子 %d!" % [vnum, count, empty_slot]
|
||||
}
|
||||
|
||||
# /level <1-105>
|
||||
func _handle_level(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
if parts.size() < 2 or not parts[1].is_valid_int():
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /level <1-105>"}
|
||||
|
||||
var target_lv: int = clampi(int(parts[1]), 1, 105)
|
||||
var player_data: Dictionary = ctx.get("player_data", {})
|
||||
player_data["level"] = target_lv
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"level": target_lv,
|
||||
"msg": "【GM】角色等级已修改为 Lv %d!" % target_lv
|
||||
}
|
||||
|
||||
# /gold <amount>
|
||||
func _handle_gold(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
if parts.size() < 2 or not parts[1].is_valid_int():
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /gold <金币数额>"}
|
||||
|
||||
var amount: int = int(parts[1])
|
||||
var player_data: Dictionary = ctx.get("player_data", {})
|
||||
var current: int = int(player_data.get("gold", 0))
|
||||
var new_gold: int = maxi(0, current + amount)
|
||||
player_data["gold"] = new_gold
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"added": amount,
|
||||
"total_gold": new_gold,
|
||||
"msg": "【GM】金币已注入!当前金币:%d Yang。" % new_gold
|
||||
}
|
||||
|
||||
# /set_skill <skill_id> <1-40>
|
||||
func _handle_skill(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
if parts.size() < 3 or not parts[1].is_valid_int() or not parts[2].is_valid_int():
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /set_skill <技能ID> <等级1-40> (40=P级)"}
|
||||
|
||||
var skill_id: int = int(parts[1])
|
||||
var skill_lv: int = clampi(int(parts[2]), 1, 40)
|
||||
var skills: Dictionary = ctx.get("skills", {})
|
||||
skills[skill_id] = skill_lv
|
||||
|
||||
var grade_str := "普通"
|
||||
if skill_lv >= 40:
|
||||
grade_str = "P级 (Perfect Master)"
|
||||
elif skill_lv >= 30:
|
||||
grade_str = "G级 (Grand Master)"
|
||||
elif skill_lv >= 20:
|
||||
grade_str = "M级 (Master)"
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"skill_id": skill_id,
|
||||
"skill_level": skill_lv,
|
||||
"grade": grade_str,
|
||||
"msg": "【GM】技能 [%d] 等级已调整为 %d (%s)!" % [skill_id, skill_lv, grade_str]
|
||||
}
|
||||
|
||||
# /warp <x> <y> 或 /map <preset_name>
|
||||
func _handle_warp(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
var target_pos := Vector3.ZERO
|
||||
var map_name := "custom"
|
||||
|
||||
if parts.size() >= 3 and parts[1].is_valid_float() and parts[2].is_valid_float():
|
||||
target_pos = Vector3(float(parts[1]), 0.0, float(parts[2]))
|
||||
elif parts.size() >= 2:
|
||||
var preset_key: String = parts[1].to_lower()
|
||||
if MAP_PRESETS.has(preset_key):
|
||||
target_pos = MAP_PRESETS[preset_key]
|
||||
map_name = preset_key
|
||||
else:
|
||||
return {"ok": false, "reason": "UNKNOWN_MAP", "msg": "未知的预设地图!可用地图: %s" % str(MAP_PRESETS.keys())}
|
||||
else:
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /warp <x> <y> 或 /map <地图名>"}
|
||||
|
||||
player_warped.emit(target_pos, map_name)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"position": target_pos,
|
||||
"map_name": map_name,
|
||||
"msg": "【GM】已瞬移传送至目标坐标 (%.1f, %.1f) [%s]!" % [target_pos.x, target_pos.z, map_name]
|
||||
}
|
||||
|
||||
# /spawn_mob <vnum> [count]
|
||||
func _handle_mob(parts: PackedStringArray, ctx: Dictionary) -> Dictionary:
|
||||
if parts.size() < 2 or not parts[1].is_valid_int():
|
||||
return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /spawn_mob <怪代码vnum> [数量count]"}
|
||||
|
||||
var mob_vnum: int = int(parts[1])
|
||||
var count: int = 1
|
||||
if parts.size() >= 3 and parts[2].is_valid_int():
|
||||
count = maxi(1, int(parts[2]))
|
||||
|
||||
var player_pos: Vector3 = ctx.get("player_pos", Vector3.ZERO)
|
||||
mob_spawned.emit(mob_vnum, count, player_pos)
|
||||
|
||||
return {
|
||||
"ok": true,
|
||||
"mob_vnum": mob_vnum,
|
||||
"count": count,
|
||||
"spawn_pos": player_pos,
|
||||
"msg": "【GM】已在身旁召唤怪物 [vnum: %d, count: %d]!" % [mob_vnum, count]
|
||||
}
|
||||
|
||||
# /help
|
||||
func _handle_help() -> Dictionary:
|
||||
return {
|
||||
"ok": true,
|
||||
"commands": [
|
||||
"/item <vnum> [count] - 生成物品",
|
||||
"/level <1-105> - 设定等级",
|
||||
"/gold <amount> - 注入金币",
|
||||
"/set_skill <id> <1-40> - 设定技能等级(40=P级)",
|
||||
"/map <village|sohan|desert|temple|flame|catacomb> - 传送至指定地图",
|
||||
"/warp <x> <y> - 坐标瞬移",
|
||||
"/spawn_mob <vnum> [count] - 刷怪/召唤首领"
|
||||
],
|
||||
"msg": "【GM指令帮助】输入对应命令快速调试单机环境!"
|
||||
}
|
||||
Reference in New Issue
Block a user