Files
mtgodot-poc/project/gm_toolkit_system.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

208 lines
6.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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指令帮助】输入对应命令快速调试单机环境!"
}