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

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