292 lines
11 KiB
C#
292 lines
11 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Invector;
|
|
using Invector.vMelee;
|
|
using Invector.vCharacterController;
|
|
using Sirenix.OdinInspector;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using DG.Tweening;
|
|
using Beyond;
|
|
|
|
namespace Beyond
|
|
{
|
|
[RequireComponent(typeof(Animator))]
|
|
public class MagicAttacks : MonoBehaviour
|
|
{
|
|
[Title("References")]
|
|
[SerializeField] private bEquipArea powersArea;
|
|
|
|
[Title("Input Settings")]
|
|
public GenericInput actionInput = new GenericInput("L", "L", "L");
|
|
|
|
[Title("Events")]
|
|
public UnityEvent OnPlayAnimation;
|
|
public UnityEvent OnEndAnimation;
|
|
public event System.Action<SpellDefinition, Collider> OnSpellHit;
|
|
|
|
// Internal State
|
|
private bThirdPersonInput tpInput;
|
|
private AutoTargetting _autoTargettingInstance;
|
|
public bool isPlaying { get; private set; }
|
|
private bool canPlayNoFaithClip = true;
|
|
[field: Sirenix.OdinInspector.ShowInInspector, Sirenix.OdinInspector.ReadOnly]
|
|
public bool IsShieldActive { get; set; }
|
|
|
|
// Constants
|
|
private const int spellLayerIndex = 5;
|
|
|
|
private void Awake()
|
|
{
|
|
tpInput = GetComponent<bThirdPersonInput>();
|
|
|
|
if (Player.Instance != null)
|
|
{
|
|
_autoTargettingInstance = Player.Instance.AutoTarget;
|
|
}
|
|
}
|
|
private void OnDisable()
|
|
{
|
|
// Safety check: ensure shield flag is reset if player is disabled/respawning
|
|
IsShieldActive = false;
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
HandleInput();
|
|
MonitorAnimationState();
|
|
}
|
|
|
|
private void HandleInput()
|
|
{
|
|
// 1. Basic Checks
|
|
if (tpInput == null || tpInput.cc == null) return;
|
|
|
|
bool canCast = !isPlaying &&
|
|
!tpInput.cc.customAction &&
|
|
!tpInput.cc.IsAnimatorTag("special") &&
|
|
!tpInput.cc.IsAnimatorTag("LockMovement");
|
|
|
|
// 2. Trigger
|
|
if (actionInput.GetButtonDown() && canCast)
|
|
{
|
|
TryCastCurrentSpell();
|
|
}
|
|
}
|
|
|
|
private void TryCastCurrentSpell()
|
|
{
|
|
// A. Get Item & Spell Definition
|
|
bItem selectedItem = GetEquippedSpellItem();
|
|
|
|
if (selectedItem == null)
|
|
{
|
|
// No item equipped in the selected slot
|
|
return;
|
|
}
|
|
|
|
if (selectedItem.spellDefinition == null)
|
|
{
|
|
Debug.LogWarning($"Item '{selectedItem.name}' is equipped but has no SpellDefinition assigned!");
|
|
return;
|
|
}
|
|
|
|
SpellDefinition spell = selectedItem.spellDefinition;
|
|
|
|
// B. Check Costs (Calculated by the Spell SO)
|
|
float cost = spell.GetFaithCost(Player.Instance);
|
|
|
|
if (Player.Instance.GetCurrentFaithValue() < cost)
|
|
{
|
|
PlayNoFaithFeedback();
|
|
return;
|
|
}
|
|
|
|
// C. Success! Consume Resources
|
|
Player.Instance.UpdateFaithCurrentValue(-cost);
|
|
|
|
// Effect: Growth (Heal on Cast) - Global Trinket logic
|
|
if (Player.Instance.CurrentTrinketStats.effectGrowth)
|
|
{
|
|
int healAmt = Mathf.Max(1, (int)(Player.Instance.MaxHealth * 0.02f));
|
|
Player.Instance.ThirdPersonController.ChangeHealth(healAmt);
|
|
}
|
|
|
|
// Handle Consumables (Scrolls)
|
|
if (selectedItem.destroyAfterUse)
|
|
{
|
|
// Uses the standard Invector logic to consume the item in the current slot
|
|
if (powersArea.currentSelectedSlot != null)
|
|
{
|
|
powersArea.UseItem(powersArea.currentSelectedSlot);
|
|
}
|
|
}
|
|
|
|
// D. Snap Rotation (Instant snap before animation starts if needed)
|
|
if (_autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null)
|
|
{
|
|
SnapLookTowardsAutoTarget();
|
|
}
|
|
|
|
// E. Play Animation
|
|
if (!string.IsNullOrEmpty(spell.animationClipName))
|
|
{
|
|
tpInput.cc.animator.CrossFadeInFixedTime(spell.animationClipName, 0.1f);
|
|
OnPlayAnimation.Invoke();
|
|
}
|
|
|
|
// F. Execute Spell Logic
|
|
// We pass 'this' (Monobehaviour) so the SO can start coroutines here.
|
|
Transform target = _autoTargettingInstance != null ?
|
|
(_autoTargettingInstance.CurrentTarget != null ? _autoTargettingInstance.CurrentTarget.transform : null)
|
|
: null;
|
|
|
|
spell.Cast(this, target);
|
|
}
|
|
|
|
// =================================================================================================
|
|
// HELPER METHODS (Called by SpellDefinitions)
|
|
// =================================================================================================
|
|
|
|
/// <summary>
|
|
/// Gets the currently equipped item in the Powers area.
|
|
/// </summary>
|
|
public bItem GetEquippedSpellItem()
|
|
{
|
|
if (powersArea == null) return null;
|
|
return powersArea.currentEquippedItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coroutine used by Spells to rotate the player towards the target over time.
|
|
/// </summary>
|
|
public IEnumerator RotateTowardsTargetRoutine(Transform target, float duration)
|
|
{
|
|
if (duration <= 0) yield break;
|
|
|
|
float timer = 0f;
|
|
while (timer < duration)
|
|
{
|
|
// Dynamic check: target might die or become null during rotation
|
|
Transform actualTarget = target;
|
|
|
|
// Fallback to AutoTarget if the passed target becomes null but a new one exists
|
|
if (actualTarget == null && _autoTargettingInstance != null && _autoTargettingInstance.CurrentTarget != null)
|
|
{
|
|
actualTarget = _autoTargettingInstance.CurrentTarget.transform;
|
|
}
|
|
|
|
if (actualTarget != null)
|
|
{
|
|
Vector3 directionToTarget = actualTarget.position - transform.position;
|
|
directionToTarget.y = 0f;
|
|
|
|
if (directionToTarget.sqrMagnitude > 0.0001f)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized);
|
|
// Using a high speed to ensure we catch up, or use the AutoTargetting speed settings
|
|
float speed = (_autoTargettingInstance != null) ? _autoTargettingInstance.playerRotationSpeed : 10f;
|
|
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, Time.deltaTime * speed * 50f);
|
|
}
|
|
}
|
|
|
|
timer += Time.deltaTime;
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to apply Trinket Damage Multipliers (Soulfire) to any instantiated spell object.
|
|
/// </summary>
|
|
public void ApplyDamageModifiers(GameObject spellObject)
|
|
{
|
|
if (spellObject == null || Player.Instance == null) return;
|
|
|
|
float mult = Player.Instance.CurrentTrinketStats.soulfireDamageMult;
|
|
|
|
// Only apply if multiplier is significant (not 1.0)
|
|
if (Mathf.Abs(mult - 1f) > 0.01f)
|
|
{
|
|
var damageComps = spellObject.GetComponentsInChildren<vObjectDamage>();
|
|
foreach (var comp in damageComps)
|
|
{
|
|
comp.damage.damageValue = Mathf.RoundToInt(comp.damage.damageValue * mult);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =================================================================================================
|
|
// INTERNAL LOGIC
|
|
// =================================================================================================
|
|
|
|
private void MonitorAnimationState()
|
|
{
|
|
if (tpInput == null || tpInput.cc == null || tpInput.cc.animator == null) return;
|
|
|
|
// Check if we are currently playing a spell animation on the specific layer
|
|
var stateInfo = tpInput.cc.animator.GetCurrentAnimatorStateInfo(spellLayerIndex);
|
|
|
|
// We consider it "Playing" if the tag is Spell, or if a spell animation is active
|
|
// Note: Your SpellDefinitions define the Clip Name.
|
|
// A generic way is checking the Tag if you set "Spell" tag in Animator,
|
|
// OR checking if we are in a transition to a spell.
|
|
|
|
// Simplified check based on your old code logic:
|
|
// Assuming all Spell Animations have the tag "Spell" or "Action" in the Animator
|
|
isPlaying = tpInput.cc.IsAnimatorTag("Spell") ||
|
|
tpInput.cc.IsAnimatorTag("special") ||
|
|
tpInput.cc.customAction;
|
|
|
|
// Optional: Trigger OnEndAnimation if needed, though most logic is now in the Coroutine
|
|
}
|
|
|
|
private void SnapLookTowardsAutoTarget()
|
|
{
|
|
if (_autoTargettingInstance == null || _autoTargettingInstance.CurrentTarget == null) return;
|
|
|
|
Transform target = _autoTargettingInstance.CurrentTarget.transform;
|
|
Vector3 direction = target.position - transform.position;
|
|
direction.y = 0f;
|
|
|
|
if (direction.sqrMagnitude > 0.001f)
|
|
{
|
|
transform.rotation = Quaternion.LookRotation(direction.normalized);
|
|
}
|
|
}
|
|
|
|
private void PlayNoFaithFeedback()
|
|
{
|
|
if (!canPlayNoFaithClip) return;
|
|
|
|
canPlayNoFaithClip = false;
|
|
|
|
// Feedback
|
|
if (bItemCollectionDisplay.Instance != null)
|
|
bItemCollectionDisplay.Instance.FadeText("Not enough Faith", 4, 0.25f);
|
|
|
|
if (Player.Instance != null)
|
|
Player.Instance.PlayNoFaithClip();
|
|
|
|
// Reset cooldown
|
|
DOVirtual.DelayedCall(1.5f, () => canPlayNoFaithClip = true);
|
|
}
|
|
|
|
public void RegisterSpellDamageEvents(GameObject spellInstance, SpellDefinition spellDef)
|
|
{
|
|
if (spellInstance == null) return;
|
|
|
|
// Find all damage components on the spawned object
|
|
var damageComps = spellInstance.GetComponentsInChildren<Invector.vObjectDamage>();
|
|
|
|
foreach (var comp in damageComps)
|
|
{
|
|
// Invector's vObjectDamage event gives us the Collider directly
|
|
comp.onHit.AddListener((Collider hitCollider) =>
|
|
{
|
|
// Forward the event to anyone listening to MagicAttacks
|
|
OnSpellHit?.Invoke(spellDef, hitCollider);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} |