effects
Reference this when implementing effects for abilities, animations, and state-based behaviors.
SKILL.md
| Name | effects |
| Description | Reference this when implementing effects for abilities, animations, and state-based behaviors. |
name: effects description: "Reference this when implementing effects for abilities, animations, and state-based behaviors."
CSL Effect System
Effects temporarily take control of an entity to execute complex behaviors like dashes, attacks, eating animations, or death sequences. Effects can be applied to Players, NPCs, or any other entity.
Core Concepts
- Two types of effects - Active effects (one at a time, interrupt each other) and passive effects (multiple allowed, stack)
- Callbacks are optional - Implement only the callbacks you need
- Works with any entity - Use
entityfor general access,playeris populated only for Player entities - Effects are a linked list - Iterate with
effect_iteratoror manually viafirst_effect/next_effect
Creating a Basic Effect
My_Effect :: class : Effect_Base {
// Custom state for this effect
some_value: float;
target_position: v2;
// Called once when effect starts
effect_start :: method() {
player_specific.freeze_player = true; // Player can't move
player->player_set_trigger("my_animation");
}
// Called every frame
effect_update :: method(dt: float) {
entity->lerp_local_position(target_position, 20 * dt);
if get_elapsed_time() > 1.0 {
remove_effect(false);
return; // Effect is invalid after removal
}
}
// Called when effect ends
effect_end :: method(interrupt: bool) {
if !interrupt {
entity->set_local_position(target_position);
}
player->player_set_trigger("RESET");
}
}
Activating Effects
Active Effects (One at a Time)
Use set_active_effect for effects that should be mutually exclusive. Setting a new active effect ends the current one with interrupt = true.
effect := new(My_Effect);
effect.target_position = target.world_position;
entity->set_active_effect(effect);
Passive Effects (Multiple Allowed)
Use add_passive_effect for effects that can stack (slows, buffs, status effects).
slow_effect := new(Slow_Effect);
slow_effect.speed_multiplier = 0.5;
slow_effect->set_duration(4); // Auto-remove after 4 seconds
entity->add_passive_effect(slow_effect);
Effect_Base Fields
Effect_Base :: class {
entity: Entity; // The entity this effect is attached to
player: Player; // Populated only for Player entities, null for NPCs
player_specific: struct {
freeze_player: bool; // Lock player position entirely
disable_movement_inputs: bool; // Ignore movement input but allow code to move
};
start_time: float; // Time when effect started (read-only)
next_effect: Effect_Base; // Next effect in linked list (read-only)
prev_effect: Effect_Base; // Previous effect in linked list (read-only)
}
Effect Methods
get_elapsed_time
Returns time since effect started.
if get_elapsed_time() > 0.5 {
remove_effect(false);
}
set_duration
Automatically removes the effect after the specified time. Can be called when creating the effect or in effect_start.
// When creating
effect := new(My_Effect);
effect->set_duration(4.0);
entity->set_active_effect(effect);
// Or in effect_start
effect_start :: method() {
set_duration(0.5);
}
remove_effect
Ends the effect. Pass false for natural completion, true for forced/interrupted end.
Important: After calling remove_effect(), the effect is invalid. Return immediately.
if get_elapsed_time() > 0.5 {
remove_effect(false);
return; // Effect is now invalid, must return immediately
}
Callbacks Reference
effect_start
Called once when effect is added. Use to:
- Set
player_specific.freeze_playerorplayer_specific.disable_movement_inputs - Trigger animations
- Store initial state
- Call
set_durationfor timed effects
effect_start :: method() {
player_specific.disable_movement_inputs = true;
original_friction = player.agent.friction;
player.agent.friction = 0;
player->player_set_trigger("dash");
set_duration(0.5);
}
effect_update
Called every frame. Use to:
- Move the entity
- Check completion conditions
- Handle ongoing effects
effect_update :: method(dt: float) {
player.agent.velocity = direction * 10;
}
effect_late_update
Called every frame after effect_update. Use for:
- Drawing UI
- Camera effects
effect_late_update :: method(dt: float) {
if player->is_local() {
World_Progress_Bar.draw(player.entity.world_position, get_elapsed_time() / 0.5, {});
}
}
effect_end
Called when effect ends. interrupt is true if ended by another effect or forced.
effect_end :: method(interrupt: bool) {
player.agent.friction = original_friction;
player->player_set_trigger("RESET");
if !interrupt {
// Ended naturally - maybe chain to next effect
}
}
Iterating Through Effects
Use effect_iterator with foreach:
foreach effect: effect_iterator(entity) {
if effect.#type == Slow_Effect {
agent.movement_speed *= effect.(Slow_Effect).speed_multiplier;
}
}
Manual iteration:
effect := entity.first_effect;
while effect != null {
defer effect = effect.next_effect;
// Process effect...
}
Common Patterns
Movement Effect (Dash/Roll)
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->player_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;
}
}
// Usage:
effect := new(Roll_Effect);
effect.direction = activation.direction;
player.entity->set_active_effect(effect);
Passive Status Effect (Slow)
Slow_Effect :: class : Effect_Base {
speed_multiplier: float;
}
// Apply slow
slow_effect := new(Slow_Effect);
slow_effect.speed_multiplier = 0.5;
slow_effect->set_duration(4);
entity->add_passive_effect(slow_effect);
// Check for slow in player update
foreach effect: effect_iterator(entity) {
if effect.#type == Slow_Effect {
agent.movement_speed *= effect.(Slow_Effect).speed_multiplier;
}
}
Attack Effect
Slash_Effect :: class : Effect_Base {
direction: v2;
original_friction: float;
already_hit_list: [..]Player;
effect_start :: method() {
player_specific.disable_movement_inputs = true;
original_friction = player.agent.friction;
player.agent.friction = 0;
player->player_set_trigger("attack");
player->set_facing_right(direction.x > 0);
}
effect_update :: method(dt: float) {
// Check for hits
foreach other: component_iterator(Player) {
if other.team == player.team continue;
if !in_range(other.entity.world_position - player.entity.world_position, 0.75) continue;
if already_hit_list->contains(other) continue;
other->take_damage(1);
already_hit_list->append(other);
}
player.agent.velocity = direction * 10;
if get_elapsed_time() > 0.3 {
remove_effect(false);
return;
}
}
effect_end :: method(interrupt: bool) {
player.agent.friction = original_friction;
player->player_set_trigger("RESET");
}
}
Death Effect
Death_Effect :: class : Effect_Base {
effect_start :: method() {
player_specific.freeze_player = true;
player->add_name_invisibility_reason("death");
player->player_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 %s", {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);
player.health->reset();
player->player_set_trigger("RESET");
}
}
NPC Effects
For NPCs, player is null. Store a reference to the NPC component and use entity for position/transform.
NPC_Death_Effect :: class : Effect_Base {
npc: NPC;
effect_update :: method(dt: float) {
t := Ease.out_quad(Ease.T(get_elapsed_time(), 1.0));
npc.sprite.color.w = lerp(1.0, 0.0, t);
if get_elapsed_time() > 5.0 {
entity->destroy();
}
}
}
// Usage:
effect := new(NPC_Death_Effect);
effect.npc = this;
entity->set_active_effect(effect);
Skip normal behaviour while effect is active:
ao_update :: method(dt: float) {
if entity.active_effect != null {
return; // Effect is controlling this entity
}
// Normal behaviour...
}
Checking Effects
// Check for active effect
if entity.active_effect != null {
// Has an active effect
}
// Check specific type
if entity.active_effect != null && entity.active_effect.#type == Eating_Effect {
entity.active_effect.(Eating_Effect)->chomp();
}
// Remove all effects
remove_all_effects(entity);
freeze_player vs disable_movement_inputs
player_specific.freeze_player = true: Position locked entirelyplayer_specific.disable_movement_inputs = true: Ignores input but code can still move player
Use disable_movement_inputs for dashes/rolls (need to set velocity).
Use freeze_player for eating/interacting (stay in place).
Best Practices
- Choose the right effect type:
set_active_effectfor exclusive control,add_passive_effectfor stackable buffs/debuffs - Use
set_durationfor timed effects - cleaner than manual time checks - Store and restore state - save
agent.frictionetc. and restore ineffect_end - Check
interruptineffect_end- handle forced vs natural ends differently - Reset animations with
player->player_set_trigger("RESET") - Use
remove_effect(false)for natural completion