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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# combat_status_effect_system.gd —— Metin2 40250 官方战斗状态异常与五大负面减益结算系统 1:1
|
||||
# 100% 对照 40250 服务端 battle.cpp, char_resist.cpp, affect.h, char.h (POINT_POISON_PCT, IMMUNE_STUN, IMMUNE_SLOW)
|
||||
class_name CombatStatusEffectSystem
|
||||
extends RefCounted
|
||||
|
||||
signal status_applied(target_id: int, effect_type: String, duration: float)
|
||||
signal status_resisted(target_id: int, effect_type: String, reason: String)
|
||||
signal status_ticked(target_id: int, effect_type: String, damage: int, remaining_ticks: int)
|
||||
signal status_cleared(target_id: int, effect_type: String)
|
||||
|
||||
# 40250 官方核心状态异常类型
|
||||
const EFFECT_POISON := "poison" # 中毒 (AFF_POISON)
|
||||
const EFFECT_STUN := "stun" # 眩晕 (AFF_STUN)
|
||||
const EFFECT_SLOW := "slow" # 减速 (AFF_SLOW)
|
||||
const EFFECT_FIRE := "fire" # 烈焰灼烧 (AFF_FIRE)
|
||||
const EFFECT_BLEEDING := "bleeding" # 流血虚弱 (AFF_BLEEDING)
|
||||
|
||||
# 等级压制中毒调整表 (40250 char_resist.cpp: poison_level_adjust)
|
||||
const POISON_LEVEL_ADJUST: Array[int] = [100, 90, 80, 70, 60, 50, 40, 30, 20]
|
||||
|
||||
# 状态记录表: target_id -> { effect_type -> { "duration": float, "timer": float, "tick_interval": float, "ticks_left": int, "param": int } }
|
||||
var _target_effects: Dictionary = {}
|
||||
|
||||
# ==========================================
|
||||
# 1. 状态异常施加与判定 (battle.cpp: NormalAttackAffect)
|
||||
# ==========================================
|
||||
|
||||
# 尝试施加攻击异常效果 (通用结算)
|
||||
func try_apply_normal_attack_effects(
|
||||
attacker: Dictionary,
|
||||
victim: Dictionary,
|
||||
forced_rolls: Dictionary = {}
|
||||
) -> Dictionary:
|
||||
var victim_id = int(victim.get("id", 0))
|
||||
var applied: Array[String] = []
|
||||
var resisted: Array[String] = []
|
||||
|
||||
var is_attacker_pc = bool(attacker.get("is_pc", true))
|
||||
var is_victim_pc = bool(victim.get("is_pc", true))
|
||||
var attacker_level = int(attacker.get("level", 1))
|
||||
var victim_level = int(victim.get("level", 1))
|
||||
|
||||
# 1. 中毒判定 (POINT_POISON_PCT)
|
||||
var poison_pct = int(attacker.get("poison_pct", 0))
|
||||
if poison_pct > 0 and not has_effect(victim_id, EFFECT_POISON):
|
||||
# 怪物只中毒一次检查 (char_resist.cpp: m_bHasPoisoned && !IsPC())
|
||||
var already_poisoned_once = bool(victim.get("has_been_poisoned", false))
|
||||
if not is_victim_pc and already_poisoned_once:
|
||||
resisted.append("poison_already_poisoned_once")
|
||||
else:
|
||||
var roll = int(forced_rolls.get("poison_pct_roll", randi_range(1, 100)))
|
||||
if roll <= poison_pct:
|
||||
# 等级差距惩罚检查
|
||||
var can_poison := true
|
||||
if attacker_level < victim_level:
|
||||
var delta = min(victim_level - attacker_level, 8)
|
||||
var level_roll = int(forced_rolls.get("poison_level_roll", randi_range(1, 100)))
|
||||
if level_roll > POISON_LEVEL_ADJUST[delta]:
|
||||
can_poison = false
|
||||
resisted.append("poison_level_penalty")
|
||||
|
||||
if can_poison:
|
||||
apply_poison(victim, attacker)
|
||||
applied.append(EFFECT_POISON)
|
||||
|
||||
# 2. 眩晕判定 (POINT_STUN_PCT)
|
||||
var stun_pct = int(attacker.get("stun_pct", 0))
|
||||
if stun_pct > 0 and not has_effect(victim_id, EFFECT_STUN):
|
||||
# 免疫眩晕判定 (IMMUNE_STUN)
|
||||
if bool(victim.get("immune_stun", false)):
|
||||
status_resisted.emit(victim_id, EFFECT_STUN, "IMMUNE_STUN")
|
||||
resisted.append("immune_stun")
|
||||
else:
|
||||
var roll = int(forced_rolls.get("stun_pct_roll", randi_range(1, 100)))
|
||||
if roll <= stun_pct:
|
||||
var stun_dur = 4.0 if (is_attacker_pc and not is_victim_pc) else 2.0
|
||||
apply_stun(victim_id, stun_dur)
|
||||
applied.append(EFFECT_STUN)
|
||||
|
||||
# 3. 减速判定 (POINT_SLOW_PCT)
|
||||
var slow_pct = int(attacker.get("slow_pct", 0))
|
||||
if slow_pct > 0 and not has_effect(victim_id, EFFECT_SLOW):
|
||||
# 免疫减速判定 (IMMUNE_SLOW)
|
||||
if bool(victim.get("immune_slow", false)):
|
||||
status_resisted.emit(victim_id, EFFECT_SLOW, "IMMUNE_SLOW")
|
||||
resisted.append("immune_slow")
|
||||
else:
|
||||
var roll = int(forced_rolls.get("slow_pct_roll", randi_range(1, 100)))
|
||||
if roll <= slow_pct:
|
||||
apply_slow(victim_id, 20.0, 30) # 40250 官方减速 20 秒,移速 -30
|
||||
applied.append(EFFECT_SLOW)
|
||||
|
||||
return {
|
||||
"applied": applied,
|
||||
"resisted": resisted
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# 2. 状态具体应用 (char_resist.cpp)
|
||||
# ==========================================
|
||||
|
||||
# 施加中毒: 10 次跳伤害,每次间隔 3 秒,总持续 30 秒 (POISON_LENGTH + 1)
|
||||
func apply_poison(victim: Dictionary, attacker: Dictionary = {}) -> void:
|
||||
var victim_id = int(victim.get("id", 0))
|
||||
victim["has_been_poisoned"] = true
|
||||
|
||||
_add_raw_effect(victim_id, EFFECT_POISON, {
|
||||
"duration": 30.0,
|
||||
"timer": 3.0,
|
||||
"tick_interval": 3.0,
|
||||
"ticks_left": 10,
|
||||
"param": int(victim.get("poison_reduce", 0)), # 抗毒减免比例 (0~100)
|
||||
"max_hp": int(victim.get("max_hp", 1000)),
|
||||
"attacker_id": int(attacker.get("id", 0))
|
||||
})
|
||||
status_applied.emit(victim_id, EFFECT_POISON, 30.0)
|
||||
|
||||
# 施加眩晕: 期间无法移动和释放技能
|
||||
func apply_stun(victim_id: int, duration: float = 2.0) -> void:
|
||||
_add_raw_effect(victim_id, EFFECT_STUN, {
|
||||
"duration": duration,
|
||||
"timer": duration,
|
||||
"tick_interval": 0.0,
|
||||
"ticks_left": 0,
|
||||
"param": 0
|
||||
})
|
||||
status_applied.emit(victim_id, EFFECT_STUN, duration)
|
||||
|
||||
# 施加减速: 移动速度扣除 speed_reduction 点
|
||||
func apply_slow(victim_id: int, duration: float = 20.0, speed_reduction: int = 30) -> void:
|
||||
_add_raw_effect(victim_id, EFFECT_SLOW, {
|
||||
"duration": duration,
|
||||
"timer": duration,
|
||||
"tick_interval": 0.0,
|
||||
"ticks_left": 0,
|
||||
"param": speed_reduction
|
||||
})
|
||||
status_applied.emit(victim_id, EFFECT_SLOW, duration)
|
||||
|
||||
# 施加烈焰灼烧: 固定火伤每 3 秒跳一次
|
||||
func apply_fire(victim_id: int, ticks: int = 5, damage_per_tick: int = 50) -> void:
|
||||
_add_raw_effect(victim_id, EFFECT_FIRE, {
|
||||
"duration": float(ticks * 3),
|
||||
"timer": 3.0,
|
||||
"tick_interval": 3.0,
|
||||
"ticks_left": ticks,
|
||||
"param": damage_per_tick
|
||||
})
|
||||
status_applied.emit(victim_id, EFFECT_FIRE, float(ticks * 3))
|
||||
|
||||
# 解除特定状态 (例如解毒药水 RemovePoison)
|
||||
func clear_effect(victim_id: int, effect_type: String) -> bool:
|
||||
if not _target_effects.has(victim_id):
|
||||
return false
|
||||
if _target_effects[victim_id].has(effect_type):
|
||||
_target_effects[victim_id].erase(effect_type)
|
||||
status_cleared.emit(victim_id, effect_type)
|
||||
return true
|
||||
return false
|
||||
|
||||
# 检查目标是否具有特定异常状态
|
||||
func has_effect(victim_id: int, effect_type: String) -> bool:
|
||||
if not _target_effects.has(victim_id):
|
||||
return false
|
||||
return _target_effects[victim_id].has(effect_type)
|
||||
|
||||
# 检查目标是否处于无法行动状态 (眩晕)
|
||||
func is_stunned(victim_id: int) -> bool:
|
||||
return has_effect(victim_id, EFFECT_STUN)
|
||||
|
||||
# 获取减速移速惩罚值
|
||||
func get_slow_reduction(victim_id: int) -> int:
|
||||
if not has_effect(victim_id, EFFECT_SLOW):
|
||||
return 0
|
||||
return int(_target_effects[victim_id][EFFECT_SLOW].get("param", 30))
|
||||
|
||||
# 检查自然回血是否被阻断 (40250 char.cpp:2453 中毒状态禁止自然回血)
|
||||
func is_natural_hp_regen_blocked(victim_id: int) -> bool:
|
||||
return has_effect(victim_id, EFFECT_POISON)
|
||||
|
||||
# ==========================================
|
||||
# 3. 状态心跳更新与跳伤害结算 (char_resist.cpp: poison_event, fire_event)
|
||||
# ==========================================
|
||||
func update(delta: float, targets_map: Dictionary = {}) -> void:
|
||||
for vid in _target_effects.keys().duplicate():
|
||||
var effects: Dictionary = _target_effects[vid]
|
||||
var to_remove: Array[String] = []
|
||||
|
||||
for eff_type in effects.keys():
|
||||
var data = effects[eff_type]
|
||||
data["duration"] -= delta
|
||||
|
||||
if data["tick_interval"] > 0.0:
|
||||
# 周期跳伤害逻辑 (中毒/灼烧)
|
||||
data["timer"] -= delta
|
||||
while data["timer"] <= 0.0 and data["ticks_left"] > 0:
|
||||
data["timer"] += data["tick_interval"]
|
||||
data["ticks_left"] -= 1
|
||||
|
||||
var dmg := 0
|
||||
if eff_type == EFFECT_POISON:
|
||||
# 40250 官方: GetMaxHP() * 5 / 100 * (100 - poison_reduce) / 100
|
||||
var max_hp = int(data.get("max_hp", 1000))
|
||||
var reduce = int(data.get("param", 0))
|
||||
var base_dmg = int(floor(max_hp * 0.05))
|
||||
dmg = max(1, int(floor(base_dmg * (100 - reduce) / 100.0)))
|
||||
elif eff_type == EFFECT_FIRE:
|
||||
dmg = int(data.get("param", 50))
|
||||
|
||||
# 扣减目标生命 (不可致死,留 1 点 HP)
|
||||
if targets_map.has(vid):
|
||||
var tgt = targets_map[vid]
|
||||
var cur_hp = int(tgt.get("hp", 100))
|
||||
tgt["hp"] = max(1, cur_hp - dmg)
|
||||
|
||||
status_ticked.emit(vid, eff_type, dmg, data["ticks_left"])
|
||||
|
||||
if data["ticks_left"] <= 0:
|
||||
to_remove.append(eff_type)
|
||||
break
|
||||
else:
|
||||
# 持续时间型 (眩晕/减速)
|
||||
if data["duration"] <= 0.0:
|
||||
to_remove.append(eff_type)
|
||||
|
||||
for eff_type in to_remove:
|
||||
effects.erase(eff_type)
|
||||
status_cleared.emit(vid, eff_type)
|
||||
|
||||
if effects.is_empty():
|
||||
_target_effects.erase(vid)
|
||||
|
||||
func _add_raw_effect(victim_id: int, effect_type: String, data: Dictionary) -> void:
|
||||
if not _target_effects.has(victim_id):
|
||||
_target_effects[victim_id] = {}
|
||||
_target_effects[victim_id][effect_type] = data
|
||||
|
||||
# 获取目标全部当前状态
|
||||
func get_target_effects(victim_id: int) -> Dictionary:
|
||||
return _target_effects.get(victim_id, {}).duplicate(true)
|
||||
|
||||
# 序列化与反序列化
|
||||
func serialize() -> Dictionary:
|
||||
return {
|
||||
"target_effects": _target_effects.duplicate(true)
|
||||
}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
_target_effects = data.get("target_effects", {}).duplicate(true)
|
||||
Reference in New Issue
Block a user