Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created September 21, 2025 11:43
Show Gist options
  • Save adammyhre/9efde70222ecdb2254b08bae9ca60ade to your computer and use it in GitHub Desktop.
Save adammyhre/9efde70222ecdb2254b08bae9ca60ade to your computer and use it in GitHub Desktop.

Revisions

  1. adammyhre created this gist Sep 21, 2025.
    83 changes: 83 additions & 0 deletions DamageOverTimeEffect.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,83 @@
    using System;
    using System.Collections.Generic;
    using ImprovedTimers; // https://github.com/adammyhre/Unity-Improved-Timers
    using UnityEngine;

    [Serializable]
    public class Ability {
    public AudioClip castSfx;
    public GameObject castVfx;
    public GameObject runningVfx;

    [SerializeReference] public List<IEffect<IDamageable>> effects = new();

    public void Execute(IDamageable target) {
    foreach (var effect in effects) {
    if (target is Enemy enemy) {
    enemy.ApplyEffect(effect);
    } else {
    effect.Apply(target);
    }
    }
    }
    }

    public interface IDamageable {
    void TakeDamage(int amount);
    }

    public interface IEffect<TTarget> {
    void Apply(TTarget target);
    void Cancel();
    event Action<IEffect<TTarget>> OnCompleted;
    }

    [Serializable]
    public class DamageEffect : IEffect<IDamageable> {
    public int damageAmount = 10;

    public event Action<IEffect<IDamageable>> OnCompleted;

    public void Apply(IDamageable target) {
    target.TakeDamage(damageAmount);
    OnCompleted?.Invoke(this);
    }

    public void Cancel() {
    OnCompleted?.Invoke(this);
    }
    }

    [Serializable]
    public class DamageOverTimeEffect : IEffect<IDamageable> {
    public float duration = 5f;
    public float tickInterval = 1f;
    public int damagePerTick;

    public event Action<IEffect<IDamageable>> OnCompleted;

    IntervalTimer timer;
    IDamageable currentTarget;

    public void Apply(IDamageable target) {
    currentTarget = target;
    timer = new IntervalTimer(duration, tickInterval);
    timer.OnInterval = OnInterval;
    timer.OnTimerStop = OnStop;
    timer.Start();
    }

    void OnInterval() => currentTarget?.TakeDamage(damagePerTick);
    void OnStop() => Cleanup();

    public void Cancel() {
    timer?.Stop();
    Cleanup();
    }

    void Cleanup() {
    timer = null;
    currentTarget = null;
    OnCompleted?.Invoke(this);
    }
    }
    42 changes: 42 additions & 0 deletions Enemy.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    using System.Collections.Generic;
    using UnityEngine;

    public class Enemy : MonoBehaviour, IDamageable {
    public int health = 50;
    readonly List<IEffect<IDamageable>> activeEffects = new();

    public void TakeDamage(int amount) {
    health -= amount;
    Debug.Log($"Enemy took {amount} damage. Health now {health}");

    if (health <= 0) {
    Die();
    }
    }

    public void ApplyEffect(IEffect<IDamageable> effect) {
    if (health <= 0) return; // Dead enemies should't receive effects

    effect.OnCompleted += RemoveEffect;
    activeEffects.Add(effect);
    effect.Apply(this);
    }

    void RemoveEffect(IEffect<IDamageable> effect) {
    effect.OnCompleted -= RemoveEffect;
    activeEffects.Remove(effect);
    }

    void Die() {
    Debug.Log("Enemy died.");

    for (int i = activeEffects.Count - 1; i >= 0; i--) {
    var effect = activeEffects[i];
    effect.OnCompleted -= RemoveEffect;
    effect.Cancel();
    }
    activeEffects.Clear();

    Destroy(gameObject);
    }
    }
    34 changes: 34 additions & 0 deletions PlayerAbilityCaster.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    using System.Collections;
    using UnityEngine;
    using UnityUtils; // https://github.com/adammyhre/Unity-Utils

    public class PlayerAbilityCaster : MonoBehaviour {
    public Ability[] hotbar;

    void Update() {
    for (int i = 0; i < hotbar.Length; i++) {
    if (Input.GetKeyDown(KeyCode.Alpha1 + i)) {
    Cast(hotbar[i], FindFirstObjectByType<Enemy>());
    }
    }
    }

    void Cast(Ability ability, IDamageable target) {
    ability.Execute(target);

    var targetMb = target as MonoBehaviour;

    if (ability.castVfx && targetMb) {
    Instantiate(ability.castVfx, targetMb.transform.position.Add(y:2), Quaternion.identity);
    }

    if (ability.runningVfx && targetMb) {
    var runningVfxInstance = Instantiate(ability.runningVfx, targetMb.transform);
    Destroy(runningVfxInstance, 3f);
    }

    if (ability.castSfx) {
    AudioSource.PlayClipAtPoint(ability.castSfx, transform.position);
    }
    }
    }