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