- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
238 lines
8.1 KiB
GDScript
238 lines
8.1 KiB
GDScript
# energy_system.gd —— Metin2 40250 官方能量碎片提取与能量水晶熔炼系统 1:1
|
|
# 对照 40250 服务端 energy_system.quest, char_item.cpp:6319 (CBuffOnAttributes), char.h (POINT_ENERGY=128)
|
|
class_name EnergySystem
|
|
extends RefCounted
|
|
|
|
signal fragments_extracted(count: int, item_name: String)
|
|
signal crystal_fused(crystal_item: Dictionary)
|
|
signal energy_activated(duration: float, bonus_pct: int)
|
|
signal energy_expired()
|
|
|
|
const NPC_ALCHEMIST := 20001 # 炼金术士
|
|
const MIN_PLAYER_LEVEL := 35 # 角色开启门槛 35 级
|
|
const MIN_EQUIP_LEVEL := 35 # 装备分解门槛 35 级
|
|
|
|
const VNUM_ENERGY_STONE := 51001 # 能量碎片 (Energy Stone / Fragment)
|
|
const VNUM_CHARGING_STONE := 51002 # 能量水晶 (Charging Stone / Energy Crystal)
|
|
|
|
const FUSION_NEED_STONES := 30 # 熔炼需要 30 颗碎片
|
|
const FUSION_COST_GOLD := 1000 # 熔炼手续费 1000 金币
|
|
const ENERGY_DURATION := 7200.0 # 2 小时 (7200 秒)
|
|
const ATTR_BONUS_PCT := 10 # 全身穿戴装备属性增幅 +10%
|
|
|
|
var is_energy_active: bool = false
|
|
var energy_time_remaining: float = 0.0
|
|
var applied_bonuses: Dictionary = {}
|
|
|
|
# 计算装备分解产出碎片数量 (40250 energy_system.quest 概率阶梯)
|
|
static func calculate_fragment_yield(equip_level_limit: int, rand_val: int = -1) -> int:
|
|
var r: int = rand_val if rand_val >= 0 else (randi() % 100)
|
|
if equip_level_limit >= 35 and equip_level_limit <= 50:
|
|
# 35~50 级装备产出 1~5 颗
|
|
return clampi(1 + (r % 5), 1, 5)
|
|
elif equip_level_limit > 50 and equip_level_limit <= 70:
|
|
# 51~70 级装备产出 3~8 颗
|
|
return clampi(3 + (r % 6), 3, 8)
|
|
else:
|
|
# 70 级以上顶级装备产出 5~15 颗
|
|
return clampi(5 + (r % 11), 5, 15)
|
|
|
|
# 在炼金术士处分解淘汰装备提取能量碎片
|
|
func extract_energy_fragments(inventory: Array, slot_index: int, player_level: int) -> Dictionary:
|
|
if player_level < MIN_PLAYER_LEVEL:
|
|
return {"ok": false, "reason": "PLAYER_LEVEL_TOO_LOW", "msg": "炼金术士只为 35 级以上的强者提取能量!"}
|
|
|
|
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var item: Dictionary = inventory[slot_index]
|
|
var limit_lv := int(item.get("level_limit", item.get("limit_level", 0)))
|
|
if limit_lv < MIN_EQUIP_LEVEL:
|
|
return {"ok": false, "reason": "EQUIP_LEVEL_TOO_LOW", "msg": "只能分解 35 级以上的武器、防具或首饰!"}
|
|
|
|
# 检查箭矢等消耗品
|
|
if item.get("type", 0) == 1 and item.get("subtype", 0) == 5: # WEAPON_ARROW
|
|
return {"ok": false, "reason": "ARROW_NOT_ALLOWED", "msg": "箭矢无法提取能量!"}
|
|
|
|
var item_name: String = str(item.get("name", "淘汰装备"))
|
|
var count_yield := calculate_fragment_yield(limit_lv)
|
|
|
|
# 移除装备
|
|
inventory[slot_index] = null
|
|
|
|
# 发放能量碎片 (查找已有堆叠或找空位)
|
|
var placed := false
|
|
for i in range(inventory.size()):
|
|
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ENERGY_STONE:
|
|
var cur_c := int(inventory[i].get("count", 1))
|
|
inventory[i]["count"] = cur_c + count_yield
|
|
placed = true
|
|
break
|
|
|
|
if not placed:
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
inventory[i] = {
|
|
"vnum": VNUM_ENERGY_STONE,
|
|
"name": "能量碎片",
|
|
"count": count_yield
|
|
}
|
|
placed = true
|
|
break
|
|
|
|
fragments_extracted.emit(count_yield, item_name)
|
|
|
|
return {
|
|
"ok": true,
|
|
"yield": count_yield,
|
|
"item_name": item_name,
|
|
"msg": "成功分解【%s】,提取出 %d 颗【能量碎片】!" % [item_name, count_yield]
|
|
}
|
|
|
|
# 熔炼能量水晶 (30 碎片 + 1000 金币)
|
|
func fuse_energy_crystal(inventory: Array, player_gold: Dictionary) -> Dictionary:
|
|
var cur_gold := int(player_gold.get("gold", 0))
|
|
if cur_gold < FUSION_COST_GOLD:
|
|
return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足!熔炼需要 1,000 金币。"}
|
|
|
|
# 统计能量碎片总数
|
|
var total_stones := 0
|
|
var stone_slots: Array = []
|
|
for i in range(inventory.size()):
|
|
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ENERGY_STONE:
|
|
total_stones += int(inventory[i].get("count", 1))
|
|
stone_slots.append(i)
|
|
|
|
if total_stones < FUSION_NEED_STONES:
|
|
return {
|
|
"ok": false,
|
|
"reason": "NOT_ENOUGH_STONES",
|
|
"msg": "能量碎片不足!需要 %d 颗,当前拥有 %d 颗。" % [FUSION_NEED_STONES, total_stones]
|
|
}
|
|
|
|
# 寻找放置能量水晶的槽位
|
|
var crystal_slot := -1
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
crystal_slot = i
|
|
break
|
|
|
|
# 扣除 30 颗碎片
|
|
var need_to_deduct := FUSION_NEED_STONES
|
|
for slot_idx in stone_slots:
|
|
var it: Dictionary = inventory[slot_idx]
|
|
var c := int(it.get("count", 1))
|
|
if c <= need_to_deduct:
|
|
need_to_deduct -= c
|
|
inventory[slot_idx] = null
|
|
if crystal_slot == -1:
|
|
crystal_slot = slot_idx # 复用清空的格子
|
|
else:
|
|
it["count"] = c - need_to_deduct
|
|
need_to_deduct = 0
|
|
break
|
|
|
|
player_gold["gold"] = cur_gold - FUSION_COST_GOLD
|
|
|
|
var crystal := {
|
|
"vnum": VNUM_CHARGING_STONE,
|
|
"name": "能量水晶",
|
|
"count": 1
|
|
}
|
|
inventory[crystal_slot] = crystal
|
|
|
|
crystal_fused.emit(crystal)
|
|
|
|
return {
|
|
"ok": true,
|
|
"crystal": crystal,
|
|
"slot": crystal_slot,
|
|
"msg": "熔炼成功!纯净的【能量水晶】已凝聚成型。"
|
|
}
|
|
|
|
# 使用能量水晶激活 2 小时全身穿戴装备 10% 增幅 (40250 CBuffOnAttributes)
|
|
func use_energy_crystal(inventory: Array, slot_index: int, equipped_items: Array, player_stats: Dictionary) -> Dictionary:
|
|
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var item: Dictionary = inventory[slot_index]
|
|
if int(item.get("vnum", 0)) != VNUM_CHARGING_STONE:
|
|
return {"ok": false, "reason": "NOT_A_CHARGING_STONE", "msg": "此物品不是能量水晶!"}
|
|
|
|
# 消耗 1 颗能量水晶
|
|
var c := int(item.get("count", 1))
|
|
if c > 1:
|
|
item["count"] = c - 1
|
|
else:
|
|
inventory[slot_index] = null
|
|
|
|
# 如果已激活,先清除旧加成
|
|
if is_energy_active:
|
|
_clear_energy_buffs(player_stats)
|
|
|
|
# 计算穿戴装备基础属性总和的 10%
|
|
applied_bonuses.clear()
|
|
for eq in equipped_items:
|
|
if eq == null:
|
|
continue
|
|
var atk = int(eq.get("attack_power", 0))
|
|
var def = int(eq.get("defense", 0))
|
|
var hp = int(eq.get("max_hp", 0))
|
|
if atk > 0:
|
|
applied_bonuses["attack_power"] = int(applied_bonuses.get("attack_power", 0)) + int(ceil(atk * 0.10))
|
|
if def > 0:
|
|
applied_bonuses["defense"] = int(applied_bonuses.get("defense", 0)) + int(ceil(def * 0.10))
|
|
if hp > 0:
|
|
applied_bonuses["max_hp"] = int(applied_bonuses.get("max_hp", 0)) + int(ceil(hp * 0.10))
|
|
|
|
# 保底全属性增益 (即使没有极品装备,也提供保底加成)
|
|
applied_bonuses["attack_power"] = maxi(10, int(applied_bonuses.get("attack_power", 0)))
|
|
applied_bonuses["defense"] = maxi(10, int(applied_bonuses.get("defense", 0)))
|
|
|
|
# 注入玩家属性
|
|
for k in applied_bonuses.keys():
|
|
player_stats[k] = int(player_stats.get(k, 0)) + applied_bonuses[k]
|
|
|
|
is_energy_active = true
|
|
energy_time_remaining = ENERGY_DURATION
|
|
|
|
energy_activated.emit(ENERGY_DURATION, ATTR_BONUS_PCT)
|
|
|
|
return {
|
|
"ok": true,
|
|
"duration": ENERGY_DURATION,
|
|
"bonuses": applied_bonuses,
|
|
"msg": "能量水晶激活!全身装备属性增幅 +10%%,持续 2 小时。"
|
|
}
|
|
|
|
# 清除能量属性
|
|
func _clear_energy_buffs(player_stats: Dictionary) -> void:
|
|
for k in applied_bonuses.keys():
|
|
player_stats[k] = int(player_stats.get(k, 0)) - applied_bonuses[k]
|
|
applied_bonuses.clear()
|
|
is_energy_active = false
|
|
energy_time_remaining = 0.0
|
|
|
|
# 时钟心跳推进
|
|
func update(delta: float, player_stats: Dictionary) -> void:
|
|
if not is_energy_active:
|
|
return
|
|
energy_time_remaining -= delta
|
|
if energy_time_remaining <= 0.0:
|
|
_clear_energy_buffs(player_stats)
|
|
energy_expired.emit()
|
|
|
|
# 序列化
|
|
func serialize() -> Dictionary:
|
|
return {
|
|
"is_energy_active": is_energy_active,
|
|
"energy_time_remaining": energy_time_remaining,
|
|
"applied_bonuses": applied_bonuses.duplicate(true)
|
|
}
|
|
|
|
# 反序列化
|
|
func deserialize(data: Dictionary) -> void:
|
|
is_energy_active = bool(data.get("is_energy_active", false))
|
|
energy_time_remaining = float(data.get("energy_time_remaining", 0.0))
|
|
applied_bonuses = data.get("applied_bonuses", {}).duplicate(true)
|