# Effects System Tutorial - Build Your First Buff in 5 Minutes

## What You'll Build

By the end of this tutorial, you'll have a complete working buff system with:

- A "Haste" buff that increases speed by 50%
- Visual particle effects that spawn/despawn with the buff
- A "Stunned" debuff that prevents player movement
- Tags you can query in gameplay code

**Time required:** 5-10 minutes

---

## Why Use the Effects System?

**The Old Way:**

```csharp
// 50-100 lines per effect type
public class HasteEffect : MonoBehaviour {
    float duration;
    float speedMultiplier;
    GameObject particles;

    void Update() {
        duration -= Time.deltaTime;
        if (duration <= 0) RemoveSelf();
    }

    void RemoveSelf() {
        // Remove speed modifier...
        // Destroy particles...
        // Handle stacking...
        // 40 more lines...
    }
}
```

**The New Way:**

```csharp
// Zero lines - everything configured in editor
player.ApplyEffect(hasteEffect);  // Done!
```

**Result:** Designers create hundreds of effects without programmer involvement.

---

## Step 1: Create Your First AttributesComponent (2 minutes)

This component will hold the stats that effects can modify.

```csharp
using UnityEngine;
using WallstopStudios.UnityHelpers.Tags;

public class PlayerStats : AttributesComponent
{
    // Define attributes that effects can modify
    public Attribute Speed = 5f;
    public Attribute MaxHealth = 100f;
    public Attribute AttackDamage = 10f;
    public Attribute Defense = 5f;

    protected override void Awake()
    {
        base.Awake();
        // Optional: Log when attributes change
        OnAttributeModified += (attributeName, oldVal, newVal) =>
            Debug.Log($"{attributeName} changed: {oldVal} → {newVal}");
    }
}
```

**What's an Attribute?**

- Holds a base value (e.g., Speed = 5)
- Tracks modifications from multiple sources
- Calculates final value automatically (Add → Multiply → Override)
- Raises events when value changes

**⚠️ Important: Use Attributes for "max" or "rate" values, NOT "current" depleting values!**

- ✅ **MaxHealth** - modified by buffs (good)
- ❌ **CurrentHealth** - modified by damage/healing from many systems (bad - causes state conflicts)
- ✅ **AttackDamage** - modified by strength buffs (good)
- ✅ **Speed** - modified by haste/slow effects (good)

If a value is frequently modified by systems outside the effects system (like health being reduced by damage), use a regular field instead. See the main documentation for details.

---

## Step 2: Add Stats to Your Player (30 seconds)

1. Open your Player prefab/GameObject
2. Add Component → `PlayerStats`
3. Set values in Inspector:
   - Speed: `5`
   - MaxHealth: `100`
   - AttackDamage: `10`
   - Defense: `5`

That's it! Your player now has modifiable attributes.

---

## Step 3: Create a Haste Effect (2 minutes)

### 3.1 Create the ScriptableObject

1. In Project window: `Right-click` → `Create` → `Wallstop Studios` → `Unity Helpers` → `Attribute Effect`
2. Name it: `HasteEffect`

### 3.2 Configure the Effect

Select `HasteEffect` and set these values in Inspector:

**Modifications:**

- Click **"+"** to add a modification
- Attribute Name: `Speed` (must match field name exactly)
- Action: `Multiplication`
- Value: `1.5` (150% of base speed)

**Duration:**

- Modifier Duration Type: `Duration`
- Duration: `5` (seconds)
- Can Reapply: ✅ (checking this resets timer when reapplied)

**Tags:**

- Effect Tags: Add `"Haste"` (used for both gameplay queries via `HasTag()` and effect organization)

### 3.3 Add Visual Effects (Optional)

**Cosmetic Effects:**

- Size: `1`
- Element 0:
  - Prefab: Drag a particle system prefab (or create one)
  - Requires Instancing: ✅ (creates a new instance per application)

---

## Step 4: Apply the Effect (30 seconds)

Add this code to test your effect:

```csharp
using UnityEngine;
using WallstopStudios.UnityHelpers.Tags;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private AttributeEffect hasteEffect;
    private PlayerStats stats;

    void Start()
    {
        stats = GetComponent<PlayerStats>();
    }

    void Update()
    {
        // Apply haste when pressing H
        if (Input.GetKeyDown(KeyCode.H))
        {
            this.ApplyEffect(hasteEffect);
            Debug.Log($"Speed is now: {stats.Speed.CurrentValue}");
        }

        // Move with current speed
        float h = Input.GetAxis("Horizontal");
        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;
    }
}
```

**Test it:**

1. Assign `HasteEffect` to the Inspector field
2. Press Play
3. Press `H` to apply haste
4. Notice: Speed increases to 7.5, particle effect spawns
5. After 5 seconds: Speed returns to 5, particles disappear

---

## Step 5: Create a Stun Debuff (2 minutes)

Let's make a more complex effect that prevents movement.

### 5.1 Create the Effect

1. `Right-click` → `Create` → `Wallstop Studios` → `Unity Helpers` → `Attribute Effect`
2. Name it: `StunEffect`

### 5.2 Configure Stun

**Modifications:**

- Attribute Name: `Speed`
- Action: `Override`
- Value: `0` (completely override speed to 0)

**Duration:**

- Modifier Duration Type: `Duration`
- Duration: `3`
- Can Reapply: ✅

**Tags:**

- Effect Tags: `"Stunned"`, `"Stun"`, `"Debuff"`, `"CC"` (for gameplay queries and organization)

### 5.3 Query Tags in Gameplay

```csharp
public class PlayerController : MonoBehaviour
{
    [SerializeField] private AttributeEffect hasteEffect;
    [SerializeField] private AttributeEffect stunEffect;
    private PlayerStats stats;

    void Update()
    {
        // Apply effects
        if (Input.GetKeyDown(KeyCode.H)) this.ApplyEffect(hasteEffect);
        if (Input.GetKeyDown(KeyCode.S)) this.ApplyEffect(stunEffect);

        // Check if player is stunned before allowing movement
        if (this.HasTag("Stunned"))
        {
            Debug.Log("Player is stunned! Cannot move.");
            return;
        }

        // Normal movement
        float h = Input.GetAxis("Horizontal");
        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;
    }
}
```

**Test it:**

1. Press `S` to stun yourself
2. Try to move - you can't!
3. After 3 seconds, movement returns

---

## Step 6: Advanced Features (5 minutes)

### Stacking Effects

Effects stack independently by default:

```csharp
// Apply haste 3 times
this.ApplyEffect(hasteEffect);  // Speed = 7.5
this.ApplyEffect(hasteEffect);  // Speed = 11.25 (1.5 × 1.5 × 5)
this.ApplyEffect(hasteEffect);  // Speed = 16.875 (1.5 × 1.5 × 1.5 × 5)

// Each stack has its own duration and can be removed independently
```

### Manual Removal

```csharp
// Apply and save handle
EffectHandle? handle = this.ApplyEffect(hasteEffect);

// Remove specific stack early
if (handle.HasValue)
{
    this.RemoveEffect(handle.Value);
}

// Remove all haste effects
this.RemoveEffects(this.GetHandlesWithTag("Haste"));
```

### Multiple Modifications Per Effect

One effect can modify multiple attributes:

**Create "Berserker Rage" effect:**

- Modification 1: Speed × 1.3
- Modification 2: AttackDamage × 2.0
- Modification 3: Defense × 0.5 (trade-off - more damage but less defense!)
- Duration: 10 seconds
- Tags: `"Berserker"`, `"Buff"`

### Infinite Duration Effects

For permanent buffs (e.g., equipment):

```csharp
// In Inspector:
// - Modifier Duration Type: Infinite
// - (Duration field is ignored)

// Apply permanent buff
EffectHandle? handle = this.ApplyEffect(permanentStrengthBonus);

// Later, remove when equipment is unequipped
if (handle.HasValue)
    this.RemoveEffect(handle.Value);
```

---

## Common Patterns

### Damage Over Time (DOT)

```csharp
// Create "Poison" effect:
// - periodicEffects: interval = 1s, maxTicks = 10, modifications = []
// - behaviors: PoisonDamageBehavior (below)
// - Duration: 10 seconds
// - Tags: "Poisoned", "DoT", "Debuff"

void ApplyPoison(GameObject target)
{
    target.ApplyEffect(poisonEffect);
}

[CreateAssetMenu(menuName = "Combat/Effects/Poison Damage")]
public sealed class PoisonDamageBehavior : EffectBehavior
{
    [SerializeField]
    private float damagePerTick = 2f;

    public override void OnPeriodicTick(
        EffectBehaviorContext context,
        PeriodicEffectTickContext tickContext
    )
    {
        if (!context.Target.TryGetComponent(out PlayerHealth health))
        {
            return;
        }

        health.ApplyDamage(damagePerTick);
    }
}

public sealed class PlayerHealth : MonoBehaviour
{
    [SerializeField]
    private float currentHealth = 100f;

    public float CurrentHealth => currentHealth;

    public void ApplyDamage(float amount)
    {
        currentHealth -= amount;

        if (currentHealth <= 0f)
        {
            currentHealth = 0f;
            Die();
        }
    }

    private void Die()
    {
        // Handle player death
    }
}
```

This keeps `CurrentHealth` as a regular gameplay field while the effect system triggers damage through behaviours.

### Cooldown Reduction

```csharp
// Create "Haste" effect (for abilities):
// - Modification: CooldownRate × 1.5 (50% faster cooldowns)

public class AbilitySystem : AttributesComponent
{
    public Attribute CooldownRate = 1f;
    private float cooldown;

    public void UseAbility()
    {
        // Cooldown respects rate
        cooldown = baseCooldown / CooldownRate.Value;
    }
}
```

### Conditional Effects

```csharp
// Only apply effect if conditions met
void TryApplyBuff(AttributeEffect effect)
{
    // Check if player already has max buffs
    if (this.TryGetTagCount("Buff", out int buffCount) && buffCount >= 5)
    {
        Debug.Log("Too many buffs active!");
        return;
    }

    // Check if effect is already active
    if (this.HasTag("Haste") && effect == hasteEffect)
    {
        Debug.Log("Haste already active!");
        return;
    }

    this.ApplyEffect(effect);
}
```

---

## Troubleshooting

### "Should I use CurrentHealth as an Attribute?"

- **No!** Use `MaxHealth` as an Attribute (modified by buffs), but keep `CurrentHealth` as a regular field (modified by damage/healing)
- **Why:** CurrentHealth is modified by many systems (combat, regeneration, etc.). Using it as an Attribute causes state conflicts when effects and other systems both try to modify it
- **Pattern:** Attribute for max/cap, regular field for current/depleting value
- **See:** "Understanding Attributes: What to Model and What to Avoid" in the main documentation

### "Attribute 'Speed' not found"

- **Cause:** Attribute name in effect doesn't match the field name in AttributesComponent
- **Fix:** Names must match exactly (case-sensitive): `Speed` not `speed`
- **Tip:** Use Attribute Metadata Cache generator for dropdown validation

### Effect doesn't apply

- **Check:** Does target GameObject have an `AttributesComponent`?
- **Check:** Is `EffectHandler` component added? (Usually added automatically)
- **Check:** Are there any errors in the console?

### Particles don't spawn

- **Check:** Cosmetic Effects → Prefab is assigned
- **Check:** Prefab has a `CosmeticEffectData` component
- **Check:** Requires Instancing is checked if using per-application instances

### Value isn't changing

- **Check:** Attribute name matches exactly
- **Check:** Modification value is non-zero
- **Check:** Action type is correct (Multiplication needs > 0, Addition can be negative)
- **Debug:** Log `attribute.Value` before and after applying effect

---

## Next Steps

You now have a complete buff/debuff system! Here are some ideas to expand:

### Create More Effects

- **Shield:** MaxHealth × 1.5, visual shield sprite
- **Slow:** Speed × 0.5, "Slowed" tag
- **Critical Strike:** AttackDamage × 2.0, "CriticalHit" tag, brief flash effect
- **Invisibility:** Just tags ("Invisible"), no stat changes, transparency effect
- **Armor Buff:** Defense + 10, metallic sheen cosmetic
- **Strength Potion:** AttackDamage × 1.5, red particle aura

### Build Systems Around Tags

```csharp
// AI ignores invisible players
if (!target.HasTag("Invisible"))
{
    ChasePlayer(target);
}

// UI shows status icons
if (player.HasTag("Poisoned"))
    ShowPoisonIcon();

// Abilities check prerequisites
if (player.HasTag("Stunned") || player.HasTag("Silenced"))
    return;  // Can't cast

// Interactions respect state
if (player.HasTag("Invulnerable"))
    damage = 0;
```

### Designer Workflows

1. Create an effect library (30+ common effects)
2. Designers mix/match on items, abilities, enemies
3. Programmers never touch effect code again!

---

## 📚 Related Documentation

**Core Guides:**

- [Effects System Full Guide](./effects-system.md) - Complete API reference and advanced patterns
- [Getting Started](../../overview/getting-started.md) - Your first 5 minutes with Unity Helpers
- [Main README](../../readme.md) - Complete feature overview

**Related Features:**

- [Relational Components](../relational-components/relational-components.md) - Auto-wire components (pairs well with effects)
- [Serialization](../serialization/serialization.md) - Save/load effects and attributes

**Need help?** [Open an issue](https://github.com/wallstop/unity-helpers/issues)

---

### Made with ❤️ by Wallstop Studios

_Effects System tutorial complete! Your designers can now create gameplay effects without code._
