- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
208 lines
6.8 KiB
GDScript
208 lines
6.8 KiB
GDScript
# 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指令帮助】输入对应命令快速调试单机环境!"
|
||
}
|