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