Runtime ability system

This commit is contained in:
Pedro Gomes 2025-06-13 21:56:16 +01:00
parent 226eae666e
commit 00b86fa97c
18 changed files with 761 additions and 1 deletions

View File

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

View File

@ -0,0 +1,52 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ============================================================================
// 5. EASY MIGRATION HELPER - Update Your Existing Components
// ============================================================================
/// <summary>
/// Helper to easily convert your existing ability references
/// Just change "BaseAbility" to "RuntimeAbilityInstance" in your existing scripts
/// </summary>
public static class AbilityMigrationHelper
{
/// <summary>
/// Convert a BaseAbility to RuntimeAbilityInstance
/// </summary>
public static RuntimeAbilityInstance ToRuntime(this BaseAbility ability)
{
return new RuntimeAbilityInstance(ability);
}
/// <summary>
/// Create a modified version of an ability
/// </summary>
public static RuntimeAbilityInstance CreateModifiedVersion(BaseAbility baseAbility, params AbilityModifier[] modifiers)
{
var runtime = new RuntimeAbilityInstance(baseAbility);
foreach (var modifier in modifiers)
{
runtime.AddModifier(modifier);
}
return runtime;
}
/// <summary>
/// Create legendary version with special behaviors
/// </summary>
public static RuntimeAbilityInstance CreateLegendaryVersion(BaseAbility baseAbility, params RuntimeBehavior[] behaviors)
{
var runtime = new RuntimeAbilityInstance(baseAbility);
foreach (var behavior in behaviors)
{
runtime.AddBehavior(behavior);
}
return runtime;
}
}

View File

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

View File

@ -0,0 +1,60 @@
// ============================================================================
// 2. ABILITY MODIFIER SYSTEM
// ============================================================================
using UnityEngine;
[System.Serializable]
public class AbilityModifier
{
[Header("Resource Cost Modifiers")]
public float manaCostMultiplier = 1f;
public float manaCostFlat = 0f;
public float healthCostMultiplier = 1f;
public float healthCostFlat = 0f;
public float classResourceCostMultiplier = 1f;
public float classResourceCostFlat = 0f;
[Header("Timing Modifiers")]
public float castTimeMultiplier = 1f;
public float cooldownMultiplier = 1f;
[Header("Effect Modifiers")]
public float damageMultiplier = 1f;
public float healingMultiplier = 1f;
public float durationMultiplier = 1f;
public float rangeMultiplier = 1f;
[Header("Meta Info")]
public string modifierName = "Unnamed Modifier";
public string description = "";
public float duration = -1f; // -1 = permanent
// Factory methods for common modifiers
public static AbilityModifier CreateDamageBoost(float multiplier, string name = "Damage Boost")
{
return new AbilityModifier
{
damageMultiplier = multiplier,
modifierName = name
};
}
public static AbilityModifier CreateCooldownReduction(float reductionPercent, string name = "Cooldown Reduction")
{
return new AbilityModifier
{
cooldownMultiplier = 1f - (reductionPercent / 100f),
modifierName = name
};
}
public static AbilityModifier CreateManaCostReduction(float reductionPercent, string name = "Mana Cost Reduction")
{
return new AbilityModifier
{
manaCostMultiplier = 1f - (reductionPercent / 100f),
modifierName = name
};
}
}

View File

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

View File

@ -0,0 +1,59 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ============================================================================
// 6. USAGE EXAMPLES - How to Use in Your Existing Code
// ============================================================================
public class AbilityUsageExamples : MonoBehaviour
{
[Header("Your Existing Abilities")]
public BaseAbility fireballAbility;
public BaseAbility healingAbility;
// These can now be RuntimeAbilityInstances!
private RuntimeAbilityInstance runtimeFireball;
private RuntimeAbilityInstance runtimeHealing;
private void Start()
{
// Convert your existing abilities
runtimeFireball = fireballAbility.ToRuntime();
runtimeHealing = healingAbility.ToRuntime();
// Example: Add legendary item effect
AddLegendaryFireballEffect();
}
private void AddLegendaryFireballEffect()
{
// This is how you'd handle your legendary item!
var explosionBehavior = new ExplodeOnHitBehavior
{
explosionRadius = 5f,
explosionDamage = 50f
};
runtimeFireball.AddBehavior(explosionBehavior);
// Also add a damage boost
var damageBoost = AbilityModifier.CreateDamageBoost(1.5f, "Legendary Damage");
runtimeFireball.AddModifier(damageBoost);
}
// Your existing ability usage code works exactly the same!
public void CastFireball()
{
var user = GetComponent<Taggable>();
// This works exactly like your old BaseAbility.Execute()
runtimeFireball.Execute(user);
}
public void CastFireballAtTarget(Transform target)
{
var user = GetComponent<Taggable>();
runtimeFireball.Execute(user, target);
}
}

View File

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

View File

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

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using UnityEngine;
public class ChainLightningBehavior : RuntimeBehavior
{
public int maxChains = 3;
public float chainRange = 8f;
public float damageMultiplier = 0.5f; // Each chain does 50% of original damage
public ChainLightningBehavior()
{
Trigger = BehaviorTrigger.OnHit;
BehaviorName = "Chain Lightning";
}
public override void Execute(RuntimeAbilityInstance ability, Taggable user, Transform target, Vector3 point)
{
if (target == null) return;
var currentTarget = target.GetComponent<Taggable>();
var chainedTargets = new List<Taggable> { currentTarget };
for (int i = 0; i < maxChains; i++)
{
var nextTarget = FindNextChainTarget(currentTarget.transform.position, chainedTargets);
if (nextTarget == null) break;
chainedTargets.Add(nextTarget);
// Apply reduced damage
var health = nextTarget.GetComponent<Health>();
if (health != null)
{
float chainDamage = CalculateChainDamage(ability, i + 1);
health.ChangeValue(-chainDamage);
}
currentTarget = nextTarget;
}
}
private Taggable FindNextChainTarget(Vector3 position, List<Taggable> excludeTargets)
{
var colliders = Physics.OverlapSphere(position, chainRange);
foreach (var collider in colliders)
{
var target = collider.GetComponent<Taggable>();
if (target != null && !excludeTargets.Contains(target))
{
return target;
}
}
return null;
}
private float CalculateChainDamage(RuntimeAbilityInstance ability, int chainNumber)
{
// This would need to be adapted to your damage system
// For now, just return a reduced amount
return 50f * Mathf.Pow(damageMultiplier, chainNumber);
}
public override RuntimeBehavior Clone()
{
return new ChainLightningBehavior
{
maxChains = this.maxChains,
chainRange = this.chainRange,
damageMultiplier = this.damageMultiplier
};
}
}

View File

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

View File

@ -0,0 +1,58 @@
// ============================================================================
// 4. COMMON RUNTIME BEHAVIORS - Your Legendary Item Effects!
// ============================================================================
using UnityEngine;
public class ExplodeOnHitBehavior : RuntimeBehavior
{
public GameObject explosionPrefab;
public float explosionRadius = 5f;
public float explosionDamage = 30f;
public LayerMask targetLayers = -1;
public ExplodeOnHitBehavior()
{
Trigger = BehaviorTrigger.OnHit;
BehaviorName = "Explode on Hit";
}
public override void Execute(RuntimeAbilityInstance ability, Taggable user, Transform target, Vector3 point)
{
Vector3 explosionPoint = target != null ? target.position : point;
// Spawn explosion visual if prefab exists
if (explosionPrefab != null)
{
Object.Instantiate(explosionPrefab, explosionPoint, Quaternion.identity);
}
// Find targets in explosion radius
var colliders = Physics.OverlapSphere(explosionPoint, explosionRadius, targetLayers);
foreach (var collider in colliders)
{
var explosionTarget = collider.GetComponent<Taggable>();
if (explosionTarget != null && explosionTarget != user)
{
// Apply explosion damage
var health = explosionTarget.GetComponent<Health>();
if (health != null)
{
health.ChangeValue(-explosionDamage);
}
}
}
}
public override RuntimeBehavior Clone()
{
return new ExplodeOnHitBehavior
{
explosionPrefab = this.explosionPrefab,
explosionRadius = this.explosionRadius,
explosionDamage = this.explosionDamage,
targetLayers = this.targetLayers
};
}
}

View File

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

View File

@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LifeStealBehavior : RuntimeBehavior
{
public float lifeStealPercent = 0.15f; // 15% life steal
public LifeStealBehavior()
{
Trigger = BehaviorTrigger.OnHit;
BehaviorName = "Life Steal";
}
public override void Execute(RuntimeAbilityInstance ability, Taggable user, Transform target, Vector3 point)
{
// This would need to track the damage dealt to calculate life steal
// For now, just heal a fixed amount
var userHealth = user.GetComponent<Health>();
if (userHealth != null)
{
float healAmount = 20f * lifeStealPercent; // Placeholder calculation
userHealth.ChangeValue(healAmount);
}
}
public override RuntimeBehavior Clone()
{
return new LifeStealBehavior
{
lifeStealPercent = this.lifeStealPercent
};
}
}

View File

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

View File

@ -0,0 +1,308 @@
// ============================================================================
// RUNTIME ABILITY WRAPPER - DROP-IN REPLACEMENT FOR BaseAbility
// ============================================================================
using System;
using System.Collections.Generic;
using UnityEngine;
// ============================================================================
// 1. RUNTIME ABILITY INSTANCE - Wraps Your Existing BaseAbility
// ============================================================================
/// <summary>
/// Runtime wrapper for BaseAbility that adds modifiers and behaviors
/// Can be used anywhere you currently use BaseAbility references
/// </summary>
public class RuntimeAbilityInstance
{
// ========================================================================
// CORE DATA - Wraps your existing ScriptableObject
// ========================================================================
[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(); // In case you add charges later
RecalculateModifiedValues();
}
// ========================================================================
// PROPERTIES - Direct access to original + modified values
// ========================================================================
// Original values (unchanged)
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;
// 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; }
// 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);
// ========================================================================
// EXECUTION METHODS - Same signature as your BaseAbility
// ========================================================================
public virtual void Execute(Taggable user)
{
if (!CanExecute(user)) return;
// Execute pre-cast behaviors
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, Vector3.zero);
// Execute original ability
sourceAbility.Execute(user);
// Execute post-cast behaviors
ExecuteBehaviors(BehaviorTrigger.PostCast, user, null, Vector3.zero);
// Update runtime state
lastUsedTime = Time.time;
}
public virtual void Execute(Taggable user, Vector3 point)
{
if (!CanExecute(user)) return;
ExecuteBehaviors(BehaviorTrigger.PreCast, user, null, point);
sourceAbility.Execute(user, 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);
sourceAbility.Execute(user, target);
ExecuteBehaviors(BehaviorTrigger.PostCast, user, target, target.position);
lastUsedTime = Time.time;
}
// ========================================================================
// ABILITY STATE CHECKS
// ========================================================================
public bool CanExecute(Taggable user)
{
// Check cooldown
if (IsOnCooldown) return false;
// Check resources using modified costs
return CanAffordResources(user);
}
private bool CanAffordResources(Taggable user)
{
// Use the modified costs, not original
var userMana = user.GetComponent<Mana>();
if (userMana != null)
{
float finalManaCost = manaCost + userMana.GetMaxValue() * percentMaxManaCost;
if (!userMana.EnoughMana(finalManaCost)) return false;
}
var userHealth = user.GetComponent<Health>();
if (userHealth != null)
{
float finalHealthCost = healthCost + userHealth.GetMaxValue() * percentMaxHealthCost;
if (userHealth.GetCurrentValue() <= finalHealthCost) return false;
}
var userClassResource = user.GetComponent<ClassResource>();
if (userClassResource != null && classResourceCost > 0)
{
if (userClassResource.GetCurrentValue() < classResourceCost) return false;
}
return true;
}
public float GetFinalManaCost(Mana userMana)
{
return manaCost + userMana.GetMaxValue() * percentMaxManaCost;
}
public float GetFinalHealthCost(Health userHealth)
{
return healthCost + userHealth.GetMaxValue() * percentMaxHealthCost;
}
// ========================================================================
// MODIFIER SYSTEM
// ========================================================================
public void AddModifier(AbilityModifier modifier)
{
activeModifiers.Add(modifier);
RecalculateModifiedValues();
}
public void RemoveModifier(AbilityModifier modifier)
{
activeModifiers.Remove(modifier);
RecalculateModifiedValues();
}
public void RemoveAllModifiers()
{
activeModifiers.Clear();
RecalculateModifiedValues();
}
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)
{
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);
// 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;
}
// ========================================================================
// IMPLICIT CONVERSION - Makes it work seamlessly
// ========================================================================
public static implicit operator BaseAbility(RuntimeAbilityInstance instance)
{
return instance.sourceAbility;
}
public static implicit operator RuntimeAbilityInstance(BaseAbility ability)
{
return new RuntimeAbilityInstance(ability);
}
}

View File

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

View File

@ -0,0 +1,19 @@
using UnityEngine;
public enum BehaviorTrigger
{
PreCast, // Before ability executes
PostCast, // After ability executes
OnHit, // When ability hits target
OnKill, // When ability kills target
OnCrit // When ability crits
}
public abstract class RuntimeBehavior
{
public BehaviorTrigger Trigger { get; set; }
public string BehaviorName { get; set; }
public abstract void Execute(RuntimeAbilityInstance ability, Taggable user, Transform target, Vector3 point);
public abstract RuntimeBehavior Clone();
}

View File

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