> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/core-engine-concepts/effects.md).

# Effects

Effects temporarily take control of an entity for complex behaviors like dashes, attacks, eating animations, cutscenes, or death/respawn sequences.

Effects can be used on players, NPCs, or any entity. They’re especially useful when you want:

* Movement you control in code (dash/knockback/roll)
* “Lock out” player input for a short sequence (eat, revive, cutscene)
* A timed window of state (invincibility, slow, stun)
* UI that is tied to a temporary state (respawn countdown)

## Active vs passive

All Out supports two kinds of effects:

* **Active effects**: one at a time per entity. Setting a new active effect interrupts the current active effect.
* **Passive effects**: multiple can be attached at once (stackable buffs/debuffs/status effects).

{% hint style="info" %}
An active effect is also inserted into the entity’s effect list, so it shows up in `effect_iterator` just like passive effects.
{% endhint %}

## Quick start (a simple timed effect)

Create a class that inherits from `Effect_Base`, then attach it to an entity.

```go
Apple_Pop_Effect :: class : Effect_Base {
    sprite: Sprite_Renderer;
    start_scale: v2;
    start_pos: v2;

    effect_start :: method() {
        sprite = entity.get_component(Sprite_Renderer);
        start_scale = entity.local_scale;
        start_pos = entity.local_position;
        set_duration(0.4); // auto-removes after 0.4s
    }

    effect_update :: method(dt: float) {
        t := get_elapsed_time() / 0.4;

        // Scale up then shrink to nothing
        scale_curve: float;
        if t < 0.3 {
            scale_curve = lerp(1.0, 1.3, Ease.out_back(t / 0.3));
        } else {
            scale_curve = lerp(1.3, 0.0, Ease.in_back((t - 0.3) / 0.7));
        }

        entity.set_local_scale(start_scale * scale_curve);

        // Float upward slightly
        rise := Ease.out_quad(t) * 0.5;
        entity.set_local_position({start_pos.x, start_pos.y + rise});

        // Fade out near the end
        if sprite != null {
            alpha := 1.0 - Ease.in_quad(max(0.0, (t - 0.5) / 0.5));
            sprite.color.w = alpha;
        }
    }

    effect_end :: method(interrupt: bool) {
        // Clean up when done (or interrupted)
        entity.destroy();
    }
}
```

Attach it as an active effect:

```go
effect := new(Apple_Pop_Effect);
entity.set_active_effect(effect);
```

## Effect lifecycle

Effects are callback-based. Implement only what you need:

* `effect_start()`: called once when the effect is attached
* `effect_update(dt)`: called every frame
* `effect_late_update(dt)`: called every frame after `effect_update`
* `effect_end(interrupt)`: called when the effect is removed

## Active effects (exclusive control)

Use `set_active_effect` for effects that should be mutually exclusive (dash, attack windup, death sequence).

```go
dash := new(Dash_Effect);
dash.direction = {1, 0};
player.entity.set_active_effect(dash);
```

If the entity already has an active effect, it is ended with `interrupt = true`.

## Passive effects (stackable buffs / status)

Use `add_passive_effect` for effects that can stack (slow, poison, shield, invincibility window).

```go
slow := new(Slow_Effect);
slow.speed_multiplier = 0.5;
slow.set_duration(4.0);
enemy.entity.add_passive_effect(slow);
```

## API reference

### `Effect_Base`

```go
Effect_Base :: class {
    entity: Entity;
    player: Player; // null if this effect wasn't added to a player!

    player_specific: struct {
        freeze_player: bool;
        disable_movement_inputs: bool;
    };

    // Optional callbacks
    #interface effect_start       :: proc(c: Effect_Base);
    #interface effect_update      :: proc(c: Effect_Base, dt: float);
    #interface effect_late_update :: proc(c: Effect_Base, dt: float);
    #interface effect_end         :: proc(c: Effect_Base, interrupt: bool);

    // Read-only
    start_time: float;
    next_effect: Effect_Base;
    prev_effect: Effect_Base;

    get_elapsed_time :: method() -> float;
    get_duration_remaining :: method() -> float;
    set_duration     :: method(duration: float);
    remove_effect    :: method(interrupt: bool);
}
```

### Applying / removing effects

```go
set_active_effect  :: proc(entity: Entity, new_active_effect: $T);
add_passive_effect :: proc(entity: Entity, new_effect: $T);

remove_all_effects :: proc(entity: Entity);
remove_effect      :: proc(entity: Entity, type: typeid, interrupt: bool) -> bool;
```

### Checking / iterating effects

```go
get_effect :: proc(entity: Entity, $T: typeid, mode := Try_Get_Effect_Mode.EXACT_MATCH) -> T, bool;
has_effect :: proc(entity: Entity, $T: typeid, mode := Try_Get_Effect_Mode.EXACT_MATCH) -> bool;

effect_iterator :: proc(entity: Entity) -> Effect_Iterator;
```

{% hint style="warning" %}
After calling `remove_effect(...)`, the effect object is freed. **Return immediately** and don’t touch `this` after removal.
{% endhint %}

## `freeze_player` vs `disable_movement_inputs`

* **`player_specific.freeze_player = true`**: locks player position entirely.
* **`player_specific.disable_movement_inputs = true`**: ignores input, but your effect can still move the player (dash/roll).

Both only apply when `player != null` (i.e. the effect is on an entity that has a `Player` component).

## Player emotes

Player emotes use a built-in active effect. Starting another emote interrupts the current emote, while any other active effect prevents a new emote from starting. Movement interrupts emotes and restores the main animation layer, except for `Emote/T_Pose`, which allows movement.

Use block reasons when gameplay should temporarily prevent the emote wheel from starting an emote:

```go
player.add_emote_block_reason("stunned");

// Later, when the restriction ends:
player.remove_emote_block_reason("stunned");
```

Reasons are counted as entries, so matching add/remove calls are important. `remove_emote_block_reason` removes one matching entry and returns whether it found one. You can also query or control the built-in effect directly:

```go
if player.is_emote_blocked() {
    // At least one block reason is active.
}

started := player.try_trigger_emote("Emote/Wave");
cancelled := player.cancel_emote();
```

`try_trigger_emote` returns `false` when the animation is not equipped, emotes are blocked, or another kind of active effect owns the player. The normal emote wheel uses this same path.

## Effect examples

### Dash / roll (active effect)

```go
Roll_Effect :: class : Effect_Base {
    direction: v2;
    original_friction: float;

    effect_start :: method() {
        player_specific.disable_movement_inputs = true;
        original_friction = player.agent.friction;
        player.agent.friction = 0;
        player.animator.state_machine.set_trigger("dodge_roll");
        player.set_facing_right(direction.x > 0);
        set_duration(0.5);
    }

    effect_update :: method(dt: float) {
        player.agent.velocity = direction * 8;
    }

    effect_end :: method(interrupt: bool) {
        player.agent.friction = original_friction;
    }
}
```

### Invincibility window (passive effect)

Use a passive effect to represent the invincible state, and check for it wherever you apply damage.

```go
Invincible_Effect :: class : Effect_Base {
}

give_invincibility :: proc(player: Player, seconds: float) {
    e := new(Invincible_Effect);
    e.set_duration(seconds);
    player.entity.add_passive_effect(e);
}

// Example damage gate:
take_damage :: proc(player: Player, amount: s64) {
    if player.entity.has_effect(Invincible_Effect) {
        return;
    }
    // assumes you have your own health field with a take_damage that bumps their spine animator redness when hit  
    player.health.take_damage(amount);
}
```

### 2x size for a few seconds (passive effect)

```go
Grow_Effect :: class : Effect_Base {
    scale_multiplier: float;
    start_scale: v2;

    effect_start :: method() {
        start_scale = entity.local_scale;
        entity.set_local_scale(start_scale * scale_multiplier);
    }

    effect_end :: method(interrupt: bool) {
        entity.set_local_scale(start_scale);
    }
}

apply_grow :: proc(entity: Entity) {
    e := new(Grow_Effect);
    e.scale_multiplier = 2.0;
    e.set_duration(3.0);
    entity.add_passive_effect(e);
}
```

### Death / respawn sequence (active effect with local UI)

```go
Death_Effect :: class : Effect_Base {
    effect_start :: method() {
        player_specific.freeze_player = true;
        player.add_name_invisibility_reason("death");
        player.animator.state_machine.set_trigger("death");
    }

    effect_update :: method(dt: float) {
        time_until_respawn := 5.0 - get_elapsed_time();

        if player.is_local() {
            ts := UI.default_text_settings();
            ts.size = 64;
            rect := UI.get_screen_rect().bottom_center_rect().offset(0, 150);
            UI.text(rect, ts, "Respawning in %", {time_until_respawn.(int) + 1});
        }

        if time_until_respawn <= 0 {
            remove_effect(false);
            return;
        }
    }

    effect_end :: method(interrupt: bool) {
        player.remove_name_invisibility_reason("death");
        respawn_player(player); // user-defined: teleport player to spawn point
        player.health.reset();
        player.animator.state_machine.set_trigger("RESET");
    }
}
```

### Cutscene lock (active effect)

```go
Cutscene_Effect :: class : Effect_Base {
    effect_start :: method() {
        player_specific.freeze_player = true;
        set_duration(2.0);
    }

    effect_late_update :: method(dt: float) {
        if player != null && player.is_local() {
            Notifier.notify("Cutscene...");
        }
    }
}
```

## Best practices

* Use `set_active_effect` when the effect “owns” the entity for a short sequence (dash/attack/death/cutscene).
* Use `add_passive_effect` for stackable state (buffs/debuffs/status).
* Use `set_duration(...)` for timed effects instead of manual timers.
* Always restore any modified state in `effect_end` (friction, scale, animation triggers, etc).
* Keep **purely cosmetic** UI/effects local (wrap in `player.is_local()`), and keep gameplay state server-authoritative.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.allout.game/core-engine-concepts/effects.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
