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

244 lines
7.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.
# safebox_storage_keeper_system.gd —— 40250 官方仓库保管员金条熔铸与密码保险箱系统 1:1
# 严格对照:
# metin2/src/server/game/src/safebox.h, safebox.cpp
# metin2/src/server/game/src/char.cpp:5569-5605 (ReqSafeboxLoad)
# metin2/src/server/game/src/input_db.cpp:1225-1250 (SafeboxWrongPassword / SafeboxChangePasswordAnswer)
# metin2/src/server/db/src/ClientManager.cpp:515-535 (RESULT_SAFEBOX_LOAD: default "000000")
# metin2/src/server/share/conf/item_proto.txt (80003~80007 金银条)
extends RefCounted
const SAFEBOX_MAX_NUM := 45 # 官方单页保险箱格数 (45 格)
const SAFEBOX_PASSWORD_MAX_LEN := 6 # 密码最长 6 位
const DEFAULT_PASSWORD := "000000" # 官方默认初始密码
const SAFEBOX_OPEN_DISTANCE := 1000 # 距离 NPC 9005 最大距离 1000
const SAFEBOX_COOLDOWN_SEC := 10 # 开箱冷却 10 秒 (PASSES_PER_SEC(10))
# 金银条规格表 (Vnum -> { "name": String, "face_value": int, "smelt_cost": int })
# 官方 1% 熔铸税 (cost = face_value * 1.01)
const GOLD_BAR_SPECS := {
80003: { "name": "银条 (5万)", "face_value": 50000, "smelt_cost": 50500 },
80004: { "name": "银条 (10万)", "face_value": 100000, "smelt_cost": 101000 },
80005: { "name": "金条 (50万)", "face_value": 500000, "smelt_cost": 505000 },
80006: { "name": "金条 (100万)", "face_value": 1000000, "smelt_cost": 1010000 },
80007: { "name": "金条 (200万)", "face_value": 2000000, "smelt_cost": 2020000 },
}
var password: String = DEFAULT_PASSWORD
var is_open: bool = false
var last_open_time: int = -100
var current_time: int = 1000
# 保险箱 45 格网格 (slot 0..44 -> Dictionary or null)
var safebox_grid: Array = []
func _init() -> void:
password = DEFAULT_PASSWORD
is_open = false
last_open_time = -100
current_time = 1000
safebox_grid.clear()
for i in range(SAFEBOX_MAX_NUM):
safebox_grid.append(null)
# 1. 密码校验并打开保险箱 (1:1 CHARACTER::ReqSafeboxLoad)
func open_safebox(entered_pw: String, npc_distance: float = 200.0) -> Dictionary:
var res := {
"success": false,
"error_code": "OK",
"message": ""
}
# 距离检查
if npc_distance > SAFEBOX_OPEN_DISTANCE:
res["error_code"] = "TOO_FAR"
res["message"] = "<仓库> 距离太远,无法打开仓库。"
return res
# 10秒防刷频冷却检查
if (current_time - last_open_time) < SAFEBOX_COOLDOWN_SEC:
res["error_code"] = "COOLDOWN_ACTIVE"
res["message"] = "<仓库> 仓库在 10 秒内只能打开一次。"
return res
# 密码长度检查
if entered_pw.is_empty() or entered_pw.length() > SAFEBOX_PASSWORD_MAX_LEN:
res["error_code"] = "INVALID_PASSWORD_LEN"
res["message"] = "<仓库> 密码格式错误,必须为 6 位字符以内。"
return res
# 密码匹配检查 (ClientManager.cpp:525)
if entered_pw != password:
res["error_code"] = "WRONG_PASSWORD"
res["message"] = "<仓库> 仓库密码错误!"
return res
# 打开成功
is_open = true
last_open_time = current_time
res["success"] = true
res["message"] = "<仓库> 仓库开启成功。"
return res
# 2. 修改仓库密码 (1:1 SafeboxChangePassword)
func change_password(old_pw: String, new_pw: String) -> Dictionary:
var res := {
"success": false,
"error_code": "OK",
"message": ""
}
if not is_open:
res["error_code"] = "SAFEBOX_NOT_OPEN"
res["message"] = "<仓库> 必须先开启仓库方可修改密码。"
return res
if old_pw != password:
res["error_code"] = "OLD_PASSWORD_MISMATCH"
res["message"] = "<仓库> 旧密码不匹配,修改失败。"
return res
if new_pw.is_empty() or new_pw.length() > SAFEBOX_PASSWORD_MAX_LEN:
res["error_code"] = "INVALID_NEW_PASSWORD"
res["message"] = "<仓库> 新密码长度不合法。"
return res
password = new_pw
res["success"] = true
res["message"] = "<仓库> 仓库密码已成功更改。"
return res
# 3. 关闭仓库
func close_safebox() -> void:
is_open = false
# 4. 存入物品 (Inventory -> Safebox)
func deposit_item(safebox_slot: int, item_data: Dictionary) -> Dictionary:
var res := { "success": false, "error_code": "OK", "message": "" }
if not is_open:
res["error_code"] = "NOT_OPEN"
return res
if safebox_slot < 0 or safebox_slot >= SAFEBOX_MAX_NUM:
res["error_code"] = "INVALID_SLOT"
return res
if safebox_grid[safebox_slot] != null:
# 尝试堆叠
var existing: Dictionary = safebox_grid[safebox_slot]
if existing.get("vnum") == item_data.get("vnum") and existing.get("stackable", false):
existing["count"] = existing.get("count", 1) + item_data.get("count", 1)
res["success"] = true
res["message"] = "物品成功堆叠存入仓库。"
return res
else:
res["error_code"] = "SLOT_OCCUPIED"
res["message"] = "目标仓库格位已被占用。"
return res
safebox_grid[safebox_slot] = item_data.duplicate(true)
res["success"] = true
res["message"] = "物品成功存入仓库。"
return res
# 5. 取出物品 (Safebox -> Inventory)
func withdraw_item(safebox_slot: int) -> Dictionary:
var res := { "success": false, "item": null, "error_code": "OK" }
if not is_open:
res["error_code"] = "NOT_OPEN"
return res
if safebox_slot < 0 or safebox_slot >= SAFEBOX_MAX_NUM:
res["error_code"] = "INVALID_SLOT"
return res
if safebox_grid[safebox_slot] == null:
res["error_code"] = "EMPTY_SLOT"
return res
var it: Dictionary = safebox_grid[safebox_slot]
safebox_grid[safebox_slot] = null
res["success"] = true
res["item"] = it
return res
# 6. 保险箱内格位互换/移动
func move_safebox_item(from_slot: int, to_slot: int) -> bool:
if not is_open:
return false
if from_slot < 0 or from_slot >= SAFEBOX_MAX_NUM or to_slot < 0 or to_slot >= SAFEBOX_MAX_NUM:
return false
if safebox_grid[from_slot] == null:
return false
var item_a = safebox_grid[from_slot]
var item_b = safebox_grid[to_slot]
if item_b != null and item_b.get("vnum") == item_a.get("vnum") and item_a.get("stackable", false):
item_b["count"] = item_b.get("count", 1) + item_a.get("count", 1)
safebox_grid[from_slot] = null
else:
safebox_grid[to_slot] = item_a
safebox_grid[from_slot] = item_b
return true
# 7. 金条/银条熔铸 (Player Gold -> Gold Bar Item)
# 仓库保管员按 1% 手续费收取金币并铸造成可储存/交易金条
func smelt_gold_bar(player_gold: int, bar_vnum: int) -> Dictionary:
var res := {
"success": false,
"new_gold": player_gold,
"bar_item": null,
"cost": 0,
"error_code": "OK",
"message": ""
}
if not GOLD_BAR_SPECS.has(bar_vnum):
res["error_code"] = "INVALID_BAR_VNUM"
res["message"] = "未知的金银条规格。"
return res
var spec: Dictionary = GOLD_BAR_SPECS[bar_vnum]
var cost: int = spec["smelt_cost"]
res["cost"] = cost
if player_gold < cost:
res["error_code"] = "INSUFFICIENT_GOLD"
res["message"] = "金币不足,铸造 %s 需支付 %d 金币 (含1%%熔铸税)。" % [spec["name"], cost]
return res
res["new_gold"] = player_gold - cost
res["bar_item"] = {
"vnum": bar_vnum,
"name": spec["name"],
"count": 1,
"face_value": spec["face_value"]
}
res["success"] = true
res["message"] = "成功熔铸 %s!扣除 %d 金币。" % [spec["name"], cost]
return res
# 8. 金条/银条承兑支取现金 (Gold Bar Item -> Player Gold)
# 承兑取回票面全额金币 (1:1 face_value)
func encash_gold_bar(player_gold: int, bar_vnum: int) -> Dictionary:
var res := {
"success": false,
"new_gold": player_gold,
"cash_value": 0,
"error_code": "OK",
"message": ""
}
if not GOLD_BAR_SPECS.has(bar_vnum):
res["error_code"] = "INVALID_BAR_VNUM"
res["message"] = "非官方认证金银条,无法承兑。"
return res
var spec: Dictionary = GOLD_BAR_SPECS[bar_vnum]
var face_val: int = spec["face_value"]
res["cash_value"] = face_val
res["new_gold"] = player_gold + face_val
res["success"] = true
res["message"] = "成功承兑 %s,兑现出金币 %d" % [spec["name"], face_val]
return res