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

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)