Files
Cielonos/Assets/Scripts/MainGame/AttackArea/AttackAreaBase.cs
SoulliesOfficial b5cb6152ff MusicBeat
2026-05-26 00:21:27 -04:00

476 lines
19 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cielonos.MainGame.Buffs.Character;
using Cielonos.MainGame.Characters;
using Cielonos.MainGame.Inventory;
using Cielonos.MainGame.UI;
using Lean.Pool;
using Sirenix.OdinInspector;
using SLSUtilities.General;
using SLSUtilities.LeanPoolAssistance;
using SLSUtilities.WwiseAssistance;
using SLSUtilities.FunctionalAnimation;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Cielonos.MainGame
{
public abstract partial class AttackAreaBase : SerializedMonoBehaviour
{
private static Dictionary<string, int> areaNameCountDictionary = new Dictionary<string, int>();
[Title("References")]
public CharacterBase creator;
public ItemBase itemSource;
public List<Fraction> targetFractions;
public Transform topParent;
public Collider areaCollider;
public Dictionary<string, GameObject> functionalParts;
[Title("Status")]
public string areaName;
public string spamGroupName;
public bool isEnabling;
public bool canTriggerHitEvent = true;
public List<string> tags = new List<string>();
public Action updateAction;
[Title("Submodules")]
[HideInEditorMode] public TransformSubmodule transformSm;
[HideInEditorMode] public AttackSubmodule attackSm;
[HideInEditorMode] public TimeSubmodule timeSm;
[HideInEditorMode] public HitSubmodule hitSm;
[HideInEditorMode] public MoveSubmoduleBase moveSm;
[HideInEditorMode] public RaycastSubmodule raycastSm;
[HideInEditorMode] public ImpulseSubmodule impulseSm;
[HideInEditorMode] public ReactionSubmodule reactionSm;
public T Initialize<T>(CharacterBase creator, params Fraction[] targetFractions) where T : AttackAreaBase
{
return Initialize<T>(creator, null, targetFractions);
}
public virtual T Initialize<T>(CharacterBase creator, ItemBase itemSource, params Fraction[] targetFractions) where T : AttackAreaBase
{
this.isEnabling = false;
this.creator = creator;
this.itemSource = itemSource;
this.targetFractions = targetFractions.ToList();
this.canTriggerHitEvent = true;
this.tags = new List<string>();
attackSm = null;
timeSm = null;
hitSm = null;
moveSm = null;
raycastSm = null;
impulseSm = null;
reactionSm = null;
areaCollider = GetComponent<Collider>();
// 通过 VFXObject 组件确定 VFX 顶层节点,比遍历 parent 链更可靠。
VFXObject vfxObject = GetComponentInParent<VFXObject>();
this.topParent = vfxObject != null ? vfxObject.transform : transform;
if (!areaNameCountDictionary.TryAdd(topParent.name, 1))
{
areaNameCountDictionary[topParent.name]++;
}
areaName = $"{topParent.name}_{areaNameCountDictionary[topParent.name]}";
spamGroupName = creator.name + (itemSource != null ? $"_{itemSource.name}" : "");
foreach (TrailRenderer trail in GetComponentsInChildren<TrailRenderer>())
{
trail.Clear();
}
this.SetReactionSubmodule<T>();
CombatManager.AttackAreaSm.Register(this);
if (vfxObject != null)
{
vfxObject.onDespawnAction = () =>
{
CombatManager.AttackAreaSm.Unregister(this);
};
}
this.isEnabling = true;
return this as T;
}
protected virtual void Update()
{
transformSm?.Update();
raycastSm?.Update();
updateAction?.Invoke();
hitSm?.Update();
timeSm?.Update();
moveSm?.Update();
}
}
public partial class AttackAreaBase
{
#region TransformSubmodule
public T SetTransformSubmodule<T>(Transform target = null, float delay = 0) where T : AttackAreaBase
{
transformSm = new TransformSubmodule(this, target, delay);
return this as T;
}
#endregion
#region AttackSubmodule
public T SetAttackSubmodule<T>(AttackUnit attackUnit, GameObject hitVFX = null) where T : AttackAreaBase
{
if (attackUnit.useVFXDataHit)
{
hitVFX ??= itemSource != null
? itemSource.vfxData.Get(attackUnit.vfxUnitName).hitVFX
: creator.vfxData.Get(attackUnit.vfxUnitName).hitVFX;
}
else
{
hitVFX ??= attackUnit.GetHitVFX();
}
attackSm = new AttackSubmodule(this, attackUnit, hitVFX);
return this as T;
}
#endregion
#region TimeSubmodule
public T SetTimeSubmodule<T>(float lifeTime) where T : AttackAreaBase
{
timeSm = new TimeSubmodule(this, lifeTime);
return this as T;
}
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime = 0.04f,
Action enableAction = null, Action timeOutAction = null) where T : AttackAreaBase
{
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction);
return this as T;
}
#endregion
#region HitSubmodule
public T SetHitSubmodule<T>() where T : AttackAreaBase
{
hitSm = new HitSubmodule(this);
return this as T;
}
public T SetHitSubmodule<T>(float hitInterval, int hitCount) where T : AttackAreaBase
{
hitSm = new HitSubmodule(this, hitInterval, hitCount);
return this as T;
}
#endregion
#region LinearDirectionMoveModule
public T SetLinearDirectionMoveModule<T>(Vector3 direction, float speed,
float acceleration = 0, bool overrideRotation = true, bool disableNegative = true, bool stopWhenHit = true,
float timeScaleCoefficient = 1) where T : AttackAreaBase
{
moveSm = new LinearDirectionMoveSubmodule(this, direction, speed, acceleration,
overrideRotation, disableNegative, stopWhenHit, timeScaleCoefficient);
return this as T;
}
#endregion
#region TraceMoveModule
/// <summary>
/// 设置自适应追踪移动子模块,根据和目标的距离自动连接和断开追踪
/// 断开追踪后,自动选择范围内最近的目标进行追踪
/// </summary>
public T SetAdaptiveTraceMoveModule<T>(CharacterBase target, float moveSpeed, float moveAcceleration,
float angularSpeed, float angularAcceleration, Vector3 initialDirection,
bool autoConnect = true, bool autoDisconnect = true, float detectRadius = 20f, bool stopWhenHit = true) where T : AttackAreaBase
{
moveSm = new TraceMoveSubmodule(this, target, moveSpeed, moveAcceleration, angularSpeed,
angularAcceleration, initialDirection, autoConnect, autoDisconnect, detectRadius, stopWhenHit);
return this as T;
}
/// <summary>
/// 设置不可更改目标的追踪移动子模块
/// 永远追踪指定目标,不会断开
/// </summary>
public T SetUnchangeableTraceMoveModule<T>(CharacterBase target, float moveSpeed, float moveAcceleration,
float angularSpeed, float angularAcceleration, Vector3 initialDirection, bool stopWhenHit = true) where T : AttackAreaBase
{
moveSm = new TraceMoveSubmodule(this, target, moveSpeed, moveAcceleration, angularSpeed,
angularAcceleration, initialDirection, false, false, 0, stopWhenHit);
return this as T;
}
/// <summary>
/// 设置可分离目标的追踪移动子模块
/// 一旦目标超出检测范围则断开追踪,断开后不再追踪其他目标
/// </summary>
public T SeDetachableTraceMoveModule<T>(CharacterBase target, float moveSpeed, float moveAcceleration,
float angularSpeed, float angularAcceleration, Vector3 initialDirection,
float detectRadius = 10f, bool stopWhenHit = true) where T : AttackAreaBase
{
moveSm = new TraceMoveSubmodule(this, target, moveSpeed, moveAcceleration, angularSpeed,
angularAcceleration, initialDirection, false, true, detectRadius, stopWhenHit);
return this as T;
}
#endregion
#region RaycastSubmodule
/// <summary>
/// 设置射线检测子模块
/// </summary>
/// <param name="direction">射线方向</param>
/// <param name="rayRadius">球形射线半径若小于等于0则为直线射线</param>
/// <param name="rayLength">射线长度若小于0则为动态长度与移动速度相等</param>
public T SetRaycastSubmodule<T>(Vector3 direction = default, float rayRadius = -1f, float rayLength = -1f) where T : AttackAreaBase
{
raycastSm = new RaycastSubmodule(this, direction, rayLength, rayRadius);
raycastSm.Update();
return this as T;
}
#endregion
#region ImpulseSubmodule
/// <summary>
/// 创建脉冲子模块并返回实例,后续通过链式 With* 方法配置力模式和参数。
/// 注意:此方法返回 ImpulseSubmodule 而非 AttackAreaBase因此应在主链末尾调用或单独使用。
/// </summary>
public ImpulseSubmodule SetImpulseSubmodule(float duration = 0.5f, AnimationCurve curve = null)
{
impulseSm = new ImpulseSubmodule(this, duration, curve);
return impulseSm;
}
#endregion
#region ReactionSubmodule
public T SetReactionSubmodule<T>() where T : AttackAreaBase
{
reactionSm = new ReactionSubmodule(this);
return this as T;
}
public T SetReactionSubmodule<T>(bool canBeBlocked, bool hasPerfectBlock, bool canBreakBlock,
bool canBeDodged, bool hasPerfectDodge, bool canBreakDodge, bool canBeReflected) where T : AttackAreaBase
{
reactionSm = new ReactionSubmodule(this);
reactionSm.SetBlock(canBeBlocked, hasPerfectBlock, canBreakBlock);
reactionSm.SetDodge(canBeDodged, hasPerfectDodge, canBreakDodge);
reactionSm.SetReflection(canBeReflected);
return this as T;
}
#endregion
}
public partial class AttackAreaBase
{
/// <summary>
/// 检查目标角色是否为有效攻击目标:非创建者自身,且属于目标阵营。
/// </summary>
public bool IsValidTarget(CharacterBase target)
{
if (target == null || target == creator) return false;
return targetFractions.Contains(target.fraction);
}
public virtual void HitCharacter(Collider characterCollider, Vector3 hitPosition)
{
}
public virtual void HitEnvironment(Collider other, Vector3 hitPosition)
{
}
protected virtual void HitOnTarget(Collider hitCollider, Vector3 hitPosition, out Attack.Result attackResult)
{
CharacterBase target = hitCollider.GetComponentInParent<CharacterBase>();
// Phase 1: 构建上下文
var context = new Attack.Context(creator, target, spamGroupName, hitPosition);
attackResult = new Attack.Result(context);
AttackUnit attackUnit = attackSm!.attackUnit;
if (target == null)
{
return;
}
if (moveSm is { stopWhenHit: true })
{
moveSm.canMove = false;
}
attackResult.isBlocked = reactionSm?.CheckBlock(target, creator, hitPosition) ?? false;
attackResult.isDodged = reactionSm?.CheckDodge(target) ?? false;
attackResult.isReflected = reactionSm?.CheckReflection(target) ?? false;
attackResult.isMissed = false;
attackResult.isEvaded = false;
attackResult.causedDeath = false;
if (!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
{
// 被动闪避概率判定(独立于动作帧主动闪避)
float evasionProbability = target.attributeSm[CharacterAttribute.EvasionProbability];
if (evasionProbability > 0f && Random.value < evasionProbability)
{
attackResult.isEvaded = true;
}
}
if (!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
{
// 无论攻击是否 invalid都需要填充 context.value 以供 breakthrough/disruption 读取
if (attackUnit.isIndependentPerHit)
{
attackSm.attackValue = attackSm.attackUnit.GetAttackValue(creator);
}
context.value = attackSm.attackValue.Clone();
if (!attackUnit.isInvalidAttack)
{
// Phase 2: 前置事件,订阅者通过 Context 修改攻击参数
creator.eventSm.onStartAttack.Invoke(this, target, context);
target.eventSm.onBeforeGetAttacked.Invoke(this, context);
for (var index = 0; index < target.buffSm.buffList.Count; index++)
{
var eventBuff = target.buffSm.buffList[index];
eventBuff.eventSubmodule?.onBeforeGetAttacked.Invoke(this, context);
}
if (context.isImmune || context.isForcedInterrupt)
{
return; // 被机制一票否决,直接短路
}
}
// 从 context.value 读取,因为前置事件可能已修改这些值
Breakthrough.Type breakthroughType = context.value.breakthroughType;
DisruptionType disruptionType = context.value.disruptionType;
Vector3 direction = (target.centerPoint.position - creator.centerPoint.position).normalized;
//TODO: 后续制作更详细的打断和击退处理
bool canBreakthrough = target.CheckBreakthrough(breakthroughType);
bool canDisrupt = target.CheckDisruption(disruptionType);
if (target.buffSm.TryGetBuff(out BreakthroughResistanceModification buff))
{
if (canBreakthrough && canDisrupt)
{
attackSm.breakthroughAction?.Invoke(target, hitPosition);
target.eventSm.onGetBreakthrough?.Invoke(this);
target.renderSc.SpawnShatters(breakthroughType, direction);
}
}
bool disrupted = target.GetHit(breakthroughType, out float recoveryTime, disruptionType, direction);
//受击事件
InvokeHitEvents(target, hitPosition);
//特效
GenerateHitEffect(target, hitCollider, hitPosition);
//音效
PlaySoundFX(hitPosition);
if (!attackUnit.isInvalidAttack)
{
// Phase 3: 伤害结算
target.TakeDamage(attackResult, out _);
// Phase 4: 后置事件,订阅者通过 Result 读取结算结果
// onHealthChanged 已由 AttributeSubmodule 值变更回调在 TakeDamage 内部自动触发
creator.eventSm.onFinishAttack.Invoke(this, target, attackResult);
if (attackResult.finalDamage > 0)
{
target.eventSm.onAfterGetAttacked.Invoke(this, attackResult);
for (var index = 0; index < target.buffSm.buffList.Count; index++)
{
var eventBuff = target.buffSm.buffList[index];
eventBuff.eventSubmodule?.onAfterGetAttacked.Invoke(this, attackResult);
}
}
}
}
//应用额外力
if (!attackResult.causedDeath &&
!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
{
impulseSm?.ApplyImpulse(target);
}
}
}
public partial class AttackAreaBase
{
protected virtual void InvokeHitEvents(CharacterBase target, Vector3 hitPosition)
{
hitSm.InvokeAllHitEvents(target, hitPosition);
if (target is Automata automata)
{
automata.behaviorSc.DispatchContextEvent("GetHit");
}
target.eventSm.onGetHit.Invoke(this);
}
protected virtual GameObject GenerateHitEffect(CharacterBase target, Collider hitCollider, Vector3 hitPosition)
{
GameObject hitEffect = attackSm.SpawnHitVFX(creator, hitPosition);
attackSm.modifyHitEffectAction?.Invoke(hitEffect, target);
return hitEffect;
}
protected virtual GameObject GenerateHitEffect(Vector3 hitPosition)
{
GameObject hitEffect = attackSm.SpawnHitVFX(creator, hitPosition);
attackSm.modifyHitEffectAction?.Invoke(hitEffect, null);
return hitEffect;
}
protected virtual void PlaySoundFX(Vector3 hitPosition, uint soundID = AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
{
if (soundID == AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
{
if (hitSm.isAutoPlayHitSound)
{
hitSm.hitSoundList.ForEach(hitSound =>
{
AudioManager.Post(hitSound, hitPosition);
});
}
}
else
{
AudioManager.Post(soundID, hitPosition);
}
}
}
public class AttackAreaSubmoduleBase : SubmoduleBase<AttackAreaBase>
{
protected AttackAreaBase attackArea => owner;
public bool isEnabling;
public AttackAreaSubmoduleBase(AttackAreaBase owner) : base(owner)
{
isEnabling = true;
}
}
}