Skip to the content.

Modifier authoring guide

A Modifier mutates an Entity once per frame. The Director sorts registered modifiers by (phase, priority, registration_order) before the first tick; each later modifier observes the cumulative effect of all earlier ones.

The trait

pub trait Modifier {
    fn meta(&self) -> &'static ModifierMeta;
    fn update(&mut self, entity: &mut Entity);
}

meta returns a &'static ModifierMeta constant declaring name, description, phase, priority, and the reads / writes field sets. update is the only mutation surface; time flows in via entity.tick.now (stamped by Director::run).

Adding a new modifier

  1. Create crates/stackchan-core/src/modifiers/<your_name>.rs.

  2. Implement the state machine:

    use crate::director::{Field, ModifierMeta, Phase};
    use crate::entity::Entity;
    use crate::modifier::Modifier;
    
    pub struct YourModifier {
        // state: timers, RNG, last-value
    }
    
    impl YourModifier {
        #[must_use]
        pub const fn new() -> Self { /* ... */ }
    }
    
    impl Modifier for YourModifier {
        fn meta(&self) -> &'static ModifierMeta {
            static META: ModifierMeta = ModifierMeta {
                name: "YourModifier",
                description: "One sentence: what triggers this and what \
                              entity fields it writes.",
                phase: Phase::Expression,
                priority: 0,
                reads: &[/* Field::... */],
                writes: &[/* Field::... */],
            };
            &META
        }
    
        fn update(&mut self, entity: &mut Entity) {
            let now = entity.tick.now;
            // Read + mutate entity fields.
        }
    }
    
  3. Pick a phase. Affect decides emotions, Expression modulates face style, Motion writes head pose, Audio drives visual from audio.

  4. Add unit tests in a mod tests at the bottom of the file. Set entity.tick.now, call update, assert.

  5. Register in crates/stackchan-firmware/src/main.rs::render_task: director.add_modifier(&mut your_modifier).expect("registry full"). Update the boot info-line listing.

  6. If the behavior is non-local, add a stackchan-sim integration test that drives the new modifier alongside related ones.

Reads vs writes

Entity fields fall into a few buckets:

The reads / writes slices on ModifierMeta are documentation, and cfg(debug_assertions) builds assert that each modifier only writes its declared Fields after every update — see Director::run.

Field granularity

Field is fine-grained per-leaf-field (e.g. LeftEyePhase vs LeftEyeWeight) so different sub-fields of the same component don’t false-flag as conflicts. Field::group() buckets fine variants into coarse FieldGroups for human-readable conflict reports.

Ordering

The Affect phase ordering matters:

When in doubt, mirror the registration order in render_task and add a sim integration test that asserts the visible behavior.

Skills

A Skill has should_fire(&Entity) -> bool and invoke(&mut Entity) -> SkillStatus, plus richer metadata than a modifier. Use it when the behavior is discoverable — selected from a menu rather than always-on. Skills don’t write face or motor directly; they go through mind / voice / events and modifiers in later phases translate that into rendered face and physical motion. The current population lives at crates/stackchan-core/src/skills/.