2025-06-22 22:31:01 +01:00

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);
}
}