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 intoMettleContentas the game does, and assertcheckholds against an app with the game’s code added, itsadd_effectandadd_abilitytypes too. - Watch what’s told.
watch_triggered::<AbilityFailed>()records every failure, soassert_triggeredcan say why a cast didn’t happen. The same works forAttributeChanged<A>,EffectAppliedand the rest. - Tick before reading. A cost lowers
Base<Mana>as it’s paid;Current<Mana>follows the next timeMettleruns. Tick once before asserting onCurrent. - 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.