- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
165 lines
5.8 KiB
GDScript
165 lines
5.8 KiB
GDScript
# inventory_expansion_system.gd —— 40250 官方背包槽位解锁凭证与扩展系统 1:1
|
|
# 严格对照:
|
|
# metin2/src/server/common/length.h (INVENTORY_MAX_NUM = 90, 45格/页)
|
|
# metin2/src/server/share/conf/item_proto.txt (72319 / 72320 储物锁匙)
|
|
# ClientVS22/source/UserInterface/GameType.h (INVENTORY_PAGE_SIZE = 45, 5x9 grid)
|
|
extends RefCounted
|
|
|
|
const PAGE_SIZE := 45 # 每页 45 格 (5 列 x 9 行)
|
|
const BASE_UNLOCKED_PAGES := 2 # 默认前 2 页解锁 (0..89 共 90 格)
|
|
const MAX_PAGES := 4 # 最大 4 页 (共 180 格)
|
|
const TOTAL_SLOTS := 180 # 45 * 4 = 180
|
|
|
|
const SLOTS_PER_STAGE := 5 # 每个解锁阶段解锁 5 格
|
|
const TOTAL_EXPANSION_STAGES := 18 # 后 2 页共 90 格,划分为 18 个阶段 (90 / 5 = 18)
|
|
|
|
const EXPANSION_KEY_VNUM := 72319 # 储物锁匙
|
|
|
|
# 18 个阶段的锁匙消耗曲线 (1:1 官方阶梯消耗)
|
|
# 阶段 1~6 (90..119 格): 1 匙/段
|
|
# 阶段 7~12 (120..149 格): 2 匙/段
|
|
# 阶段 13~17 (150..174 格): 3 匙/段
|
|
# 阶段 18 (175..179 格): 4 匙/段
|
|
const STAGE_KEY_COSTS := [
|
|
1, 1, 1, 1, 1, 1, # Stage 1..6
|
|
2, 2, 2, 2, 2, 2, # Stage 7..12
|
|
3, 3, 3, 3, 3, # Stage 13..17
|
|
4 # Stage 18
|
|
]
|
|
|
|
# 当前已解锁的扩展阶段数 (0..18)
|
|
var unlocked_stages: int = 0
|
|
# 背包槽位存储 (slot 0..179 -> item_data Dictionary or null)
|
|
var inventory_slots: Array = []
|
|
|
|
func _init(initial_unlocked_stages: int = 0) -> void:
|
|
unlocked_stages = clamp(initial_unlocked_stages, 0, TOTAL_EXPANSION_STAGES)
|
|
inventory_slots.clear()
|
|
for i in range(TOTAL_SLOTS):
|
|
inventory_slots.append(null)
|
|
|
|
# 获取当前已解锁的最大格位数 (基础 90 格 + 解锁阶段数 * 5)
|
|
func get_unlocked_slot_count() -> int:
|
|
return (BASE_UNLOCKED_PAGES * PAGE_SIZE) + (unlocked_stages * SLOTS_PER_STAGE)
|
|
|
|
# 检查特定格位是否已解锁
|
|
func is_slot_unlocked(slot_idx: int) -> bool:
|
|
if slot_idx < 0 or slot_idx >= TOTAL_SLOTS:
|
|
return false
|
|
return slot_idx < get_unlocked_slot_count()
|
|
|
|
# 获取下一阶段所需的储物锁匙数量
|
|
func get_next_stage_key_cost() -> int:
|
|
if unlocked_stages >= TOTAL_EXPANSION_STAGES:
|
|
return 0
|
|
return STAGE_KEY_COSTS[unlocked_stages]
|
|
|
|
# 解锁下一阶段背包槽位 (5 格)
|
|
func unlock_next_stage(player_keys: int) -> Dictionary:
|
|
var res := {
|
|
"success": false,
|
|
"stage": unlocked_stages,
|
|
"keys_deducted": 0,
|
|
"remaining_keys": player_keys,
|
|
"unlocked_slots": get_unlocked_slot_count(),
|
|
"is_all_unlocked": false,
|
|
"error_code": "OK",
|
|
"message": ""
|
|
}
|
|
|
|
if unlocked_stages >= TOTAL_EXPANSION_STAGES:
|
|
res["is_all_unlocked"] = true
|
|
res["error_code"] = "ALREADY_MAX_EXPANDED"
|
|
res["message"] = "背包所有 4 页 180 格位已全部解锁,达到上限!"
|
|
return res
|
|
|
|
var needed_keys := get_next_stage_key_cost()
|
|
if player_keys < needed_keys:
|
|
res["error_code"] = "INSUFFICIENT_KEYS"
|
|
res["message"] = "储物锁匙不足!解锁阶段 %d 需要 %d 枚储物锁匙。" % [unlocked_stages + 1, needed_keys]
|
|
return res
|
|
|
|
unlocked_stages += 1
|
|
res["success"] = true
|
|
res["stage"] = unlocked_stages
|
|
res["keys_deducted"] = needed_keys
|
|
res["remaining_keys"] = player_keys - needed_keys
|
|
res["unlocked_slots"] = get_unlocked_slot_count()
|
|
res["is_all_unlocked"] = (unlocked_stages >= TOTAL_EXPANSION_STAGES)
|
|
res["message"] = "解锁成功!阶段 %d 已解除封印,新增 5 个可用格位 (当前可用格数: %d)。" % [
|
|
unlocked_stages, res["unlocked_slots"]
|
|
]
|
|
return res
|
|
|
|
# 检查物品是否能放置在指定格位 (考虑物品高度 height 与跨页/封印限制)
|
|
# 在 Metin2 中,背包每行 5 格,高度占据 slot, slot+5, slot+10
|
|
func can_place_item_at(slot_idx: int, item_height: int = 1) -> Dictionary:
|
|
var res := { "can_place": false, "error_code": "OK", "message": "" }
|
|
|
|
if slot_idx < 0 or slot_idx >= TOTAL_SLOTS:
|
|
res["error_code"] = "OUT_OF_BOUNDS"
|
|
res["message"] = "超出背包格位边界。"
|
|
return res
|
|
|
|
var start_page := slot_idx / PAGE_SIZE
|
|
var page_local_slot := slot_idx % PAGE_SIZE
|
|
var row := page_local_slot / 5
|
|
|
|
# 跨页检查 (单物品不能从第 1 页跨到底部溢出到第 2 页)
|
|
if row + item_height > 9:
|
|
res["error_code"] = "PAGE_OVERFLOW"
|
|
res["message"] = "物品长度超出单页底部边界。"
|
|
return res
|
|
|
|
# 检查高度占用的每一个单元格
|
|
for h in range(item_height):
|
|
var check_slot := slot_idx + (h * 5)
|
|
if not is_slot_unlocked(check_slot):
|
|
res["error_code"] = "SLOT_LOCKED"
|
|
res["message"] = "目标单元格 [%d] 处于锁链封印状态,需使用储物锁匙解锁。" % check_slot
|
|
return res
|
|
|
|
if inventory_slots[check_slot] != null:
|
|
res["error_code"] = "SLOT_OCCUPIED"
|
|
res["message"] = "目标单元格已被占用。"
|
|
return res
|
|
|
|
res["can_place"] = true
|
|
return res
|
|
|
|
# 放置物品到背包格位
|
|
func put_item(slot_idx: int, item_data: Dictionary, item_height: int = 1) -> Dictionary:
|
|
var check := can_place_item_at(slot_idx, item_height)
|
|
if not check["can_place"]:
|
|
return check
|
|
|
|
var it_copy := item_data.duplicate(true)
|
|
it_copy["size"] = item_height
|
|
it_copy["head_slot"] = slot_idx
|
|
|
|
for h in range(item_height):
|
|
var cell := slot_idx + (h * 5)
|
|
inventory_slots[cell] = it_copy
|
|
|
|
return { "success": true, "error_code": "OK", "slot": slot_idx }
|
|
|
|
# 从背包移除物品
|
|
func remove_item(slot_idx: int) -> Dictionary:
|
|
var res := { "success": false, "item": null, "error_code": "OK" }
|
|
if slot_idx < 0 or slot_idx >= TOTAL_SLOTS or inventory_slots[slot_idx] == null:
|
|
res["error_code"] = "EMPTY_SLOT"
|
|
return res
|
|
|
|
var it: Dictionary = inventory_slots[slot_idx]
|
|
var head_slot: int = it.get("head_slot", slot_idx)
|
|
var size: int = it.get("size", 1)
|
|
|
|
for h in range(size):
|
|
var cell := head_slot + (h * 5)
|
|
if cell < TOTAL_SLOTS:
|
|
inventory_slots[cell] = null
|
|
|
|
res["success"] = true
|
|
res["item"] = it
|
|
return res
|