> 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_draw(dt)`: cosmetic-only drawing on renderable frames
* `effect_end(interrupt)`: called when the effect is removed

`effect_draw` includes players and skips resim. Interactive UI must use `effect_update`/`effect_late_update`, never `effect_draw`.

## 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_draw        :: 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 is detached and no longer rooted. Its reference is invalid. **Return immediately** and do not access `this` again.
{% 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. Use `has_emote_block_reason(reason)` to query one reason and `has_any_emote_block_reason()` to query whether any are active. You can also control the built-in effect directly:

```go
if player.has_any_emote_block_reason() {
    // 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 your Player class has a health field.
    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) {
    if entity.has_effect(Grow_Effect) return;

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

My_Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        death, found := entity.get_effect(Death_Effect);
        if !found return;

        time_until_respawn := max(0.0, 5.0 - death.get_elapsed_time());
        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});
    }
}
```

### Cutscene lock (active effect)

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

## Smooth custom visuals

The simulation runs at a fixed rate while rendering can run faster. Drawing from a component callback is automatically anchored to that component's entity. For other cases:

* Wrap world-space drawing for another entity in `UI.begin_world_space_ui(entity)` and `UI.end_world_space_ui()`.
* For a moving position that does not belong to an entity, keep a persistent `Position_Interpolation_Helper`. Pass the offset from `update(position)` to `UI.push_interpolation_offset`, then pop it after drawing.
* Use a persistent `Float_Interpolation_Helper` with `UI.quad_fill` for a changing bar value.
* Call `entity.mark_teleported()` after a discontinuous position change.

```go
Smooth_Bar :: class {
    progress: float;
    interpolation: Float_Interpolation_Helper;

    draw :: method(rect: Rect) {
        fill := UI.quad_fill(interpolation.update(progress), .RIGHT);
        UI.quad(rect, core_globals.white_sprite, {0.2, 0.9, 0.3, 1}, params={fill=fill});
    }
}
```

Keep interpolation helpers in persistent component or UI state. A helper created inside the draw method has no previous frame to interpolate from.

## 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).
* Run gameplay effects in the shared predicted path.
* `effect_draw` is cosmetic-only. Draw interactive effect UI from the player's `ao_late_update` under `is_local_or_server()`.
