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

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)