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 areaNameCountDictionary = new Dictionary(); [Title("References")] public CharacterBase creator; public ItemBase itemSource; public List targetFractions; public Transform topParent; public AudioContainer audioContainer; public Collider areaCollider; public Dictionary 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(CharacterBase creator, params Fraction[] targetFractions) where T : AttackAreaBase { return Initialize(creator, null, targetFractions); } public T Initialize(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(); if (areaCollider != null) { //areaCollider.excludeLayers = LayerMask.GetMask("AttackAreaVFX", "DecoVFX", "Ignore Raycast"); } audioContainer = GetComponent(); if (audioContainer != null) { audioContainer.soundEventDictionary = new Dictionary(); } 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()) { trail.Clear(); } this.SetReactionSubmodule(); BattleManager.AttackAreaSm.Register(this); topParent.GetComponent().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(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(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(float lifeTime) where T : AttackAreaBase { timeSm = new TimeSubmodule(this, lifeTime); return this as T; } public T SetTimeSubmodule(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() where T : AttackAreaBase { hitSm = new HitSubmodule(this); return this as T; } public T SetHitSubmodule(float hitInterval, int hitCount) where T : AttackAreaBase { hitSm = new HitSubmodule(this, hitInterval, hitCount); return this as T; } #endregion #region LinearDirectionMoveModule public T SetLinearDirectionMoveModule(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 /// /// 设置自适应追踪移动子模块,根据和目标的距离自动连接和断开追踪 /// 断开追踪后,自动选择范围内最近的目标进行追踪 /// public T SetAdaptiveTraceMoveModule(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; } /// /// 设置不可更改目标的追踪移动子模块 /// 永远追踪指定目标,不会断开 /// public T SetUnchangeableTraceMoveModule(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; } /// /// 设置可分离目标的追踪移动子模块 /// 一旦目标超出检测范围则断开追踪,断开后不再追踪其他目标 /// public T SeDetachableTraceMoveModule(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 /// /// 设置射线检测子模块 /// /// 射线方向 /// 球形射线半径,若小于等于0则为直线射线 /// 射线长度,若小于0则为动态长度(与移动速度相等) public T SetRaycastSubmodule(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(float dynamicForce) where T : AttackAreaBase { forceSm = new ForceSubmodule(this, dynamicForce); return this as T; } public T SetForceSubmodule(Vector3 customForce) where T : AttackAreaBase { forceSm = new ForceSubmodule(this, customForce); return this as T; } public T SetForceSubmodule(float strengthXZ, bool isRepulsion, float strengthY = 0) where T : AttackAreaBase { forceSm = new ForceSubmodule(this, strengthXZ, isRepulsion, strengthY); return this as T; } public T SetForceSubmodule(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() where T : AttackAreaBase { reactionSm = new ReactionSubmodule(this); return this as T; } public T SetReactionSubmodule(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(); 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); 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 { 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; } } }