Files
Cielonos/Assets/Scripts/MainGame/AttackArea/AttackAreaBase.cs

476 lines
19 KiB
C#
Raw Normal View History

2025-11-25 08:19:33 -05:00
using System;
using System.Collections.Generic;
using System.Linq;
2026-02-13 09:22:11 -05:00
using System.Text;
2026-03-20 12:07:44 -04:00
using Cielonos.MainGame.Buffs.Character;
2025-11-25 08:19:33 -05:00
using Cielonos.MainGame.Characters;
2026-05-23 08:27:50 -04:00
using Cielonos.MainGame.Inventory;
2026-02-13 09:22:11 -05:00
using Cielonos.MainGame.UI;
using Lean.Pool;
2025-11-25 08:19:33 -05:00
using Sirenix.OdinInspector;
2026-02-13 09:22:11 -05:00
using SLSUtilities.General;
using SLSUtilities.LeanPoolAssistance;
using SLSUtilities.WwiseAssistance;
2025-11-25 08:19:33 -05:00
using SLSUtilities.FunctionalAnimation;
using UnityEngine;
2025-12-22 18:36:29 -05:00
using Random = UnityEngine.Random;
2025-11-25 08:19:33 -05:00
namespace Cielonos.MainGame
{
2025-12-24 16:58:51 -05:00
public abstract partial class AttackAreaBase : SerializedMonoBehaviour
2025-11-25 08:19:33 -05:00
{
2025-12-22 18:36:29 -05:00
private static Dictionary<string, int> areaNameCountDictionary = new Dictionary<string, int>();
2025-11-25 08:19:33 -05:00
[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")]
2025-12-22 18:36:29 -05:00
public string areaName;
2026-02-13 09:22:11 -05:00
public string spamGroupName;
2025-11-25 08:19:33 -05:00
public bool isEnabling;
public bool canTriggerHitEvent = true;
2026-05-26 00:21:27 -04:00
public List<string> tags = new List<string>();
2025-11-25 08:19:33 -05:00
public Action updateAction;
[Title("Submodules")]
2025-12-24 16:58:51 -05:00
[HideInEditorMode] public TransformSubmodule transformSm;
2025-11-25 08:19:33 -05:00
[HideInEditorMode] public AttackSubmodule attackSm;
[HideInEditorMode] public TimeSubmodule timeSm;
[HideInEditorMode] public HitSubmodule hitSm;
[HideInEditorMode] public MoveSubmoduleBase moveSm;
[HideInEditorMode] public RaycastSubmodule raycastSm;
2026-05-23 08:27:50 -04:00
[HideInEditorMode] public ImpulseSubmodule impulseSm;
2025-11-25 08:19:33 -05:00
[HideInEditorMode] public ReactionSubmodule reactionSm;
public T Initialize<T>(CharacterBase creator, params Fraction[] targetFractions) where T : AttackAreaBase
{
return Initialize<T>(creator, null, targetFractions);
}
2026-04-18 13:57:19 -04:00
public virtual T Initialize<T>(CharacterBase creator, ItemBase itemSource, params Fraction[] targetFractions) where T : AttackAreaBase
2025-11-25 08:19:33 -05:00
{
2026-05-23 08:27:50 -04:00
this.isEnabling = false;
2025-11-25 08:19:33 -05:00
this.creator = creator;
this.itemSource = itemSource;
this.targetFractions = targetFractions.ToList();
this.canTriggerHitEvent = true;
2026-05-26 00:21:27 -04:00
this.tags = new List<string>();
2025-11-25 08:19:33 -05:00
2025-12-08 05:27:53 -05:00
attackSm = null;
timeSm = null;
hitSm = null;
moveSm = null;
raycastSm = null;
2026-05-23 08:27:50 -04:00
impulseSm = null;
2025-12-08 05:27:53 -05:00
reactionSm = null;
2025-11-25 08:19:33 -05:00
areaCollider = GetComponent<Collider>();
2026-05-23 08:27:50 -04:00
// 通过 VFXObject 组件确定 VFX 顶层节点,比遍历 parent 链更可靠。
VFXObject vfxObject = GetComponentInParent<VFXObject>();
this.topParent = vfxObject != null ? vfxObject.transform : transform;
2025-12-22 18:36:29 -05:00
if (!areaNameCountDictionary.TryAdd(topParent.name, 1))
{
areaNameCountDictionary[topParent.name]++;
}
areaName = $"{topParent.name}_{areaNameCountDictionary[topParent.name]}";
2026-02-13 09:22:11 -05:00
spamGroupName = creator.name + (itemSource != null ? $"_{itemSource.name}" : "");
2025-11-25 08:19:33 -05:00
foreach (TrailRenderer trail in GetComponentsInChildren<TrailRenderer>())
{
trail.Clear();
}
this.SetReactionSubmodule<T>();
2025-12-24 16:58:51 -05:00
2026-05-23 08:27:50 -04:00
CombatManager.AttackAreaSm.Register(this);
if (vfxObject != null)
2025-12-24 16:58:51 -05:00
{
2026-05-23 08:27:50 -04:00
vfxObject.onDespawnAction = () =>
{
CombatManager.AttackAreaSm.Unregister(this);
};
}
this.isEnabling = true;
2025-11-25 08:19:33 -05:00
return this as T;
}
protected virtual void Update()
{
2025-12-24 16:58:51 -05:00
transformSm?.Update();
2025-11-25 08:19:33 -05:00
raycastSm?.Update();
updateAction?.Invoke();
hitSm?.Update();
timeSm?.Update();
moveSm?.Update();
}
}
public partial class AttackAreaBase
{
2025-12-24 16:58:51 -05:00
#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
2025-11-25 08:19:33 -05:00
#region AttackSubmodule
2026-01-03 18:19:39 -05:00
2025-11-25 08:19:33 -05:00
public T SetAttackSubmodule<T>(AttackUnit attackUnit, GameObject hitVFX = null) where T : AttackAreaBase
2026-01-03 18:19:39 -05:00
{
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;
}
2025-11-25 08:19:33 -05:00
#endregion
#region TimeSubmodule
public T SetTimeSubmodule<T>(float lifeTime) where T : AttackAreaBase
{
timeSm = new TimeSubmodule(this, lifeTime);
return this as T;
}
2026-05-23 08:27:50 -04:00
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime = 0.04f,
2025-11-25 08:19:33 -05:00
Action enableAction = null, Action timeOutAction = null) where T : AttackAreaBase
{
2025-12-08 05:27:53 -05:00
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction);
2025-11-25 08:19:33 -05:00
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,
2025-12-08 05:27:53 -05:00
float acceleration = 0, bool overrideRotation = true, bool disableNegative = true, bool stopWhenHit = true,
float timeScaleCoefficient = 1) where T : AttackAreaBase
2025-11-25 08:19:33 -05:00
{
2025-12-08 05:27:53 -05:00
moveSm = new LinearDirectionMoveSubmodule(this, direction, speed, acceleration,
overrideRotation, disableNegative, stopWhenHit, timeScaleCoefficient);
2025-11-25 08:19:33 -05:00
return this as T;
}
#endregion
#region TraceMoveModule
2025-12-24 16:58:51 -05:00
/// <summary>
/// 设置自适应追踪移动子模块,根据和目标的距离自动连接和断开追踪
/// 断开追踪后,自动选择范围内最近的目标进行追踪
/// </summary>
public T SetAdaptiveTraceMoveModule<T>(CharacterBase target, float moveSpeed, float moveAcceleration,
float angularSpeed, float angularAcceleration, Vector3 initialDirection,
2026-05-10 11:47:55 -04:00
bool autoConnect = true, bool autoDisconnect = true, float detectRadius = 20f, bool stopWhenHit = true) where T : AttackAreaBase
2025-12-24 16:58:51 -05:00
{
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,
2025-12-08 05:27:53 -05:00
float angularSpeed, float angularAcceleration, Vector3 initialDirection, bool stopWhenHit = true) where T : AttackAreaBase
2025-11-25 08:19:33 -05:00
{
2025-12-24 16:58:51 -05:00
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);
2025-11-25 08:19:33 -05:00
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
2026-05-23 08:27:50 -04:00
#region ImpulseSubmodule
/// <summary>
/// 创建脉冲子模块并返回实例,后续通过链式 With* 方法配置力模式和参数。
/// 注意:此方法返回 ImpulseSubmodule 而非 AttackAreaBase因此应在主链末尾调用或单独使用。
/// </summary>
public ImpulseSubmodule SetImpulseSubmodule(float duration = 0.5f, AnimationCurve curve = null)
2026-02-13 09:22:11 -05:00
{
2026-05-23 08:27:50 -04:00
impulseSm = new ImpulseSubmodule(this, duration, curve);
return impulseSm;
2026-02-13 09:22:11 -05:00
}
2025-11-25 08:19:33 -05:00
#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,
2026-02-13 09:22:11 -05:00
bool canBeDodged, bool hasPerfectDodge, bool canBreakDodge, bool canBeReflected) where T : AttackAreaBase
2025-11-25 08:19:33 -05:00
{
reactionSm = new ReactionSubmodule(this);
reactionSm.SetBlock(canBeBlocked, hasPerfectBlock, canBreakBlock);
reactionSm.SetDodge(canBeDodged, hasPerfectDodge, canBreakDodge);
2026-02-13 09:22:11 -05:00
reactionSm.SetReflection(canBeReflected);
2025-11-25 08:19:33 -05:00
return this as T;
}
#endregion
}
public partial class AttackAreaBase
{
2026-05-23 08:27:50 -04:00
/// <summary>
/// 检查目标角色是否为有效攻击目标:非创建者自身,且属于目标阵营。
/// </summary>
public bool IsValidTarget(CharacterBase target)
{
if (target == null || target == creator) return false;
return targetFractions.Contains(target.fraction);
}
2025-11-25 08:19:33 -05:00
public virtual void HitCharacter(Collider characterCollider, Vector3 hitPosition)
{
}
public virtual void HitEnvironment(Collider other, Vector3 hitPosition)
{
}
2026-05-23 08:27:50 -04:00
protected virtual void HitOnTarget(Collider hitCollider, Vector3 hitPosition, out Attack.Result attackResult)
2025-11-25 08:19:33 -05:00
{
CharacterBase target = hitCollider.GetComponentInParent<CharacterBase>();
2026-02-13 09:22:11 -05:00
2026-05-23 08:27:50 -04:00
// Phase 1: 构建上下文
var context = new Attack.Context(creator, target, spamGroupName, hitPosition);
attackResult = new Attack.Result(context);
2026-02-13 09:22:11 -05:00
AttackUnit attackUnit = attackSm!.attackUnit;
2025-11-25 08:19:33 -05:00
2026-02-13 09:22:11 -05:00
if (target == null)
{
return;
}
2025-11-25 08:19:33 -05:00
2025-12-08 05:27:53 -05:00
if (moveSm is { stopWhenHit: true })
{
moveSm.canMove = false;
}
2026-02-13 09:22:11 -05:00
attackResult.isBlocked = reactionSm?.CheckBlock(target, creator, hitPosition) ?? false;
attackResult.isDodged = reactionSm?.CheckDodge(target) ?? false;
attackResult.isReflected = reactionSm?.CheckReflection(target) ?? false;
2026-05-23 08:27:50 -04:00
attackResult.isMissed = false;
attackResult.isEvaded = false;
2026-02-13 09:22:11 -05:00
attackResult.causedDeath = false;
2025-11-25 08:19:33 -05:00
2026-02-13 09:22:11 -05:00
if (!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
2025-11-25 08:19:33 -05:00
{
2026-05-23 08:27:50 -04:00
// 被动闪避概率判定(独立于动作帧主动闪避)
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();
2026-02-13 09:22:11 -05:00
if (!attackUnit.isInvalidAttack)
2026-01-03 18:19:39 -05:00
{
2026-05-23 08:27:50 -04:00
// Phase 2: 前置事件,订阅者通过 Context 修改攻击参数
creator.eventSm.onStartAttack.Invoke(this, target, context);
target.eventSm.onBeforeGetAttacked.Invoke(this, context);
2026-05-10 11:47:55 -04:00
for (var index = 0; index < target.buffSm.buffList.Count; index++)
{
var eventBuff = target.buffSm.buffList[index];
2026-05-23 08:27:50 -04:00
eventBuff.eventSubmodule?.onBeforeGetAttacked.Invoke(this, context);
2026-05-10 11:47:55 -04:00
}
2026-05-23 08:27:50 -04:00
if (context.isImmune || context.isForcedInterrupt)
2026-03-20 12:07:44 -04:00
{
return; // 被机制一票否决,直接短路
}
2026-01-03 18:19:39 -05:00
}
2026-05-23 08:27:50 -04:00
// 从 context.value 读取,因为前置事件可能已修改这些值
Breakthrough.Type breakthroughType = context.value.breakthroughType;
DisruptionType disruptionType = context.value.disruptionType;
2026-03-20 12:07:44 -04:00
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);
2026-05-23 08:27:50 -04:00
target.renderSc.SpawnShatters(breakthroughType, direction);
2026-03-20 12:07:44 -04:00
}
}
bool disrupted = target.GetHit(breakthroughType, out float recoveryTime, disruptionType, direction);
2025-11-25 08:19:33 -05:00
//受击事件
InvokeHitEvents(target, hitPosition);
2026-03-20 12:07:44 -04:00
//特效
GenerateHitEffect(target, hitCollider, hitPosition);
//音效
PlaySoundFX(hitPosition);
2026-02-13 09:22:11 -05:00
if (!attackUnit.isInvalidAttack)
2025-11-25 08:19:33 -05:00
{
2026-05-23 08:27:50 -04:00
// Phase 3: 伤害结算
target.TakeDamage(attackResult, out _);
2026-04-18 13:57:19 -04:00
2026-05-23 08:27:50 -04:00
// Phase 4: 后置事件,订阅者通过 Result 读取结算结果
// onHealthChanged 已由 AttributeSubmodule 值变更回调在 TakeDamage 内部自动触发
2026-02-13 09:22:11 -05:00
creator.eventSm.onFinishAttack.Invoke(this, target, attackResult);
if (attackResult.finalDamage > 0)
{
target.eventSm.onAfterGetAttacked.Invoke(this, attackResult);
2026-05-10 11:47:55 -04:00
for (var index = 0; index < target.buffSm.buffList.Count; index++)
{
var eventBuff = target.buffSm.buffList[index];
eventBuff.eventSubmodule?.onAfterGetAttacked.Invoke(this, attackResult);
}
2026-02-13 09:22:11 -05:00
}
2025-11-25 08:19:33 -05:00
}
}
2026-02-13 09:22:11 -05:00
2025-11-25 08:19:33 -05:00
//应用额外力
2026-02-13 09:22:11 -05:00
if (!attackResult.causedDeath &&
!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
2025-11-25 08:19:33 -05:00
{
2026-05-23 08:27:50 -04:00
impulseSm?.ApplyImpulse(target);
2025-11-25 08:19:33 -05:00
}
}
}
public partial class AttackAreaBase
{
protected virtual void InvokeHitEvents(CharacterBase target, Vector3 hitPosition)
{
hitSm.InvokeAllHitEvents(target, hitPosition);
2026-03-20 12:07:44 -04:00
if (target is Automata automata)
{
automata.behaviorSc.DispatchContextEvent("GetHit");
}
2025-12-08 05:27:53 -05:00
target.eventSm.onGetHit.Invoke(this);
2025-11-25 08:19:33 -05:00
}
protected virtual GameObject GenerateHitEffect(CharacterBase target, Collider hitCollider, Vector3 hitPosition)
{
2025-12-17 04:19:38 -05:00
GameObject hitEffect = attackSm.SpawnHitVFX(creator, hitPosition);
2025-11-25 08:19:33 -05:00
attackSm.modifyHitEffectAction?.Invoke(hitEffect, target);
return hitEffect;
}
protected virtual GameObject GenerateHitEffect(Vector3 hitPosition)
{
2025-12-17 04:19:38 -05:00
GameObject hitEffect = attackSm.SpawnHitVFX(creator, hitPosition);
2025-11-25 08:19:33 -05:00
attackSm.modifyHitEffectAction?.Invoke(hitEffect, null);
return hitEffect;
}
2026-03-20 12:07:44 -04:00
protected virtual void PlaySoundFX(Vector3 hitPosition, uint soundID = AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
2025-11-25 08:19:33 -05:00
{
2026-03-20 12:07:44 -04:00
if (soundID == AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
2025-11-25 08:19:33 -05:00
{
if (hitSm.isAutoPlayHitSound)
{
2026-03-20 12:07:44 -04:00
hitSm.hitSoundList.ForEach(hitSound =>
{
AudioManager.Post(hitSound, hitPosition);
});
2025-11-25 08:19:33 -05:00
}
}
else
{
2026-03-20 12:07:44 -04:00
AudioManager.Post(soundID, hitPosition);
2025-11-25 08:19:33 -05:00
}
}
}
public class AttackAreaSubmoduleBase : SubmoduleBase<AttackAreaBase>
{
protected AttackAreaBase attackArea => owner;
public bool isEnabling;
public AttackAreaSubmoduleBase(AttackAreaBase owner) : base(owner)
{
isEnabling = true;
}
}
}