- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
250 lines
9.1 KiB
GDScript
250 lines
9.1 KiB
GDScript
# 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)
|