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

294 lines
8.3 KiB
GDScript

# cube_crafting_system.gd —— Metin2 40250 官方魔方炼金合成台系统 1:1
# 100% 对照 40250 服务端 cube.cpp, cube.h, cube.txt, quest/cube.quest
class_name CubeCraftingSystem
extends RefCounted
signal cube_opened(npc_vnum: int, npc_name: String)
signal cube_closed()
signal craft_succeeded(reward_vnum: int, count: int, reward_name: String)
signal craft_failed(recipe_name: String)
signal cube_error(reason: String)
# 40250 官方四大魔方炼金 NPC
const NPC_BAEK_GO := 20018 # 药剂师 白高 (草药汁提炼)
const NPC_YU_HWAN := 20017 # 游侠助手 刘焕 (中级药水配制)
const NPC_HUAHN_SO := 20022 # 隐者 幻素 (高级露水炼制)
const NPC_JAE_SEON_KIM:= 20383 # 顶级工匠 金在善 (神武/神甲熔铸)
const NPC_NAMES: Dictionary = {
20018: "药剂师 白高 (Baek-Go)",
20017: "游侠助手 刘焕 (Yu-Hwan)",
20022: "隐者 幻素 (Huahn-So)",
20383: "神级工匠 金在善 (Jae-Seon Kim)"
}
# 40250 经典 cube.txt 配方定义表
const RECIPES: Array[Dictionary] = [
# --- 药剂师 白高 (20018) & 刘焕 (20017) 基础草药汁 ---
{
"id": "herb_peach_flower",
"name": "桃花汁提炼",
"npcs": [20018, 20017],
"materials": [{"vnum": 50721, "count": 1}], # 桃花
"reward": {"vnum": 50801, "count": 1, "name": "桃花汁 (Peach Flower Juice)"},
"gold": 0,
"percent": 100
},
{
"id": "herb_campanula",
"name": "风铃草汁提炼",
"npcs": [20018, 20017],
"materials": [{"vnum": 50722, "count": 1}], # 风铃草
"reward": {"vnum": 50802, "count": 1, "name": "风铃草汁 (Campanula Juice)"},
"gold": 0,
"percent": 100
},
{
"id": "herb_lilac",
"name": "紫丁香汁批量提炼",
"npcs": [20018, 20017],
"materials": [{"vnum": 50723, "count": 1}], # 紫丁香
"reward": {"vnum": 50803, "count": 100, "name": "紫丁香汁 x100"},
"gold": 0,
"percent": 100
},
{
"id": "herb_persimmon",
"name": "柿子花汁批量提炼",
"npcs": [20018, 20017],
"materials": [{"vnum": 50724, "count": 1}], # 柿子花
"reward": {"vnum": 50804, "count": 100, "name": "柿子花汁 x100"},
"gold": 0,
"percent": 100
},
# --- 隐者 幻素 (20022) 露水药水炼制 (Dew Crafting) ---
{
"id": "dew_peach_flower",
"name": "桃花露水炼制",
"npcs": [20022],
"materials": [
{"vnum": 50801, "count": 10}, # 桃花汁 x10
{"vnum": 70031, "count": 10} # 空瓶 (Empty Bottle) x10
],
"reward": {"vnum": 50821, "count": 1, "name": "纯净桃花露 (Peach Flower Dew)"},
"gold": 10000,
"percent": 100
},
{
"id": "dew_campanula",
"name": "风铃草露水炼制",
"npcs": [20022],
"materials": [
{"vnum": 50802, "count": 10}, # 风铃草汁 x10
{"vnum": 70031, "count": 10} # 空瓶 x10
],
"reward": {"vnum": 50822, "count": 1, "name": "纯净风铃露 (Campanula Dew)"},
"gold": 10000,
"percent": 100
},
# --- 工匠 金在善 (20383) 顶级神兵海神剑 (Triton Sword) 熔铸 ---
{
"id": "craft_triton_low",
"name": "海神剑试炼熔铸 (低阶+5)",
"npcs": [20383],
"materials": [
{"vnum": 145, "count": 1}, # 战神之剑+5
{"vnum": 30500, "count": 10},# 能量晶石 x10
{"vnum": 30505, "count": 10} # 熔铸秘石 x10
],
"reward": {"vnum": 460, "count": 1, "name": "海神之剑+0 (Triton Sword)"},
"gold": 100000,
"percent": 5 # 40250 原版 5% 概率
},
{
"id": "craft_triton_master",
"name": "海神剑巅峰熔铸 (满阶+9)",
"npcs": [20383],
"materials": [
{"vnum": 149, "count": 1}, # 战神之剑+9
{"vnum": 30500, "count": 10},# 能量晶石 x10
{"vnum": 30505, "count": 10} # 熔铸秘石 x10
],
"reward": {"vnum": 460, "count": 1, "name": "海神之剑+0 (Triton Sword)"},
"gold": 100000,
"percent": 100 # 40250 原版 +9 保底 100%
}
]
# 当前魔方状态
var current_active_npc: int = 0
var is_cube_open: bool = false
var crafting_history: Array[Dictionary] = []
# 打开魔方窗口
func open_cube(npc_vnum: int) -> bool:
if not NPC_NAMES.has(npc_vnum):
cube_error.emit("INVALID_NPC")
return false
current_active_npc = npc_vnum
is_cube_open = true
cube_opened.emit(npc_vnum, NPC_NAMES[npc_vnum])
return true
# 关闭魔方窗口
func close_cube() -> void:
current_active_npc = 0
is_cube_open = false
cube_closed.emit()
# 根据放入槽位的材料与当前 NPC 匹配配方 (对照 cube.cpp: FN_find_cube)
func find_matching_recipe(cube_slots: Array, npc_vnum: int) -> Dictionary:
# 统计放入槽位的物品 Vnum 与数量
var inputs: Dictionary = {}
for it in cube_slots:
if it != null:
var v = int(it.get("vnum", 0))
var c = int(it.get("count", 1))
inputs[v] = inputs.get(v, 0) + c
if inputs.is_empty():
return {}
for rec in RECIPES:
var npcs: Array = rec["npcs"]
if not npcs.has(npc_vnum):
continue
var req_materials: Array = rec["materials"]
if req_materials.size() != inputs.size():
continue
var matched := true
for mat in req_materials:
var mv = int(mat["vnum"])
var mc = int(mat["count"])
if inputs.get(mv, -1) != mc:
matched = false
break
if matched:
return rec
return {}
# 执行魔方制造 (对照 cube.cpp: Cube_make)
func craft_item(
cube_slots: Array,
inventory: Array,
player_gold: int,
forced_roll: int = -1
) -> Dictionary:
if not is_cube_open:
cube_error.emit("CUBE_NOT_OPEN")
return {"ok": false, "reason": "CUBE_NOT_OPEN", "msg": "制造窗口尚未打开!"}
var recipe = find_matching_recipe(cube_slots, current_active_npc)
if recipe.is_empty():
cube_error.emit("NO_MATCHING_RECIPE")
return {"ok": false, "reason": "NO_MATCHING_RECIPE", "msg": "材料不足或配方不存在!"}
var fee = int(recipe.get("gold", 0))
if player_gold < fee:
cube_error.emit("NOT_ENOUGH_GOLD")
return {
"ok": false,
"reason": "NOT_ENOUGH_GOLD",
"msg": "金币不足!本次合成需要 %d 金币,你只有 %d 金币。" % [fee, player_gold]
}
# 扣除材料 (清空魔方放置槽)
for i in range(cube_slots.size()):
cube_slots[i] = null
# 扣除手续费金币
var remaining_gold = player_gold - fee
# 判定成功概率
var pct = int(recipe.get("percent", 100))
var roll = forced_roll if forced_roll > 0 else randi_range(1, 100)
if roll <= pct:
# 合成成功 (cube success)
var rew = recipe["reward"]
var reward_vnum = int(rew["vnum"])
var reward_count = int(rew.get("count", 1))
var reward_name = str(rew.get("name", "制造道具"))
_give_or_stack_item(inventory, reward_vnum, reward_count)
var record = {
"recipe_id": recipe["id"],
"success": true,
"reward_vnum": reward_vnum,
"reward_count": reward_count,
"fee": fee
}
crafting_history.append(record)
craft_succeeded.emit(reward_vnum, reward_count, reward_name)
return {
"ok": true,
"success": true,
"reward_vnum": reward_vnum,
"reward_count": reward_count,
"remaining_gold": remaining_gold,
"msg": "叮!伴随着绚烂的灵光,你成功制造出了【%s】!" % reward_name
}
else:
# 合成失败 (cube fail)
var record = {
"recipe_id": recipe["id"],
"success": false,
"fee": fee
}
crafting_history.append(record)
craft_failed.emit(recipe["name"])
return {
"ok": true,
"success": false,
"remaining_gold": remaining_gold,
"msg": "制造失败了!魔方中传出一阵焦黑的青烟,材料化为了灰烬。"
}
# 获取当前 NPC 允许合成的所有配方清单
func get_available_recipes(npc_vnum: int) -> Array[Dictionary]:
var list: Array[Dictionary] = []
for rec in RECIPES:
if rec["npcs"].has(npc_vnum):
list.append(rec)
return list
# 历史记录
func get_history() -> Array[Dictionary]:
return crafting_history
func clear_history() -> void:
crafting_history.clear()
# 内部发放道具堆叠
func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void:
for it in inventory:
if it != null and int(it.get("vnum", 0)) == vnum:
it["count"] = int(it.get("count", 1)) + count
return
for i in range(inventory.size()):
if inventory[i] == null:
inventory[i] = {"vnum": vnum, "count": count}
return
inventory.append({"vnum": vnum, "count": count})
# 序列化与反序列化
func serialize() -> Dictionary:
return {
"history": crafting_history.duplicate(true)
}
func deserialize(data: Dictionary) -> void:
crafting_history.clear()
var raw = data.get("history", [])
for it in raw:
crafting_history.append(it.duplicate(true))