#Noita-style spell system

37 messages · Page 1 of 1 (latest)

ancient crystal
#

Hello, I'm attempting to recreate Noita's want system but am stuck on a some parts of the implementation. Currently, I have an Inventory component that stores the equipped Spell structs. I use a custom Command to build the spell projectile entity by attaching different components depending on the spell and clone the entity when a Multi-cast is used. The part where this impl seems to fall short is when trying to add "Trigger" spells. I can't use the Bundle of the next spell as I can't spawn in the projectile until after the spell is triggered and iirc you can't store Bundles. Triggers basically build the next spell in front of it.

I could have an enum of spell traits that is added to list then can be used to construct a spell entity? Is there a better way to think about the data structure of spells that I may be missing?

normal thicket
#

Ok. So consider this. You have a Spell struct correct? And you are using it as the input to the system that spawns projectiles?

#

A "Trigger" spell triggers a sub-spell correct?

#

So why not store the sub-spell instead of a bundle?

ancient crystal
# normal thicket Ok. So consider this. You have a `Spell` struct correct? And you are using it as...

Spell is more used as an what components to add to an Entity and can't be modified meaningfully. Only the end result of the Entity has all the attributes for the spell projectile.

But, I could look into changing it so Spell holds all information on the end-result spell as well. Kinda like Add Trigger + Arrow = A spell that holds the properties of a trigger and arrow. It would also make cloning for multi-cast spells way easier I'd assume? Then I can add that resulting sub-Spell to the Trigger component. Is that what you mean?

normal thicket
#

Well not sure. But what I mean is instead of storing an "arbitrary bundle" (which is currently impossible in bevy) you should consider storing the data the generating function uses to generate the bundle in question.

ancient crystal
#

All the generation function does is add the needed components to the projectile entity. It doesn't really return anything that can be store for later entity creation

#

I think I would have to use a more generic system to all for storing what is needed to construct a projectile?

#

Like, run the cast function, and it returns the completed Spell struct. This struct could have a list of sub-spells and then the Spell is constructed into an entity with all the required components from there?

normal thicket
#

I don't understand what you mean. Fyi Noita's wands use reverse polish notation (it's basically a programming language), and they are "interpreted". I suspect it builds a tree data structure (I think that's what you are describing?) and execute a function that traverses the tree each time the wand is used

ancient crystal
#

Ya, a tree-like structure is, afaik, kinda what I got going. I have the multi-cast, modifiers, and projectiles to work like their system, but an entity-building impl is too restrictive for triggers-type spells. But storing the sub-spells like:

struct Spell{
    sub_spells: Vec<Spell>
}

I think works. The Spell can be constructed into an entity and all sub-spells will be stored in a Trigger component, or whatever component need to use the sub-spells. This, I think, would allow for triggers in triggers type recursion, while being flexible.

normal thicket
#

In rust, you can take advantage of enums for tree data structures. For example

enum Spell {
  Double(Box<Spell>),
  Combine { spell1: Box<Spell>, spell2: Box<Spell> },
  SplitAtContact(Box<Spell>),
  Icicle,
  Fire,
  Saw,
  // etc.
}
ancient crystal
# normal thicket I don't understand what you mean. Fyi Noita's wands use reverse polish notation ...

I don't use reverse polish notation but do use a tree-like iteration. After messing around with their spell system thought, I did find the pattern on how spells operate and was able to re-create it partially. Modifiers apply themself then ask the next spell to modify the spell. Multicast clones the current spell and steps each one after each other across the spell list. But, if any spell return to the starting index, the spell is finished

ancient crystal
normal thicket
#

It's less efficient than storing the spell as a Vec<SpellElement>, but it's a bit easier to work with

#

With the noita system, you could clone a slice of the Vec to have the sub-spell.

ancient crystal
#

I attempt to implement these ideas a bit later. But, thank you for the insight and the brainstorming. I'll let you know how it goes, if you want to know.

normal thicket
#

I'd love to

rotund junco
#

Yeah you should avoid potentially recursive structures 🙂

In situations like this I prefer to work with a "registry" resource containing (in this case) the spells. The registry is a simple vector or hashmap of Spells, each identified by an ID (e.g. an u32).

You can then simply look up the relevant spell definition using its ID, through the registry passed to the system as a resource. This ID can be used in the trigger.

#

I don't know if that helps you, but it works for me.

ancient crystal
errant pivot
#

an alternative to think about: your spells could be hierarchical entities under a wand, using bevys entity hierarchy instead of you own thing

#

though i think the ecs queries might not be powerfull enough to make that a nicely expressed patten

ancient crystal
ancient crystal
#

@normal thicket I think I did it! It definitely needs polish and some cleanup though. I'm just going with the quicker route for a prototype. I create a SpellInventory like:

spells: vec![
    SpellType::Modifier(ModifierType::SpeedUp),
    SpellType::MultiCast(MultiCastType::Double),
    SpellType::Projectile(ProjectileType::Arrow),
    SpellType::Trigger,
    SpellType::Projectile(ProjectileType::Arrow),
    SpellType::Projectile(ProjectileType::Arrow),
],
#

Then in when I call cast() it runs this code here:

pub fn cast(&mut self) -> SpellOutput{
        self.starting_index = self.index;
        self.apply(Spell::empty(), true)
    }

    fn apply(&mut self, starting_spell: Spell, first: bool) -> SpellOutput{
        if self.index == self.starting_index && !first{
            return SpellOutput::None;
        }

        let next_spell = self.current_spell();
        let spell = starting_spell + next_spell;

        self.increment_index();

        let spells = match spell.spell_type {
            SpellType::Projectile(_) => self.apply_projectile(spell),
            SpellType::Modifier(_) => self.apply_modifier(spell),
            SpellType::MultiCast(_) => self.apply_multicast(spell),
            SpellType::Trigger => self.apply_trigger(spell),
        };

        spells
    }

    fn apply_projectile(&mut self, mut spell: Spell) -> SpellOutput{
        println!("Apply Projectile");

        if spell.trigger{
            println!("Applying Sub-Spells:");
            spell.sub_spells = Box::new(self.apply(Spell::empty(), false));
        }

        SpellOutput::Single(spell)
    }

    fn apply_modifier(&mut self, spell: Spell) -> SpellOutput{
        println!("Apply Modifer");

        self.apply(spell, false)
    }

    fn apply_multicast(&mut self, spell: Spell) -> SpellOutput{
        println!("Apply Multicast");

        let mut spell_list = vec![];
        // This needs to be changed based on multicast type (eg double, triple)
        for _ in 0..2{
            let cloned_spell = spell.clone();
            spell_list.push(self.apply(cloned_spell, false));
        }

        SpellOutput::single_to_multiple(spell_list)
    }

    fn apply_trigger(&mut self, spell: Spell) -> SpellOutput{
        println!("Apply Trigger");

        self.apply(spell, false)
    }

If you want to know any specifics on why this works, I can elaborate.

#

This creates a SpellOutput

pub enum SpellOutput{
    None,
    Single(Spell),
    Multiple(Vec<Spell>),
}

which is then constructed into an Entity depending on the what the Spell data is.

#

The terminal prints

Apply Modifer
Apply Multicast
Apply Projectile
Apply Trigger
Apply Projectile
Applying Sub-Spells:
Apply Projectile

which is what I expect from the spells I put in the inventory

#

Is there anything immediate that can be changed for the better? Possibly turning SpellOutput into a list as output decreases ergonomics.

normal thicket
#

Yeah, looks like a recursive descent parser and AST now lol

#

I'm not sure I follow your implementation. I don't understand the intent of self.index and self.starting_index.

ancient crystal
ancient crystal
ancient crystal
# normal thicket This might help you out <https://rosettacode.org/wiki/Parsing/RPN_calculator_alg...

I don't fully follow this though. MultiCast more like branches/splits the tree. So, if you had a SpeedUp modifier and a MultiCast after it, it would split the SpeedUp into two spells and continue running the cast independently for each spell. MultiCasts don't combine the spells, does it? Technically, all spells add, or subtracts if the values are negative, their values like this:

impl Add for Spell{
    type Output = Self;

    fn add(self, other: Self) -> Self::Output {
        Self{
            spell_type: other.spell_type,
            target: other.target,
            mana_usage: self.mana_usage + other.mana_usage,
            damage: self.damage + other.damage,
            speed: self.speed + other.speed,
            cast_delay: self.cast_delay + other.cast_delay,
            speed_modifier: self.speed_modifier + other.speed_modifier,
            trigger: self.trigger || other.trigger,
            sub_spells: self.sub_spells,
        }
    }
}

Am I still doing what you said, but just not grasping it?

normal thicket
#

hmm I don't know.