Ability Runtime instances, modifiers and behaviours (WIP)

This commit is contained in:
Pedro Gomes 2025-06-19 19:46:29 +01:00
parent d7eb479da7
commit c4d084acb2
10 changed files with 434 additions and 153 deletions

View File

@ -236,7 +236,7 @@ AnimatorStateTransition:
m_HasFixedDuration: 1 m_HasFixedDuration: 1
m_InterruptionSource: 0 m_InterruptionSource: 0
m_OrderedInterruption: 1 m_OrderedInterruption: 1
m_CanTransitionToSelf: 1 m_CanTransitionToSelf: 0
--- !u!1101 &-3316037427580542980 --- !u!1101 &-3316037427580542980
AnimatorStateTransition: AnimatorStateTransition:
m_ObjectHideFlags: 1 m_ObjectHideFlags: 1
@ -564,9 +564,9 @@ AnimatorStateTransition:
m_Mute: 0 m_Mute: 0
m_IsExit: 0 m_IsExit: 0
serializedVersion: 3 serializedVersion: 3
m_TransitionDuration: 0.2 m_TransitionDuration: 0.15
m_TransitionOffset: 0 m_TransitionOffset: 0
m_ExitTime: 0.8 m_ExitTime: 0.5
m_HasExitTime: 1 m_HasExitTime: 1
m_HasFixedDuration: 1 m_HasFixedDuration: 1
m_InterruptionSource: 0 m_InterruptionSource: 0

View File

@ -78,7 +78,7 @@ Material:
- _Mode: 0 - _Mode: 0
- _OcclusionStrength: 1 - _OcclusionStrength: 1
- _Parallax: 0.02 - _Parallax: 0.02
- _Rotation: 7.162766 - _Rotation: 6.1873918
- _SmoothnessTextureChannel: 0 - _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1 - _SpecularHighlights: 1
- _SrcBlend: 1 - _SrcBlend: 1

View File

@ -2301,6 +2301,26 @@ PrefabInstance:
serializedVersion: 3 serializedVersion: 3
m_TransformParent: {fileID: 0} m_TransformParent: {fileID: 0}
m_Modifications: 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} - target: {fileID: 952476992021474036, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 0 value: 0
@ -2429,6 +2449,26 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -600 value: -600
objectReference: {fileID: 0} 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} - target: {fileID: 3044532144327822992, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 0 value: 0
@ -2513,6 +2553,26 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 0 value: 0
objectReference: {fileID: 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} - target: {fileID: 6069245676466788546, guid: 7cf303e1116e7fb46ba92e7d73321eeb, type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 0 value: 0

View File

@ -29,7 +29,7 @@ MonoBehaviour:
- {fileID: 11400000, guid: 7b7a9c9ae4b8ece4eb680f621937c58a, type: 2} - {fileID: 11400000, guid: 7b7a9c9ae4b8ece4eb680f621937c58a, type: 2}
- {fileID: 11400000, guid: 127f92fd14a21174695a692c0cf18746, type: 2} - {fileID: 11400000, guid: 127f92fd14a21174695a692c0cf18746, type: 2}
castTime: 0.5 castTime: 0.5
manaCost: 3 manaCost: 30
healthCost: 0 healthCost: 0
classResourceCost: 0 classResourceCost: 0
spiritPowerReserveCost: 0 spiritPowerReserveCost: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ebbaf77cf9aa1bf4686e2835b46e8e66
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,59 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ============================================================================
// COST CALCULATION CONTEXT - Holds user stats for cost calculations
// ============================================================================
/// <summary>
/// Context containing user stats needed for cost calculations
/// Allows ability cost calculation without direct user reference
/// </summary>
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<Mana>();
if (mana != null)
{
context.maxMana = mana.GetMaxValue();
context.currentMana = mana.GetCurrentValue();
}
var health = user.GetComponent<Health>();
if (health != null)
{
context.maxHealth = health.GetMaxValue();
context.currentHealth = health.GetCurrentValue();
}
var classResource = user.GetComponent<ClassResource>();
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
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5050e09038372134fbe8fa068698e68a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,25 +1,20 @@
// ============================================================================
// RUNTIME ABILITY WRAPPER - DROP-IN REPLACEMENT FOR BaseAbility
// ============================================================================
using Kryz.CharacterStats.Examples; using Kryz.CharacterStats.Examples;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
// ============================================================================ // ============================================================================
// 1. RUNTIME ABILITY INSTANCE - Wraps Your Existing BaseAbility // RUNTIME ABILITY INSTANCE - Context-Aware Version
// ============================================================================ // ============================================================================
/// <summary> /// <summary>
/// Runtime wrapper for BaseAbility that adds modifiers and behaviors /// Runtime wrapper for BaseAbility with context-aware cost calculations
/// Can be used anywhere you currently use BaseAbility references /// Modifiers now properly affect total costs (flat + percentage combined)
/// </summary> /// </summary>
public class RuntimeAbilityInstance public class RuntimeAbilityInstance
{ {
// ======================================================================== // ========================================================================
// CORE DATA - Wraps your existing ScriptableObject // CORE DATA
// ======================================================================== // ========================================================================
[SerializeField] private BaseAbility sourceAbility; [SerializeField] private BaseAbility sourceAbility;
@ -37,15 +32,13 @@ public class RuntimeAbilityInstance
public RuntimeAbilityInstance(BaseAbility source) public RuntimeAbilityInstance(BaseAbility source)
{ {
sourceAbility = source; sourceAbility = source;
currentCharges = GetMaxCharges(); // In case you add charges later currentCharges = GetMaxCharges();
RecalculateModifiedValues();
} }
// ======================================================================== // ========================================================================
// PROPERTIES - Direct access to original + modified values // PROPERTIES - Direct access to original values
// ======================================================================== // ========================================================================
// Original values (unchanged)
public BaseAbility SourceAbility => sourceAbility; public BaseAbility SourceAbility => sourceAbility;
public string displayName => sourceAbility.displayName; public string displayName => sourceAbility.displayName;
public Sprite Icon => sourceAbility.Icon; public Sprite Icon => sourceAbility.Icon;
@ -55,15 +48,18 @@ public class RuntimeAbilityInstance
public bool castableWhileMoving => sourceAbility.castableWhileMoving; public bool castableWhileMoving => sourceAbility.castableWhileMoving;
public AbilityAnimationType animationType => sourceAbility.animationType; public AbilityAnimationType animationType => sourceAbility.animationType;
// Modified values (affected by modifiers) // Base costs (for display when no context available)
public float manaCost { get; private set; } public float baseManaFlat => sourceAbility.manaCost;
public float healthCost { get; private set; } public float baseHealthFlat => sourceAbility.healthCost;
public float classResourceCost { get; private set; } public float baseClassResourceFlat => sourceAbility.classResourceCost;
public float spiritPowerReserveCost { get; private set; } public float baseManaPercent => sourceAbility.percentMaxManaCost;
public float percentMaxManaCost { get; private set; } public float baseHealthPercent => sourceAbility.percentMaxHealthCost;
public float percentMaxHealthCost { get; private set; } public float baseCastTime => sourceAbility.castTime;
public float castTime { get; private set; } public float baseCooldown => sourceAbility.cooldown;
public float cooldown { get; private set; }
// Modified timing values (these don't need user context)
public float castTime => GetModifiedCastTime();
public float cooldown => GetModifiedCooldown();
// Runtime state properties // Runtime state properties
public float LastUsedTime => lastUsedTime; public float LastUsedTime => lastUsedTime;
@ -73,23 +69,143 @@ public class RuntimeAbilityInstance
public List<RuntimeBehavior> RuntimeBehaviors => new List<RuntimeBehavior>(runtimeBehaviors); public List<RuntimeBehavior> RuntimeBehaviors => new List<RuntimeBehavior>(runtimeBehaviors);
// ======================================================================== // ========================================================================
// EXECUTION METHODS - Same signature as your BaseAbility // CONTEXT-AWARE COST CALCULATIONS
// ======================================================================== // ========================================================================
/// <summary>
/// Calculate final mana cost including modifiers (affects flat + percentage total)
/// </summary>
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);
}
/// <summary>
/// Calculate final health cost including modifiers (affects flat + percentage total)
/// </summary>
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);
}
/// <summary>
/// Calculate final class resource cost (flat only, no percentage version)
/// </summary>
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
// ========================================================================
/// <summary>
/// Get display-friendly mana cost text
/// </summary>
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";
}
}
/// <summary>
/// Get display-friendly health cost text
/// </summary>
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";
}
}
/// <summary>
/// Get all cost information as formatted string
/// </summary>
public string GetAllCostsDisplayText(CostCalculationContext? context = null)
{
var costs = new List<string>();
// 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<ClassResource>()?.ChangeValue(-GetFinalClassResourceCost());
}
public virtual void Execute(Taggable user) public virtual void Execute(Taggable user)
{ {
if (!CanExecute(user)) return; if (!CanExecute(user)) return;
// Execute pre-cast behaviors
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, Vector3.zero); ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, Vector3.zero);
SpendResourcesNecessary(user);
// Execute original ability
sourceAbility.Execute(user); sourceAbility.Execute(user);
// Execute post-cast behaviors
ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, Vector3.zero); ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, Vector3.zero);
// Update runtime state
lastUsedTime = Time.time; lastUsedTime = Time.time;
} }
@ -98,6 +214,7 @@ public class RuntimeAbilityInstance
if (!CanExecute(user)) return; if (!CanExecute(user)) return;
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, point); ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, point);
SpendResourcesNecessary(user);
sourceAbility.Execute(user, point); sourceAbility.Execute(user, point);
ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, point); ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, point);
@ -109,6 +226,7 @@ public class RuntimeAbilityInstance
if (!CanExecute(user)) return; if (!CanExecute(user)) return;
ExecuteBehaviors(BehaviorTrigger.PreCast, user, target, target.position); ExecuteBehaviors(BehaviorTrigger.PreCast, user, target, target.position);
SpendResourcesNecessary(user);
sourceAbility.Execute(user, target); sourceAbility.Execute(user, target);
ExecuteBehaviors(BehaviorTrigger.PostCast, user, target, target.position); ExecuteBehaviors(BehaviorTrigger.PostCast, user, target, target.position);
@ -121,83 +239,131 @@ public class RuntimeAbilityInstance
public bool CanExecute(Taggable user) public bool CanExecute(Taggable user)
{ {
// Check cooldown
if (IsOnCooldown) return false; if (IsOnCooldown) return false;
return CanAffordResources(CostCalculationContext.FromUser(user));
// Check resources using modified costs
return CanAffordResources(user);
} }
private bool CanAffordResources(Taggable user) public bool CanAffordResources(CostCalculationContext context)
{ {
// Use the modified costs, not original // Check mana cost
var userMana = user.GetComponent<Mana>(); if (context.maxMana > 0)
if (userMana != null)
{ {
float finalManaCost = manaCost + userMana.GetMaxValue() * percentMaxManaCost; float finalManaCost = GetFinalManaCost(context);
if (!userMana.EnoughMana(finalManaCost)) return false; if (context.currentMana < finalManaCost) return false;
} }
var userHealth = user.GetComponent<Health>(); // Check health cost
if (userHealth != null) if (context.maxHealth > 0)
{ {
float finalHealthCost = healthCost + userHealth.GetMaxValue() * percentMaxHealthCost; float finalHealthCost = GetFinalHealthCost(context);
if (userHealth.GetCurrentValue() <= finalHealthCost) return false; if (context.currentHealth <= finalHealthCost) return false;
} }
var userClassResource = user.GetComponent<ClassResource>(); // Check class resource cost
if (userClassResource != null && classResourceCost > 0) if (context.currentClassResource > 0)
{ {
if (userClassResource.GetCurrentValue() < classResourceCost) return false; float finalClassResourceCost = GetFinalClassResourceCost();
if (context.currentClassResource < finalClassResourceCost) return false;
} }
return true; 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) public void AddModifier(AbilityModifier modifier)
{ {
activeModifiers.Add(modifier); activeModifiers.Add(modifier);
RecalculateModifiedValues();
} }
public void AddModifier(AbilityModifier modifier, object source) public void AddModifier(AbilityModifier modifier, object source)
{ {
// Create a copy with the specified source
var modifierWithSource = new AbilityModifier(modifier, source); var modifierWithSource = new AbilityModifier(modifier, source);
activeModifiers.Add(modifierWithSource); activeModifiers.Add(modifierWithSource);
RecalculateModifiedValues();
} }
public void RemoveModifier(AbilityModifier modifier) public void RemoveModifier(AbilityModifier modifier)
{ {
activeModifiers.Remove(modifier); activeModifiers.Remove(modifier);
RecalculateModifiedValues();
} }
public bool RemoveAllModifiersFromSource(object source) public bool RemoveAllModifiersFromSource(object source)
{ {
int numRemovals = activeModifiers.RemoveAll(mod => mod.Source != null && mod.Source.Equals(source)); int numRemovals = activeModifiers.RemoveAll(mod => mod.Source != null && mod.Source.Equals(source));
return numRemovals > 0;
if (numRemovals > 0)
{
RecalculateModifiedValues();
return true;
}
return false;
} }
public bool HasModifiersFromSource(object source) public bool HasModifiersFromSource(object source)
@ -213,87 +379,39 @@ public class RuntimeAbilityInstance
public void RemoveAllModifiers() public void RemoveAllModifiers()
{ {
activeModifiers.Clear(); activeModifiers.Clear();
RecalculateModifiedValues();
} }
// ======================================================================== // ========================================================================
// CONVENIENCE METHODS - Common source-based operations // CONVENIENCE METHODS
// ======================================================================== // ========================================================================
// Add modifier from equipment
public void AddEquipmentModifier(AbilityModifier modifier, EquippableItem equipment) public void AddEquipmentModifier(AbilityModifier modifier, EquippableItem equipment)
{ {
AddModifier(modifier, equipment); AddModifier(modifier, equipment);
} }
// Add modifier from buff/effect
public void AddEffectModifier(AbilityModifier modifier, BaseEffect effect) public void AddEffectModifier(AbilityModifier modifier, BaseEffect effect)
{ {
AddModifier(modifier, effect); AddModifier(modifier, effect);
} }
// Add modifier from skill/talent
public void AddSkillModifier(AbilityModifier modifier, string skillSource) public void AddSkillModifier(AbilityModifier modifier, string skillSource)
{ {
AddModifier(modifier, skillSource); AddModifier(modifier, skillSource);
} }
// Remove equipment modifiers when unequipping
public void RemoveEquipmentModifiers(EquippableItem equipment) public void RemoveEquipmentModifiers(EquippableItem equipment)
{ {
RemoveAllModifiersFromSource(equipment); RemoveAllModifiersFromSource(equipment);
} }
// Remove effect modifiers when effect expires
public void RemoveEffectModifiers(BaseEffect effect) public void RemoveEffectModifiers(BaseEffect effect)
{ {
RemoveAllModifiersFromSource(effect); RemoveAllModifiersFromSource(effect);
} }
// ======================================================================== // ========================================================================
// MODIFIER CALCULATION // RUNTIME BEHAVIOR SYSTEM (same as before)
// ========================================================================
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
// ======================================================================== // ========================================================================
public void AddBehavior(RuntimeBehavior behavior) 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<object, int> GetModifierCountBySource()
{
var counts = new Dictionary<object, int>();
foreach (var modifier in activeModifiers)
{
var source = modifier.Source ?? "No Source";
counts[source] = counts.ContainsKey(source) ? counts[source] + 1 : 1;
}
return counts;
}
// ======================================================================== // ========================================================================
// UTILITY METHODS // UTILITY METHODS
// ======================================================================== // ========================================================================
@ -366,29 +459,48 @@ public class RuntimeAbilityInstance
{ {
var clone = new RuntimeAbilityInstance(sourceAbility); var clone = new RuntimeAbilityInstance(sourceAbility);
// Copy modifiers
foreach (var modifier in activeModifiers) foreach (var modifier in activeModifiers)
{
clone.AddModifier(modifier); clone.AddModifier(modifier);
}
// Copy behaviors
foreach (var behavior in runtimeBehaviors) foreach (var behavior in runtimeBehaviors)
{
clone.AddBehavior(behavior.Clone()); clone.AddBehavior(behavior.Clone());
}
return clone; return clone;
} }
private int GetMaxCharges() private int GetMaxCharges()
{ {
// Future: add charge system return 1; // Future: add charge system
return 1;
} }
// ======================================================================== // ========================================================================
// 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) public static implicit operator BaseAbility(RuntimeAbilityInstance instance)

View File

@ -3,6 +3,7 @@ using UnityEngine;
public enum BehaviorTrigger public enum BehaviorTrigger
{ {
PreCast, // Before ability executes PreCast, // Before ability executes
Execute, // MAIN EXECUTION - replaces your override Execute()
PostCast, // After ability executes PostCast, // After ability executes
OnHit, // When ability hits target OnHit, // When ability hits target
OnKill, // When ability kills target OnKill, // When ability kills target

View File

@ -39,6 +39,9 @@ public class AbilityKeyBinder : MonoBehaviour
float finalHealthCost; float finalHealthCost;
float finalManaCost; float finalManaCost;
RuntimeAbilityInstance abilityInstance;
public RuntimeAbilityInstance AbilityInstance => abilityInstance;
private void Awake() private void Awake()
{ {
userTag = GetComponentInParent<Taggable>(); userTag = GetComponentInParent<Taggable>();
@ -86,6 +89,17 @@ public class AbilityKeyBinder : MonoBehaviour
if (abilityBindInstance != null) if (abilityBindInstance != null)
abilityBindInstance.pressed.SetActive(true); 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 (IsAbilityOffCooldown() && mana.EnoughMana(ability.GetFinalManaCost(mana)) && health.EnoughHealth(ability.GetFinalHealthCost(health)))
{ {
if (ability is ChanneledAbility) if (ability is ChanneledAbility)
@ -159,6 +173,7 @@ public class AbilityKeyBinder : MonoBehaviour
public void SetupAbilityBindInstance(AbilityBindInstance abilityBindInstance) public void SetupAbilityBindInstance(AbilityBindInstance abilityBindInstance)
{ {
Debug.Log("#RACE: SETUP BIND INSTANCE");
this.abilityBindInstance = abilityBindInstance; this.abilityBindInstance = abilityBindInstance;
mana.onResourceChanged.AddListener(OnManaChanged); mana.onResourceChanged.AddListener(OnManaChanged);
health.onResourceChanged.AddListener(OnHealthChanged); health.onResourceChanged.AddListener(OnHealthChanged);
@ -167,15 +182,23 @@ public class AbilityKeyBinder : MonoBehaviour
abilityBindInstance.ForceUpdateOnComboAbility(GetCurrentAbility()); abilityBindInstance.ForceUpdateOnComboAbility(GetCurrentAbility());
} }
[ContextMenu("debug Add manacost reduction")]
public void AddManaCostModifierDebug()
{
abilityInstance.AddModifier(AbilityModifier.CreateManaCostReduction(100f));
}
public void OnManaChanged(float currentMana) public void OnManaChanged(float currentMana)
{ {
if (ability == null || abilityInstance == null) return;
if (isComboAbility) if (isComboAbility)
{ {
finalManaCost = GetCurrentAbility().GetFinalManaCost(mana); finalManaCost = GetCurrentAbility().GetFinalManaCost(mana);
} }
else else
{ {
finalManaCost = ability.GetFinalManaCost(mana); finalManaCost = abilityInstance.GetFinalManaCost(CostCalculationContext.FromUser(userTag));
} }
abilityBindInstance.manaCost.text = finalManaCost.ToString("F0"); abilityBindInstance.manaCost.text = finalManaCost.ToString("F0");
abilityBindInstance.noMana.SetActive(!mana.EnoughMana(finalManaCost)); abilityBindInstance.noMana.SetActive(!mana.EnoughMana(finalManaCost));
@ -183,13 +206,15 @@ public class AbilityKeyBinder : MonoBehaviour
public void OnHealthChanged(float currentHealth) public void OnHealthChanged(float currentHealth)
{ {
if (ability == null || abilityInstance == null) return;
if (isComboAbility) if (isComboAbility)
{ {
finalHealthCost = GetCurrentAbility().GetFinalHealthCost(health); finalHealthCost = GetCurrentAbility().GetFinalHealthCost(health);
} }
else else
{ {
finalHealthCost = ability.GetFinalHealthCost(health); finalHealthCost = abilityInstance.GetFinalHealthCost(CostCalculationContext.FromUser(userTag));
} }
abilityBindInstance.healthCost.text = finalHealthCost.ToString("F0"); abilityBindInstance.healthCost.text = finalHealthCost.ToString("F0");
@ -199,11 +224,15 @@ public class AbilityKeyBinder : MonoBehaviour
public bool IsAbilityOffCooldown() public bool IsAbilityOffCooldown()
{ {
if (abilityBindInstance != null)
return !abilityInstance.IsOnCooldown;
return ability.cooldown <= 0 || !cooldownTracker.OnCooldown(ability); return ability.cooldown <= 0 || !cooldownTracker.OnCooldown(ability);
} }
public void BindAbility(BaseAbility ability) public void BindAbility(BaseAbility ability)
{ {
Debug.Log("#RACE: BIND ABILITY");
this.ability = ability; this.ability = ability;
if (ability is ComboAbility comboAbility) if (ability is ComboAbility comboAbility)
{ {
@ -213,6 +242,7 @@ public class AbilityKeyBinder : MonoBehaviour
} }
else else
{ {
abilityInstance = new RuntimeAbilityInstance(ability);
isComboAbility = false; isComboAbility = false;
combo = null; combo = null;
} }