Files
Cielonos/Assets/Scripts/MainGame/AttackArea/AttackAreaBase.cs
2026-04-18 13:57:19 -04:00

511 lines
20 KiB
C#
Raw 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.Characters.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 AudioContainer audioContainer;
public Collider areaCollider;
public Dictionary<string, GameObject> functionalParts;
[Title("Status")]
public string areaName;
public string spamGroupName;
public bool isEnabling;
public bool canTriggerHitEvent = true;
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 ForceSubmodule forceSm;
[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 = true;
this.creator = creator;
this.itemSource = itemSource;
this.targetFractions = targetFractions.ToList();
this.topParent = transform;
this.canTriggerHitEvent = true;
attackSm = null;
timeSm = null;
hitSm = null;
moveSm = null;
raycastSm = null;
forceSm = null;
reactionSm = null;
areaCollider = GetComponent<Collider>();
if (areaCollider != null)
{
//areaCollider.excludeLayers = LayerMask.GetMask("AttackAreaVFX", "DecoVFX", "Ignore Raycast");
}
audioContainer = GetComponent<AudioContainer>();
if (audioContainer != null)
{
audioContainer.soundEventDictionary = new Dictionary<string, AK.Wwise.Event>();
}
while (topParent.parent != null &&
//topParent.parent != creator.flexibleCenterPoint &&
//topParent.parent != creator.staticCenterPoint &&
topParent.parent != creator.transform)
{
topParent = topParent.parent;
}
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>();
BattleManager.AttackAreaSm.Register(this);
topParent.GetComponent<VFXObject>().onDespawnAction = () =>
{
BattleManager.AttackAreaSm.Unregister(this);
};
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.06f,
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 = 10f, 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 ForceSubmodule
public T SetForceSubmodule<T>(float dynamicForce) where T : AttackAreaBase
{
forceSm = new ForceSubmodule(this, dynamicForce);
return this as T;
}
public T SetForceSubmodule<T>(Vector3 customForce) where T : AttackAreaBase
{
forceSm = new ForceSubmodule(this, customForce);
return this as T;
}
public T SetForceSubmodule<T>(float strengthXZ, bool isRepulsion, float strengthY = 0) where T : AttackAreaBase
{
forceSm = new ForceSubmodule(this, strengthXZ, isRepulsion, strengthY);
return this as T;
}
public T SetForceSubmodule<T>(float strengthXZ, bool isRepulsion, bool isLaunch,
float strengthY = 0, float stasisDuration = 0f) where T : AttackAreaBase
{
forceSm = new ForceSubmodule(this, strengthXZ, isRepulsion, isLaunch, strengthY, stasisDuration);
return this as T;
}
#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
{
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 AttackResult attackResult)
{
CharacterBase target = hitCollider.GetComponentInParent<CharacterBase>();
attackResult = new AttackResult(creator, target, spamGroupName, hitPosition);
AttackUnit attackUnit = attackSm!.attackUnit;
if (!attackUnit.isInvalidAttack)
{
// We will invoke onStartAttack down below with the AttackResult cloned.
}
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; // reactionModule?.SetMiss(creator.attributeModule.currentAttributes.GetValue("AttackMissProbability", 0)) ?? false;
attackResult.isEvaded = false; // reactionModule?.SetEvasion(player.attributeModule.currentAttributes.GetValue("EvasionProbability", 0)) ?? false;
attackResult.causedDeath = false;
if (!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
{
if (!attackUnit.isInvalidAttack)
{
if (attackUnit.isIndependentPerHit)
{
attackSm.attackValue = attackSm.attackUnit.GetAttackValue(creator);
}
attackResult.attackValue = attackSm.attackValue.Clone();
creator.eventSm.onStartAttack.Invoke(this, target, attackResult);
target.eventSm.onBeforeGetAttacked.Invoke(this, attackResult);
if (attackResult.isImmune || attackResult.isForceInterrupt)
{
return; // 被机制一票否决,直接短路
}
}
BreakthroughType breakthroughType = attackSm.attackValue.breakthroughType;
DisruptionType disruptionType = attackSm.attackValue.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);
}
}
bool disrupted = target.GetHit(breakthroughType, out float recoveryTime, disruptionType, direction);
//受击事件
InvokeHitEvents(target, hitPosition);
//特效
GenerateHitEffect(target, hitCollider, hitPosition);
//音效
PlaySoundFX(hitPosition);
if (!attackUnit.isInvalidAttack)
{
//最终伤害结算
target.TakeDamage(ref attackResult);
Attack.AttackType attackType = attackResult.attackValue.attackType;
bool isCritical = attackResult.attackValue.isCritical;
MainGameBaseCollection.Instance.DamageNumber(attackType, isCritical)
.Spawn(attackResult.hitPosition, attackResult.finalDamage, transform)
.SetSpamGroup(attackResult.spamGroupID);
creator.eventSm.onFinishAttack.Invoke(this, target, attackResult);
if (attackResult.finalDamage > 0)
{
target.eventSm.onAfterGetAttacked.Invoke(this, attackResult);
target.eventSm.onHealthChanged.Invoke(-attackResult.finalDamage);
}
}
}
//应用额外力
if (!attackResult.causedDeath &&
!attackResult.isBlocked && !attackResult.isDodged &&
!attackResult.isMissed && !attackResult.isEvaded)
{
forceSm?.ApplyForce(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;
}
}
public class AttackResult
{
public CharacterBase attacker;
public CharacterBase target;
public Vector3 hitPosition;
public string spamGroupID;
public bool isBlocked;
public bool isDodged;
public bool isReflected;
public bool isMissed;
public bool isEvaded;
public bool causedDeath;
public bool isImmune;
public bool isForceInterrupt;
public AttackValue attackValue;
public float shieldBlockedDamage;
public float finalDamage;
public AttackResult(CharacterBase attacker, CharacterBase target,
string spamGroupMainPart, Vector3 hitPosition = default)
{
this.attacker = attacker;
this.target = target;
StringBuilder fullIDBuilder = new StringBuilder();
fullIDBuilder.Append(attacker is null ? "Null" : this.attacker.GetInstanceID().ToString());
fullIDBuilder.Append("_");
fullIDBuilder.Append(spamGroupMainPart);
fullIDBuilder.Append("_");
fullIDBuilder.Append(target is null ? "Null" : this.target.GetInstanceID().ToString());
this.spamGroupID = fullIDBuilder.ToString();
//Debug.Log($"Generated AttackResult with SpamGroupID: {this.spamGroupID}");
this.hitPosition = hitPosition;
}
}
}