Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Gameplay is easiest to test headless, one tick at a time. You give the mage mana and the fireball, cast it, tick, and check what it paid and what was told. bevy_harness does the ticking. Each tick moves time by one fixed step, so tests are repeatable.

Add it as a dev-dependency:

[dev-dependencies]
bevy_harness = { git = "https://github.com/declaredlost/bevy_harness" }

The fireball costs mana, and cools down

This test casts the fireball twice. The first cast pays 20 mana; the second fails, since the first’s cooldown still lasts. Two seconds on, it can be cast again:

use bevy::prelude::*;
use bevy_mettle::prelude::*;
use bevy_harness::TestApp;

#[derive(Attribute)] #[attribute(base = 100.0, min = 0.0)] struct Mana;
#[derive(Component, Default, Behaviour)] struct CastFireball;
#[derive(Ability)]
struct Fireball;

/// The mage's content: the fireball, its cost and its cooldown.
fn content() -> MettleContent {
    MettleContent {
        tags: ["cooldown", "cooldown.fireball"]
            .into_iter()
            .map(TagKey::named)
            .collect(),
        effects: vec![
            EffectData::instant("fireball.cost")
                .modifier(EffectModifier::new::<Mana>(Operation::Flat, -20.0)),
            EffectData::lasting("fireball.cooldown", 2.0).owner_tag("cooldown.fireball"),
        ],
        abilities: vec![
            AbilityData::new(Key::of::<Fireball>(), Key::of::<CastFireball>())
                .cost("fireball.cost")
                .cooldown("fireball.cooldown"),
        ],
        ..default()
    }
}

/// An app with the mage, granted the fireball.
fn mage_app() -> (TestApp, Entity) {
    let mut app = TestApp::builder()
        .plugins(MettlePlugins)
        .with(|app| {
            app.add_attribute::<Mana>()
                .add_behaviour::<CastFireball>()
                .add_ability::<Fireball>()
                .insert_resource(content());
        })
        .watch_triggered::<ContentRejected>()
        .watch_triggered::<AbilityFailed>()
        .build();
    app.assert_triggered_none::<ContentRejected>(|_| true);
    let mage = app.spawn(Base::<Mana>::default());
    app.world_mut().grant_ability(mage, Fireball).unwrap();
    app.tick();
    (app, mage)
}

fn main() {
// #[test]
// fn the_fireball_costs_mana_and_cools_down() {
let (mut app, mage) = mage_app();

// Cast: 20 mana paid.
let fireball = app.world_mut().activate_ability(mage, Fireball).unwrap().ability();
app.tick();
app.entity(mage).assert::<Current<Mana>>(|mana| mana.get() == 80.0);

// It's running, as a `CastFireball` on the ability.
app.entity(fireball).assert_has::<CastFireball>();

// End it, and cast again: still cooling down.
app.world_mut().end_ability(fireball);
app.world_mut().activate_ability(mage, Fireball).unwrap_err();
app.assert_triggered::<AbilityFailed>(|failed| failed.reason == Failure::Cooldown);

// Two seconds on, it can be cast again.
app.secs(2.0);
app.world_mut().activate_ability(mage, Fireball).unwrap();
app.tick();
app.entity(mage).assert::<Current<Mana>>(|mana| mana.get() == 60.0);
// }
}

In your crate, the body is a #[test] function. activate_ability on the world returns what activating came to, an Activated, or why it didn’t activate: running here, the usual, or only asked of the server, a client’s server-run ability. .ability() is the granted ability’s entity either way, and its behaviour’s component is on it while it’s active.

What helps

  • Content in code. The definitions’ builders (EffectData::instant, AbilityData::new) make a test’s content short. Check the game’s real content in a test of its own: load it into MettleContent as the game does, and assert check holds against an app with the game’s code added, its add_effect and add_ability types too.
  • Watch what’s told. watch_triggered::<AbilityFailed>() records every failure, so assert_triggered can say why a cast didn’t happen. The same works for AttributeChanged<A>, EffectApplied and the rest.
  • Tick before reading. A cost lowers Base<Mana> as it’s paid; Current<Mana> follows the next time Mettle runs. Tick once before asserting on Current.
  • Seconds, not ticks. app.secs(2.0) runs as many ticks as two seconds take, so a test doesn’t depend on the tick rate.