From c4d084acb282a618e6dfbbb2756f1071c8685f6c Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Thu, 19 Jun 2025 19:46:29 +0100 Subject: [PATCH] Ability Runtime instances, modifiers and behaviours (WIP) --- .../Characters/Skeleton_Enemy.controller | 6 +- .../Panoramics/FS002/FS002_Night.mat | 2 +- Assets/Scenes/0-Splash.unity | 60 +++ .../Mage/0-IceShard_ProjectileAbility.asset | 2 +- .../AbilityInstance/Refactor.meta | 8 + .../Refactor/CostCalculationContext.cs | 59 +++ .../Refactor/CostCalculationContext.cs.meta | 11 + .../AbilityInstance/RuntimeAbilityInstance.cs | 404 +++++++++++------- .../AbilityInstance/RuntimeBehavior.cs | 1 + Assets/Scripts/Player/AbilityKeyBinder.cs | 34 +- 10 files changed, 434 insertions(+), 153 deletions(-) create mode 100644 Assets/Scripts/AbilitySystem/AbilityInstance/Refactor.meta create mode 100644 Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs create mode 100644 Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs.meta diff --git a/Assets/1-Packs/3D/CharactersPack/KayKit_Adventurers_1.0_FREE/Characters/Skeleton_Enemy.controller b/Assets/1-Packs/3D/CharactersPack/KayKit_Adventurers_1.0_FREE/Characters/Skeleton_Enemy.controller index fcf9d85c..7f712a11 100644 --- a/Assets/1-Packs/3D/CharactersPack/KayKit_Adventurers_1.0_FREE/Characters/Skeleton_Enemy.controller +++ b/Assets/1-Packs/3D/CharactersPack/KayKit_Adventurers_1.0_FREE/Characters/Skeleton_Enemy.controller @@ -236,7 +236,7 @@ AnimatorStateTransition: m_HasFixedDuration: 1 m_InterruptionSource: 0 m_OrderedInterruption: 1 - m_CanTransitionToSelf: 1 + m_CanTransitionToSelf: 0 --- !u!1101 &-3316037427580542980 AnimatorStateTransition: m_ObjectHideFlags: 1 @@ -564,9 +564,9 @@ AnimatorStateTransition: m_Mute: 0 m_IsExit: 0 serializedVersion: 3 - m_TransitionDuration: 0.2 + m_TransitionDuration: 0.15 m_TransitionOffset: 0 - m_ExitTime: 0.8 + m_ExitTime: 0.5 m_HasExitTime: 1 m_HasFixedDuration: 1 m_InterruptionSource: 0 diff --git a/Assets/Fantasy Skybox FREE/Panoramics/FS002/FS002_Night.mat b/Assets/Fantasy Skybox FREE/Panoramics/FS002/FS002_Night.mat index 3dde5691..e12f787b 100644 --- a/Assets/Fantasy Skybox FREE/Panoramics/FS002/FS002_Night.mat +++ b/Assets/Fantasy Skybox FREE/Panoramics/FS002/FS002_Night.mat @@ -78,7 +78,7 @@ Material: - _Mode: 0 - _OcclusionStrength: 1 - _Parallax: 0.02 - - _Rotation: 7.162766 + - _Rotation: 6.1873918 - _SmoothnessTextureChannel: 0 - _SpecularHighlights: 1 - _SrcBlend: 1 diff --git a/Assets/Scenes/0-Splash.unity b/Assets/Scenes/0-Splash.unity index 811428fd..2a521f14 100644 --- a/Assets/Scenes/0-Splash.unity +++ b/Assets/Scenes/0-Splash.unity @@ -2301,6 +2301,26 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 0} m_Modifications: + - target: {fileID: 559795999353192350, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 559795999353192350, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 559795999353192350, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 559795999353192350, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 559795999353192350, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} - target: {fileID: 952476992021474036, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} propertyPath: m_AnchorMax.y value: 0 @@ -2429,6 +2449,26 @@ PrefabInstance: propertyPath: m_AnchoredPosition.y value: -600 objectReference: {fileID: 0} + - target: {fileID: 3015579015349867821, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3015579015349867821, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3015579015349867821, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3015579015349867821, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3015579015349867821, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} - target: {fileID: 3044532144327822992, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} propertyPath: m_AnchorMax.y value: 0 @@ -2513,6 +2553,26 @@ PrefabInstance: propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} + - target: {fileID: 5126747884924751304, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5126747884924751304, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5126747884924751304, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5126747884924751304, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5126747884924751304, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} - target: {fileID: 6069245676466788546, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3} propertyPath: m_AnchorMax.y value: 0 diff --git a/Assets/Scriptables/Data/Resources/Abilities/Debug/Player/Mage/0-IceShard_ProjectileAbility.asset b/Assets/Scriptables/Data/Resources/Abilities/Debug/Player/Mage/0-IceShard_ProjectileAbility.asset index 9de6557c..01c49208 100644 --- a/Assets/Scriptables/Data/Resources/Abilities/Debug/Player/Mage/0-IceShard_ProjectileAbility.asset +++ b/Assets/Scriptables/Data/Resources/Abilities/Debug/Player/Mage/0-IceShard_ProjectileAbility.asset @@ -29,7 +29,7 @@ MonoBehaviour: - {fileID: 11400000, guid: 7b7a9c9ae4b8ece4eb680f621937c58a, type: 2} - {fileID: 11400000, guid: 127f92fd14a21174695a692c0cf18746, type: 2} castTime: 0.5 - manaCost: 3 + manaCost: 30 healthCost: 0 classResourceCost: 0 spiritPowerReserveCost: 0 diff --git a/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor.meta b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor.meta new file mode 100644 index 00000000..279af8b7 --- /dev/null +++ b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ebbaf77cf9aa1bf4686e2835b46e8e66 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs new file mode 100644 index 00000000..d82431c1 --- /dev/null +++ b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs @@ -0,0 +1,59 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +// ============================================================================ +// COST CALCULATION CONTEXT - Holds user stats for cost calculations +// ============================================================================ + +/// +/// Context containing user stats needed for cost calculations +/// Allows ability cost calculation without direct user reference +/// +public struct CostCalculationContext +{ + public float maxMana; + public float maxHealth; + public float currentMana; + public float currentHealth; + public float currentClassResource; + + public static CostCalculationContext FromUser(Taggable user) + { + var context = new CostCalculationContext(); + + var mana = user.GetComponent(); + if (mana != null) + { + context.maxMana = mana.GetMaxValue(); + context.currentMana = mana.GetCurrentValue(); + } + + var health = user.GetComponent(); + if (health != null) + { + context.maxHealth = health.GetMaxValue(); + context.currentHealth = health.GetCurrentValue(); + } + + var classResource = user.GetComponent(); + if (classResource != null) + { + context.currentClassResource = classResource.GetCurrentValue(); + } + + return context; + } + + public static CostCalculationContext CreateMockContext(float maxMana = 100f, float maxHealth = 100f) + { + return new CostCalculationContext + { + maxMana = maxMana, + maxHealth = maxHealth, + currentMana = maxMana, + currentHealth = maxHealth, + currentClassResource = 100f + }; + } +} diff --git a/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs.meta b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs.meta new file mode 100644 index 00000000..814fa7e9 --- /dev/null +++ b/Assets/Scripts/AbilitySystem/AbilityInstance/Refactor/CostCalculationContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5050e09038372134fbe8fa068698e68a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeAbilityInstance.cs b/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeAbilityInstance.cs index e75b0127..5a9f5cb9 100644 --- a/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeAbilityInstance.cs +++ b/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeAbilityInstance.cs @@ -1,25 +1,20 @@ -// ============================================================================ -// RUNTIME ABILITY WRAPPER - DROP-IN REPLACEMENT FOR BaseAbility -// ============================================================================ - using Kryz.CharacterStats.Examples; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; - // ============================================================================ -// 1. RUNTIME ABILITY INSTANCE - Wraps Your Existing BaseAbility +// RUNTIME ABILITY INSTANCE - Context-Aware Version // ============================================================================ /// -/// Runtime wrapper for BaseAbility that adds modifiers and behaviors -/// Can be used anywhere you currently use BaseAbility references +/// Runtime wrapper for BaseAbility with context-aware cost calculations +/// Modifiers now properly affect total costs (flat + percentage combined) /// public class RuntimeAbilityInstance { // ======================================================================== - // CORE DATA - Wraps your existing ScriptableObject + // CORE DATA // ======================================================================== [SerializeField] private BaseAbility sourceAbility; @@ -37,15 +32,13 @@ public class RuntimeAbilityInstance public RuntimeAbilityInstance(BaseAbility source) { sourceAbility = source; - currentCharges = GetMaxCharges(); // In case you add charges later - RecalculateModifiedValues(); + currentCharges = GetMaxCharges(); } // ======================================================================== - // PROPERTIES - Direct access to original + modified values + // PROPERTIES - Direct access to original values // ======================================================================== - // Original values (unchanged) public BaseAbility SourceAbility => sourceAbility; public string displayName => sourceAbility.displayName; public Sprite Icon => sourceAbility.Icon; @@ -55,15 +48,18 @@ public class RuntimeAbilityInstance public bool castableWhileMoving => sourceAbility.castableWhileMoving; public AbilityAnimationType animationType => sourceAbility.animationType; - // Modified values (affected by modifiers) - public float manaCost { get; private set; } - public float healthCost { get; private set; } - public float classResourceCost { get; private set; } - public float spiritPowerReserveCost { get; private set; } - public float percentMaxManaCost { get; private set; } - public float percentMaxHealthCost { get; private set; } - public float castTime { get; private set; } - public float cooldown { get; private set; } + // Base costs (for display when no context available) + public float baseManaFlat => sourceAbility.manaCost; + public float baseHealthFlat => sourceAbility.healthCost; + public float baseClassResourceFlat => sourceAbility.classResourceCost; + public float baseManaPercent => sourceAbility.percentMaxManaCost; + public float baseHealthPercent => sourceAbility.percentMaxHealthCost; + public float baseCastTime => sourceAbility.castTime; + public float baseCooldown => sourceAbility.cooldown; + + // Modified timing values (these don't need user context) + public float castTime => GetModifiedCastTime(); + public float cooldown => GetModifiedCooldown(); // Runtime state properties public float LastUsedTime => lastUsedTime; @@ -73,23 +69,143 @@ public class RuntimeAbilityInstance public List RuntimeBehaviors => new List(runtimeBehaviors); // ======================================================================== - // EXECUTION METHODS - Same signature as your BaseAbility + // CONTEXT-AWARE COST CALCULATIONS // ======================================================================== + /// + /// Calculate final mana cost including modifiers (affects flat + percentage total) + /// + public float GetFinalManaCost(CostCalculationContext context) + { + // Calculate total base cost (flat + percentage) + float totalBaseCost = sourceAbility.manaCost + (context.maxMana * sourceAbility.percentMaxManaCost); + + // Apply modifiers to the total cost + float modifiedCost = (totalBaseCost + GetTotalManaCostFlat()) * GetTotalManaCostMultiplier(); + + return Mathf.Max(0f, modifiedCost); + } + + /// + /// Calculate final health cost including modifiers (affects flat + percentage total) + /// + public float GetFinalHealthCost(CostCalculationContext context) + { + // Calculate total base cost (flat + percentage) + float totalBaseCost = sourceAbility.healthCost + (context.maxHealth * sourceAbility.percentMaxHealthCost); + + // Apply modifiers to the total cost + float modifiedCost = (totalBaseCost + GetTotalHealthCostFlat()) * GetTotalHealthCostMultiplier(); + + return Mathf.Max(0f, modifiedCost); + } + + /// + /// Calculate final class resource cost (flat only, no percentage version) + /// + public float GetFinalClassResourceCost() + { + float modifiedCost = (sourceAbility.classResourceCost + GetTotalClassResourceFlat()) * GetTotalClassResourceMultiplier(); + return Mathf.Max(0f, modifiedCost); + } + + // Convenience overloads for when you have a user reference + public float GetFinalManaCost(Taggable user) => GetFinalManaCost(CostCalculationContext.FromUser(user)); + public float GetFinalHealthCost(Taggable user) => GetFinalHealthCost(CostCalculationContext.FromUser(user)); + + // ======================================================================== + // DISPLAY METHODS - For UI when no user context available + // ======================================================================== + + /// + /// Get display-friendly mana cost text + /// + public string GetManaCostDisplayText(CostCalculationContext? context = null) + { + if (context.HasValue) + { + return $"{GetFinalManaCost(context.Value):F0} Mana"; + } + else + { + // Show base cost + modifiers when no context + float flatCost = baseManaFlat * GetTotalManaCostMultiplier() + GetTotalManaCostFlat(); + string text = $"{Mathf.Max(0f, flatCost):F0}"; + + if (baseManaPercent > 0) + text += $" + {baseManaPercent:P0} Max"; + + return text + " Mana"; + } + } + + /// + /// Get display-friendly health cost text + /// + public string GetHealthCostDisplayText(CostCalculationContext? context = null) + { + if (context.HasValue) + { + return $"{GetFinalHealthCost(context.Value):F0} Health"; + } + else + { + float flatCost = baseHealthFlat * GetTotalHealthCostMultiplier() + GetTotalHealthCostFlat(); + string text = $"{Mathf.Max(0f, flatCost):F0}"; + + if (baseHealthPercent > 0) + text += $" + {baseHealthPercent:P0} Max"; + + return text + " Health"; + } + } + + /// + /// Get all cost information as formatted string + /// + public string GetAllCostsDisplayText(CostCalculationContext? context = null) + { + var costs = new List(); + + // Mana cost + if (baseManaFlat > 0 || baseManaPercent > 0) + costs.Add(GetManaCostDisplayText(context)); + + // Health cost + if (baseHealthFlat > 0 || baseHealthPercent > 0) + costs.Add(GetHealthCostDisplayText(context)); + + // Class resource cost + float classResourceCost = GetFinalClassResourceCost(); + if (classResourceCost > 0) + costs.Add($"{classResourceCost:F0} Energy"); + + return costs.Count > 0 ? string.Join(", ", costs) : "No Cost"; + } + + // ======================================================================== + // EXECUTION METHODS + // ======================================================================== + + Mana userMana; + Health userHealth; + public virtual void SpendResourcesNecessary(Taggable user) + { + + userMana.ChangeValue(-GetFinalManaCost(user)); + userHealth.ChangeValue(-GetFinalHealthCost(user)); + //user.GetComponent()?.ChangeValue(-GetFinalClassResourceCost()); + } + public virtual void Execute(Taggable user) { if (!CanExecute(user)) return; - // Execute pre-cast behaviors ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, Vector3.zero); - - // Execute original ability + SpendResourcesNecessary(user); sourceAbility.Execute(user); - - // Execute post-cast behaviors ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, Vector3.zero); - // Update runtime state lastUsedTime = Time.time; } @@ -98,6 +214,7 @@ public class RuntimeAbilityInstance if (!CanExecute(user)) return; ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, point); + SpendResourcesNecessary(user); sourceAbility.Execute(user, point); ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, point); @@ -109,6 +226,7 @@ public class RuntimeAbilityInstance if (!CanExecute(user)) return; ExecuteBehaviors(BehaviorTrigger.PreCast, user, target, target.position); + SpendResourcesNecessary(user); sourceAbility.Execute(user, target); ExecuteBehaviors(BehaviorTrigger.PostCast, user, target, target.position); @@ -121,83 +239,131 @@ public class RuntimeAbilityInstance public bool CanExecute(Taggable user) { - // Check cooldown if (IsOnCooldown) return false; - - // Check resources using modified costs - return CanAffordResources(user); + return CanAffordResources(CostCalculationContext.FromUser(user)); } - private bool CanAffordResources(Taggable user) + public bool CanAffordResources(CostCalculationContext context) { - // Use the modified costs, not original - var userMana = user.GetComponent(); - if (userMana != null) + // Check mana cost + if (context.maxMana > 0) { - float finalManaCost = manaCost + userMana.GetMaxValue() * percentMaxManaCost; - if (!userMana.EnoughMana(finalManaCost)) return false; + float finalManaCost = GetFinalManaCost(context); + if (context.currentMana < finalManaCost) return false; } - var userHealth = user.GetComponent(); - if (userHealth != null) + // Check health cost + if (context.maxHealth > 0) { - float finalHealthCost = healthCost + userHealth.GetMaxValue() * percentMaxHealthCost; - if (userHealth.GetCurrentValue() <= finalHealthCost) return false; + float finalHealthCost = GetFinalHealthCost(context); + if (context.currentHealth <= finalHealthCost) return false; } - var userClassResource = user.GetComponent(); - if (userClassResource != null && classResourceCost > 0) + // Check class resource cost + if (context.currentClassResource > 0) { - if (userClassResource.GetCurrentValue() < classResourceCost) return false; + float finalClassResourceCost = GetFinalClassResourceCost(); + if (context.currentClassResource < finalClassResourceCost) return false; } return true; } - public float GetFinalManaCost(Mana userMana) + // Convenience overload + public bool CanAffordResources(Taggable user) => CanAffordResources(CostCalculationContext.FromUser(user)); + + // ======================================================================== + // MODIFIER CALCULATION HELPERS + // ======================================================================== + + private float GetTotalManaCostMultiplier() { - return manaCost + userMana.GetMaxValue() * percentMaxManaCost; + float multiplier = 1f; + foreach (var modifier in activeModifiers) + multiplier *= modifier.manaCostMultiplier; + return multiplier; } - public float GetFinalHealthCost(Health userHealth) + private float GetTotalManaCostFlat() { - return healthCost + userHealth.GetMaxValue() * percentMaxHealthCost; + float flat = 0f; + foreach (var modifier in activeModifiers) + flat += modifier.manaCostFlat; + return flat; + } + + private float GetTotalHealthCostMultiplier() + { + float multiplier = 1f; + foreach (var modifier in activeModifiers) + multiplier *= modifier.healthCostMultiplier; + return multiplier; + } + + private float GetTotalHealthCostFlat() + { + float flat = 0f; + foreach (var modifier in activeModifiers) + flat += modifier.healthCostFlat; + return flat; + } + + private float GetTotalClassResourceMultiplier() + { + float multiplier = 1f; + foreach (var modifier in activeModifiers) + multiplier *= modifier.classResourceCostMultiplier; + return multiplier; + } + + private float GetTotalClassResourceFlat() + { + float flat = 0f; + foreach (var modifier in activeModifiers) + flat += modifier.classResourceCostFlat; + return flat; + } + + private float GetModifiedCastTime() + { + float time = sourceAbility.castTime; + foreach (var modifier in activeModifiers) + time *= modifier.castTimeMultiplier; + return Mathf.Max(0f, time); + } + + private float GetModifiedCooldown() + { + float cd = sourceAbility.cooldown; + foreach (var modifier in activeModifiers) + cd *= modifier.cooldownMultiplier; + return Mathf.Max(0f, cd); } // ======================================================================== - // ENHANCED MODIFIER SYSTEM - Source-Aware Management + // MODIFIER MANAGEMENT (same as before) // ======================================================================== public void AddModifier(AbilityModifier modifier) { activeModifiers.Add(modifier); - RecalculateModifiedValues(); } public void AddModifier(AbilityModifier modifier, object source) { - // Create a copy with the specified source var modifierWithSource = new AbilityModifier(modifier, source); activeModifiers.Add(modifierWithSource); - RecalculateModifiedValues(); } public void RemoveModifier(AbilityModifier modifier) { activeModifiers.Remove(modifier); - RecalculateModifiedValues(); } public bool RemoveAllModifiersFromSource(object source) { int numRemovals = activeModifiers.RemoveAll(mod => mod.Source != null && mod.Source.Equals(source)); - - if (numRemovals > 0) - { - RecalculateModifiedValues(); - return true; - } - return false; + return numRemovals > 0; } public bool HasModifiersFromSource(object source) @@ -213,87 +379,39 @@ public class RuntimeAbilityInstance public void RemoveAllModifiers() { activeModifiers.Clear(); - RecalculateModifiedValues(); } // ======================================================================== - // CONVENIENCE METHODS - Common source-based operations + // CONVENIENCE METHODS // ======================================================================== - // Add modifier from equipment public void AddEquipmentModifier(AbilityModifier modifier, EquippableItem equipment) { AddModifier(modifier, equipment); } - // Add modifier from buff/effect public void AddEffectModifier(AbilityModifier modifier, BaseEffect effect) { AddModifier(modifier, effect); } - // Add modifier from skill/talent public void AddSkillModifier(AbilityModifier modifier, string skillSource) { AddModifier(modifier, skillSource); } - // Remove equipment modifiers when unequipping public void RemoveEquipmentModifiers(EquippableItem equipment) { RemoveAllModifiersFromSource(equipment); } - // Remove effect modifiers when effect expires public void RemoveEffectModifiers(BaseEffect effect) { RemoveAllModifiersFromSource(effect); } // ======================================================================== - // MODIFIER CALCULATION - // ======================================================================== - - private void RecalculateModifiedValues() - { - // Start with original values - manaCost = sourceAbility.manaCost; - healthCost = sourceAbility.healthCost; - classResourceCost = sourceAbility.classResourceCost; - spiritPowerReserveCost = sourceAbility.spiritPowerReserveCost; - percentMaxManaCost = sourceAbility.percentMaxManaCost; - percentMaxHealthCost = sourceAbility.percentMaxHealthCost; - castTime = sourceAbility.castTime; - cooldown = sourceAbility.cooldown; - - // Apply all modifiers - foreach (var modifier in activeModifiers) - { - ApplyModifier(modifier); - } - } - - private void ApplyModifier(AbilityModifier modifier) - { - // Resource cost modifiers - manaCost = manaCost * modifier.manaCostMultiplier + modifier.manaCostFlat; - healthCost = healthCost * modifier.healthCostMultiplier + modifier.healthCostFlat; - classResourceCost = classResourceCost * modifier.classResourceCostMultiplier + modifier.classResourceCostFlat; - - // Timing modifiers - castTime *= modifier.castTimeMultiplier; - cooldown *= modifier.cooldownMultiplier; - - // Ensure values don't go negative - manaCost = Mathf.Max(0f, manaCost); - healthCost = Mathf.Max(0f, healthCost); - classResourceCost = Mathf.Max(0f, classResourceCost); - castTime = Mathf.Max(0f, castTime); - cooldown = Mathf.Max(0f, cooldown); - } - - // ======================================================================== - // RUNTIME BEHAVIOR SYSTEM + // RUNTIME BEHAVIOR SYSTEM (same as before) // ======================================================================== public void AddBehavior(RuntimeBehavior behavior) @@ -333,31 +451,6 @@ public class RuntimeAbilityInstance } } - // ======================================================================== - // DEBUG/UTILITY METHODS - // ======================================================================== - - public void PrintActiveModifiers() - { - Debug.Log($"=== Active Modifiers for {displayName} ==="); - foreach (var modifier in activeModifiers) - { - string sourceStr = modifier.Source?.ToString() ?? "No Source"; - Debug.Log($"- {modifier.modifierName} (Source: {sourceStr})"); - } - } - - public Dictionary GetModifierCountBySource() - { - var counts = new Dictionary(); - foreach (var modifier in activeModifiers) - { - var source = modifier.Source ?? "No Source"; - counts[source] = counts.ContainsKey(source) ? counts[source] + 1 : 1; - } - return counts; - } - // ======================================================================== // UTILITY METHODS // ======================================================================== @@ -366,29 +459,48 @@ public class RuntimeAbilityInstance { var clone = new RuntimeAbilityInstance(sourceAbility); - // Copy modifiers foreach (var modifier in activeModifiers) - { clone.AddModifier(modifier); - } - // Copy behaviors foreach (var behavior in runtimeBehaviors) - { clone.AddBehavior(behavior.Clone()); - } return clone; } private int GetMaxCharges() { - // Future: add charge system - return 1; + return 1; // Future: add charge system } // ======================================================================== - // IMPLICIT CONVERSION - Makes it work seamlessly + // DEBUG METHODS + // ======================================================================== + + public void PrintCostAnalysis(CostCalculationContext? context = null) + { + Debug.Log($"=== Cost Analysis for {displayName} ==="); + + if (context.HasValue) + { + var ctx = context.Value; + Debug.Log($"With Context (Max Mana: {ctx.maxMana}, Max Health: {ctx.maxHealth}):"); + Debug.Log($" Final Mana Cost: {GetFinalManaCost(ctx):F1}"); + Debug.Log($" Final Health Cost: {GetFinalHealthCost(ctx):F1}"); + } + else + { + Debug.Log("Without Context:"); + Debug.Log($" Base Flat Mana: {baseManaFlat} + {baseManaPercent:P0} Max"); + Debug.Log($" Base Flat Health: {baseHealthFlat} + {baseHealthPercent:P0} Max"); + } + + Debug.Log($" Final Class Resource Cost: {GetFinalClassResourceCost():F1}"); + Debug.Log($" Active Modifiers: {activeModifiers.Count}"); + } + + // ======================================================================== + // IMPLICIT CONVERSION // ======================================================================== public static implicit operator BaseAbility(RuntimeAbilityInstance instance) diff --git a/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeBehavior.cs b/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeBehavior.cs index 7e5f2d16..46d0b4c0 100644 --- a/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeBehavior.cs +++ b/Assets/Scripts/AbilitySystem/AbilityInstance/RuntimeBehavior.cs @@ -3,6 +3,7 @@ using UnityEngine; public enum BehaviorTrigger { PreCast, // Before ability executes + Execute, // MAIN EXECUTION - replaces your override Execute() PostCast, // After ability executes OnHit, // When ability hits target OnKill, // When ability kills target diff --git a/Assets/Scripts/Player/AbilityKeyBinder.cs b/Assets/Scripts/Player/AbilityKeyBinder.cs index b0f2bdda..e076063a 100644 --- a/Assets/Scripts/Player/AbilityKeyBinder.cs +++ b/Assets/Scripts/Player/AbilityKeyBinder.cs @@ -39,6 +39,9 @@ public class AbilityKeyBinder : MonoBehaviour float finalHealthCost; float finalManaCost; + RuntimeAbilityInstance abilityInstance; + public RuntimeAbilityInstance AbilityInstance => abilityInstance; + private void Awake() { userTag = GetComponentInParent(); @@ -86,6 +89,17 @@ public class AbilityKeyBinder : MonoBehaviour if (abilityBindInstance != null) abilityBindInstance.pressed.SetActive(true); + if(abilityInstance != null) + { + if(abilityInstance.CanExecute(userTag)) + { + castingStateController.RequestAbilityCast(abilityInstance, () => + { + abilityInstance.Execute(userTag); + }); + } + } + else if (IsAbilityOffCooldown() && mana.EnoughMana(ability.GetFinalManaCost(mana)) && health.EnoughHealth(ability.GetFinalHealthCost(health))) { if (ability is ChanneledAbility) @@ -159,6 +173,7 @@ public class AbilityKeyBinder : MonoBehaviour public void SetupAbilityBindInstance(AbilityBindInstance abilityBindInstance) { + Debug.Log("#RACE: SETUP BIND INSTANCE"); this.abilityBindInstance = abilityBindInstance; mana.onResourceChanged.AddListener(OnManaChanged); health.onResourceChanged.AddListener(OnHealthChanged); @@ -167,15 +182,23 @@ public class AbilityKeyBinder : MonoBehaviour abilityBindInstance.ForceUpdateOnComboAbility(GetCurrentAbility()); } + [ContextMenu("debug Add manacost reduction")] + public void AddManaCostModifierDebug() + { + abilityInstance.AddModifier(AbilityModifier.CreateManaCostReduction(100f)); + } + public void OnManaChanged(float currentMana) { + if (ability == null || abilityInstance == null) return; + if (isComboAbility) { finalManaCost = GetCurrentAbility().GetFinalManaCost(mana); } else { - finalManaCost = ability.GetFinalManaCost(mana); + finalManaCost = abilityInstance.GetFinalManaCost(CostCalculationContext.FromUser(userTag)); } abilityBindInstance.manaCost.text = finalManaCost.ToString("F0"); abilityBindInstance.noMana.SetActive(!mana.EnoughMana(finalManaCost)); @@ -183,13 +206,15 @@ public class AbilityKeyBinder : MonoBehaviour public void OnHealthChanged(float currentHealth) { + if (ability == null || abilityInstance == null) return; + if (isComboAbility) { finalHealthCost = GetCurrentAbility().GetFinalHealthCost(health); } else { - finalHealthCost = ability.GetFinalHealthCost(health); + finalHealthCost = abilityInstance.GetFinalHealthCost(CostCalculationContext.FromUser(userTag)); } abilityBindInstance.healthCost.text = finalHealthCost.ToString("F0"); @@ -199,11 +224,15 @@ public class AbilityKeyBinder : MonoBehaviour public bool IsAbilityOffCooldown() { + if (abilityBindInstance != null) + return !abilityInstance.IsOnCooldown; + return ability.cooldown <= 0 || !cooldownTracker.OnCooldown(ability); } public void BindAbility(BaseAbility ability) { + Debug.Log("#RACE: BIND ABILITY"); this.ability = ability; if (ability is ComboAbility comboAbility) { @@ -213,6 +242,7 @@ public class AbilityKeyBinder : MonoBehaviour } else { + abilityInstance = new RuntimeAbilityInstance(ability); isComboAbility = false; combo = null; }