523 lines
18 KiB
C#
523 lines
18 KiB
C#
using Kryz.CharacterStats.Examples;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
// ============================================================================
|
|
// RUNTIME ABILITY INSTANCE - Context-Aware Version
|
|
// ============================================================================
|
|
|
|
/// <summary>
|
|
/// Runtime wrapper for BaseAbility with context-aware cost calculations
|
|
/// Modifiers now properly affect total costs (flat + percentage combined)
|
|
/// </summary>
|
|
public class RuntimeAbilityInstance
|
|
{
|
|
// ========================================================================
|
|
// CORE DATA
|
|
// ========================================================================
|
|
|
|
[SerializeField] private BaseAbility sourceAbility;
|
|
private List<AbilityModifier> activeModifiers = new List<AbilityModifier>();
|
|
private List<RuntimeBehavior> runtimeBehaviors = new List<RuntimeBehavior>();
|
|
|
|
// Runtime state
|
|
private float lastUsedTime;
|
|
private int currentCharges;
|
|
|
|
// ========================================================================
|
|
// CONSTRUCTOR
|
|
// ========================================================================
|
|
|
|
public RuntimeAbilityInstance(BaseAbility source)
|
|
{
|
|
sourceAbility = source;
|
|
currentCharges = GetMaxCharges();
|
|
}
|
|
|
|
// ========================================================================
|
|
// PROPERTIES - Direct access to original values
|
|
// ========================================================================
|
|
|
|
public BaseAbility SourceAbility => sourceAbility;
|
|
public string displayName => sourceAbility.displayName;
|
|
public Sprite Icon => sourceAbility.Icon;
|
|
public List<TargetTag> targettingTags => sourceAbility.targettingTags;
|
|
public List<GameTag> tags => sourceAbility.tags;
|
|
public List<BaseEffect> abilityEffects => sourceAbility.abilityEffects;
|
|
public bool castableWhileMoving => sourceAbility.castableWhileMoving;
|
|
public AbilityAnimationType animationType => sourceAbility.animationType;
|
|
|
|
// 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;
|
|
public bool IsOnCooldown => Time.time < lastUsedTime + cooldown;
|
|
public float CooldownRemaining => Mathf.Max(0f, (lastUsedTime + cooldown) - Time.time);
|
|
public List<AbilityModifier> ActiveModifiers => new List<AbilityModifier>(activeModifiers);
|
|
public List<RuntimeBehavior> RuntimeBehaviors => new List<RuntimeBehavior>(runtimeBehaviors);
|
|
|
|
// ========================================================================
|
|
// 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)
|
|
{
|
|
if (userMana == null) userMana = user.GetComponent<Mana>();
|
|
if (userHealth == null) userHealth = user.GetComponent<Health>();
|
|
|
|
userMana.ChangeValue(-GetFinalManaCost(user));
|
|
userHealth.ChangeValue(-GetFinalHealthCost(user));
|
|
//user.GetComponent<ClassResource>()?.ChangeValue(-GetFinalClassResourceCost());
|
|
}
|
|
|
|
public virtual void Execute(Taggable user)
|
|
{
|
|
if (!CanExecute(user)) return;
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, Vector3.zero);
|
|
|
|
SpendResourcesNecessary(user);
|
|
ExecuteBehaviors(BehaviorTrigger.Execute, user, null, Vector3.zero);
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, Vector3.zero);
|
|
|
|
lastUsedTime = Time.time;
|
|
}
|
|
|
|
public virtual void Execute(Taggable user, Vector3 point)
|
|
{
|
|
if (!CanExecute(user)) return;
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, point);
|
|
|
|
SpendResourcesNecessary(user);
|
|
ExecuteBehaviors(BehaviorTrigger.Execute, user, null, point);
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, point);
|
|
|
|
lastUsedTime = Time.time;
|
|
}
|
|
|
|
public virtual void Execute(Taggable user, Transform target)
|
|
{
|
|
if (!CanExecute(user)) return;
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PreCast, user, target, target.position);
|
|
|
|
SpendResourcesNecessary(user);
|
|
ExecuteBehaviors(BehaviorTrigger.Execute, user, target, target.position);
|
|
|
|
ExecuteBehaviors(BehaviorTrigger.PostCast, user, target, target.position);
|
|
|
|
lastUsedTime = Time.time;
|
|
}
|
|
|
|
// ========================================================================
|
|
// ABILITY STATE CHECKS
|
|
// ========================================================================
|
|
|
|
public bool CanExecute(Taggable user)
|
|
{
|
|
if (IsOnCooldown) return false;
|
|
return CanAffordResources(CostCalculationContext.FromUser(user));
|
|
}
|
|
|
|
public bool CanAffordResources(CostCalculationContext context)
|
|
{
|
|
// Check mana cost
|
|
if (context.maxMana > 0)
|
|
{
|
|
float finalManaCost = GetFinalManaCost(context);
|
|
if (context.currentMana < finalManaCost) return false;
|
|
}
|
|
|
|
// Check health cost
|
|
if (context.maxHealth > 0)
|
|
{
|
|
float finalHealthCost = GetFinalHealthCost(context);
|
|
if (context.currentHealth <= finalHealthCost) return false;
|
|
}
|
|
|
|
// Check class resource cost
|
|
if (context.currentClassResource > 0)
|
|
{
|
|
float finalClassResourceCost = GetFinalClassResourceCost();
|
|
if (context.currentClassResource < finalClassResourceCost) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Convenience overload
|
|
public bool CanAffordResources(Taggable user) => CanAffordResources(CostCalculationContext.FromUser(user));
|
|
|
|
// ========================================================================
|
|
// MODIFIER CALCULATION HELPERS
|
|
// ========================================================================
|
|
|
|
private float GetTotalManaCostMultiplier()
|
|
{
|
|
float multiplier = 1f;
|
|
foreach (var modifier in activeModifiers)
|
|
multiplier *= modifier.manaCostMultiplier;
|
|
return multiplier;
|
|
}
|
|
|
|
private float GetTotalManaCostFlat()
|
|
{
|
|
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);
|
|
}
|
|
|
|
// ========================================================================
|
|
// MODIFIER MANAGEMENT (same as before)
|
|
// ========================================================================
|
|
|
|
public void AddModifier(AbilityModifier modifier)
|
|
{
|
|
activeModifiers.Add(modifier);
|
|
}
|
|
|
|
public void AddModifier(AbilityModifier modifier, object source)
|
|
{
|
|
var modifierWithSource = new AbilityModifier(modifier, source);
|
|
activeModifiers.Add(modifierWithSource);
|
|
}
|
|
|
|
public void RemoveModifier(AbilityModifier modifier)
|
|
{
|
|
activeModifiers.Remove(modifier);
|
|
}
|
|
|
|
public bool RemoveAllModifiersFromSource(object source)
|
|
{
|
|
int numRemovals = activeModifiers.RemoveAll(mod => mod.Source != null && mod.Source.Equals(source));
|
|
return numRemovals > 0;
|
|
}
|
|
|
|
public bool HasModifiersFromSource(object source)
|
|
{
|
|
return activeModifiers.Any(mod => mod.Source != null && mod.Source.Equals(source));
|
|
}
|
|
|
|
public List<AbilityModifier> GetModifiersFromSource(object source)
|
|
{
|
|
return activeModifiers.Where(mod => mod.Source != null && mod.Source.Equals(source)).ToList();
|
|
}
|
|
|
|
public void RemoveAllModifiers()
|
|
{
|
|
activeModifiers.Clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CONVENIENCE METHODS
|
|
// ========================================================================
|
|
|
|
public void AddEquipmentModifier(AbilityModifier modifier, EquippableItem equipment)
|
|
{
|
|
AddModifier(modifier, equipment);
|
|
}
|
|
|
|
public void AddEffectModifier(AbilityModifier modifier, BaseEffect effect)
|
|
{
|
|
AddModifier(modifier, effect);
|
|
}
|
|
|
|
public void AddSkillModifier(AbilityModifier modifier, string skillSource)
|
|
{
|
|
AddModifier(modifier, skillSource);
|
|
}
|
|
|
|
public void RemoveEquipmentModifiers(EquippableItem equipment)
|
|
{
|
|
RemoveAllModifiersFromSource(equipment);
|
|
}
|
|
|
|
public void RemoveEffectModifiers(BaseEffect effect)
|
|
{
|
|
RemoveAllModifiersFromSource(effect);
|
|
}
|
|
|
|
// ========================================================================
|
|
// RUNTIME BEHAVIOR SYSTEM (same as before)
|
|
// ========================================================================
|
|
|
|
public void AddBehavior(RuntimeBehavior behavior)
|
|
{
|
|
runtimeBehaviors.Add(behavior);
|
|
}
|
|
|
|
public void RemoveBehavior(RuntimeBehavior behavior)
|
|
{
|
|
runtimeBehaviors.Remove(behavior);
|
|
}
|
|
|
|
public T GetBehavior<T>() where T : RuntimeBehavior
|
|
{
|
|
return runtimeBehaviors.Find(b => b is T) as T;
|
|
}
|
|
|
|
public void RemoveBehavior<T>() where T : RuntimeBehavior
|
|
{
|
|
for (int i = runtimeBehaviors.Count - 1; i >= 0; i--)
|
|
{
|
|
if (runtimeBehaviors[i] is T)
|
|
{
|
|
runtimeBehaviors.RemoveAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ExecuteBehaviors(BehaviorTrigger trigger, Taggable user, Transform target, Vector3 point)
|
|
{
|
|
foreach (var behavior in runtimeBehaviors)
|
|
{
|
|
if (behavior.Trigger == trigger)
|
|
{
|
|
behavior.Execute(this, user, target, point);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// UTILITY METHODS
|
|
// ========================================================================
|
|
|
|
public RuntimeAbilityInstance Clone()
|
|
{
|
|
var clone = new RuntimeAbilityInstance(sourceAbility);
|
|
|
|
foreach (var modifier in activeModifiers)
|
|
clone.AddModifier(modifier);
|
|
|
|
foreach (var behavior in runtimeBehaviors)
|
|
clone.AddBehavior(behavior.Clone());
|
|
|
|
return clone;
|
|
}
|
|
|
|
private int GetMaxCharges()
|
|
{
|
|
return 1; // Future: add charge system
|
|
}
|
|
|
|
// ========================================================================
|
|
// 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)
|
|
{
|
|
return instance.sourceAbility;
|
|
}
|
|
|
|
public static implicit operator RuntimeAbilityInstance(BaseAbility ability)
|
|
{
|
|
return new RuntimeAbilityInstance(ability);
|
|
}
|
|
} |