SKILL.md
CSL Effect System
Effects temporarily take control of an entity to execute complex behaviors (dashes, attacks, death sequences). Works with Players, NPCs, or any entity.
Creating an Effect
My_Effect :: class : Effect_Base {
target_position: v2;
effect_start :: method() {
player_specific.freeze_player = true;
player.animator.state_machine.set_trigger("my_animation");
}
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 -- must return immediately
}
}
effect_end :: method(interrupt: bool) {
if !interrupt {
entity.set_local_position(target_position);
}
player.animator.state_machine.set_trigger("RESET");
}
}
Activating Effects
Active effects (setactiveeffect) -- one at a time, setting a new one ends the current with interrupt = true:
effect := new(My_Effect);
effect.target_position = target.world_position;
entity.set_active_effect(effect);
Passive effects (addpassiveeffect) -- multiple allowed, stack (slows, buffs):
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;
player: Player; // null for NPCs
player_specific: struct {
freeze_player: bool; // Zero out Player Movement_Agent velocity (use for eat/interact)
disable_movement_inputs: bool; // Ignore input but code can still move (use for dash/roll)
};
start_time: float #read_only;
next_effect: Effect_Base #read_only;
prev_effect: Effect_Base #read_only;
}
Callbacks (each optional)
effectstart-- Set freeze/movement flags, trigger animations, store initial state, callsetduration.effect_update(dt: float)effectlateupdate(dt: float)-- Post-update UI, camera effects, or local-only visuals. Screen UI is valid here for effects attached to a Player because player effects run fromcoreplayerlateupdate; guard UI withplayer.islocalorserver().effectend(interrupt: bool)-- Restore saved state, reset animations withplayer.animator.statemachine.set_trigger("RESET").
Checking Effects
if has_effect(entity, Slow_Effect) {
agent.movement_speed *= 0.5;
}
Examples
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.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;
}
}
Attack Effect (Hit Detection)
Same structure as RollEffect, but add an alreadyhit_list to avoid hitting the same target twice:
already_hit_list: [..]Player;
effect_update :: method(dt: float) {
for 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); // user-defined damage method
already_hit_list.append(other);
}
player.agent.velocity = direction * 10;
if get_elapsed_time() > 0.3 {
remove_effect(false);
return;
}
}
Death/Respawn Effect
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 respawn proc
reset_player_health(player); // replace with your game's health reset logic
player.animator.state_machine.set_trigger("RESET");
}
effect_late_update :: method(dt: float) {
if player.is_local_or_server() {
time_until_respawn := 5.0 - 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}`);
}
}
}
NPC Effects
For NPCs, player is null. Store a reference to the NPC component and use entity for transforms.
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 behavior while an effect is active:
ao_update :: method(dt: float) {
if entity.get_active_effect() != null return;
// Normal behaviour...
}
Checking Effects
effect := entity.get_active_effect();
if effect != null && effect.#type == Eating_Effect {
effect.(Eating_Effect).chomp();
}
remove_all_effects(entity);