Reading in context
Current<A> is an attribute with nothing in play. A fireball hitting the
mage is something in play: it’s fire, it has a source, it flew some way.
Some modifiers count only for reads like that. This chapter reads
attributes in a context.
Kinds of a value
The mage resists fire better than cold. That’s not two attributes. It’s one,
Resistance, with modifiers that count only for reads tagged with a damage
type:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(max = MaxResistance)] struct Resistance;
#[derive(Attribute)] #[attribute(base = 0.75)] struct MaxResistance;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<Resistance>()
.add_attribute::<MaxResistance>();
let mage = app
.world_mut()
.spawn((Base::<Resistance>::default(), Base::<MaxResistance>::default()))
.id();
#[derive(Tag)]
struct Damage;
#[derive(Tag)]
#[tag(parent = Damage)]
struct Elemental;
#[derive(Tag)]
#[tag(parent = Elemental)]
struct Fire;
#[derive(Tag)]
#[tag(parent = Elemental)]
struct Cold;
// A ring: +90% elemental resistance, +5% maximum fire resistance.
app.world_mut().spawn(ModifierOf(mage)).modifiers([
ModifierSpec::flat::<Resistance>(0.9).read_tagged(Elemental),
ModifierSpec::flat::<MaxResistance>(0.05).read_tagged(Fire),
]);
app.update();
let world = app.world();
let fire = AttributeContext::new().tag(Fire);
let cold = AttributeContext::new().tag(Cold);
let resists = |context: &AttributeContext| world.value::<Resistance>(mage, context).unwrap();
assert!((resists(&fire) - 0.8).abs() < 1e-4); // capped at 75% + 5%
assert!((resists(&cold) - 0.75).abs() < 1e-4); // capped at 75%
assert_eq!(resists(&AttributeContext::new()), 0.0); // nothing in play
Tags are a tree, so the modifier for damage.elemental counts for a fire
hit: a read tagged damage.elemental.fire is tagged damage.elemental too.
Limits are read in the same context, so fire resistance is capped by
maximum fire resistance. A mod adds a damage type by adding its tag. Unreal’s
Gameplay Ability System does the same: modifiers with tag requirements,
weighed as the attribute is read.
The context
AttributeContext says what’s in play. Start from
AttributeContext::new(), nothing, and add to it:
| Builder | What the read has |
|---|---|
tags(&tags), tag("..") | The read’s own tags: the hit’s, the ability’s |
target_tags(&owned), target_tag("..") | What the target owns |
source_tags(&owned), source_tag("..") | What the source (the caster, the attacker) owns |
fact("fact.flown", 7.0) | A number named by a tag. Supplying it again replaces it |
source(entity) | The source involved: its local modifiers count |
The owner’s own tags are always in play: they’re the actor’s OwnedTags.
These are what a modifier’s condition is weighed in.
ReadTagged reads the read’s tags, TargetHas the target’s, SourceHas
the source’s, and FactRamp and the other fact terms read the facts. A
fact the read didn’t supply isn’t there, so a read with no hit gets no hit
bonuses.
tags, target_tags and source_tags borrow what they’re given. A read per
hit copies nothing.
In a system
AttributeReader is a system parameter that reads in context:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(base = 10.0)] struct SpellPower;
#[derive(Component)] struct Mage;
#[derive(Resource, Default)] struct Dealt(f32);
fn fireball_damage(
mages: Query<Entity, With<Mage>>,
attributes: AttributeReader,
mut dealt: ResMut<Dealt>,
) {
for mage in &mages {
let hit = AttributeContext::new()
.tag("ability.spell.fire")
.fact("fact.flown", 7.0);
if let Some(power) = attributes.value::<SpellPower>(mage, &hit) {
dealt.0 = power;
}
}
}
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<SpellPower>()
.init_resource::<Dealt>()
.add_systems(Update, fireball_damage.after(MettleSystems));
app.world_mut().spawn((Mage, Base::<SpellPower>::default())).modifiers([
ModifierSpec::additive::<SpellPower>(0.5).read_tagged("ability.spell.fire"),
ModifierSpec::flat::<SpellPower>(10.0)
.condition(Condition::fact_ramp("fact.flown", 2.0, 12.0)),
]);
app.update();
// (10 + 10 × half) × 1.5: the ramp is half-way at 7.
assert!((app.world().resource::<Dealt>().0 - 22.5).abs() < 1e-4);
value is None if the actor doesn’t have the attribute. With nothing in
play, read Current<A> instead: it’s already worked out.
The reader also has breakdown (below), and owned, an actor’s owned
tags. Each names its attribute by type.
On the world
AttributeWorld does the same on a World, for exclusive systems and
tests: value, breakdown and base, by type. attribute_id::<A>() is
A’s number, what events such as EffectExecuted name it by.
It also changes a base the way an instant effect does. apply_to_base
changes it by an operation and a value, then rounds it and keeps it within
its limits. base_after says what that would leave, without changing
anything:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(base = 100.0, min = 0.0, max = MaxLife)] struct Life;
#[derive(Attribute)] #[attribute(base = 100.0)] struct MaxLife;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<Life>()
.add_attribute::<MaxLife>();
let mage = app
.world_mut()
.spawn((Base::<Life>::default(), Base::<MaxLife>::default()))
.id();
app.update();
let world = app.world_mut();
// A fireball for 130: life stops at its minimum, 0.
assert_eq!(
world.base_after::<Life>(mage, 100.0, Operation::Flat, -130.0),
Some(0.0)
);
world.apply_to_base::<Life>(mage, Operation::Flat, -30.0);
assert_eq!(world.base::<Life>(mage), Some(70.0));
Limits are read as the bases are now, even if they changed since attributes were last kept.
Breakdowns
A Breakdown is an attribute’s modifiers summed but not yet applied: the
base, Σ flat, Σ additive, the product of the multipliers, Σ divide, Σ
unscaled, and the override if there is one. total() applies them with the
formula from Modifiers.
Use it for a tooltip, or to combine an attacker’s and a target’s modifiers into one hit:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(base = 10.0)] struct SpellPower;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<SpellPower>();
let mage = app
.world_mut()
.spawn(Base::<SpellPower>::default())
.modifiers([
ModifierSpec::flat::<SpellPower>(5.0),
ModifierSpec::additive::<SpellPower>(0.2),
])
.id();
app.update();
let parts = app
.world()
.breakdown::<SpellPower>(mage, &AttributeContext::new())
.unwrap();
assert_eq!((parts.base, parts.flat), (10.0, 5.0));
assert!((parts.total() - 18.0).abs() < 1e-4);
Local modifiers
A local modifier counts only for reads naming its source. The mage’s staff adds 100 spell power to its own spells, not to every spell:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(base = 10.0)] struct SpellPower;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<SpellPower>();
let mage = app.world_mut().spawn(Base::<SpellPower>::default()).id();
let staff = app
.world_mut()
.spawn(ModifierOf(mage))
.modifiers([ModifierSpec::flat::<SpellPower>(100.0).local()])
.id();
app.update();
let world = app.world();
let with_staff = AttributeContext::new().source(staff);
assert_eq!(world.value::<SpellPower>(mage, &with_staff), Some(110.0));
assert_eq!(world.get::<Current<SpellPower>>(mage).unwrap().get(), 10.0);
Guards
Two guards read attributes with nothing in play:
attribute_above and attribute_below. They’re read-only systems
taking an agent and a value, the shape bevy_arbor’s behavior trees take
as conditions: when(attribute_below::<Life>, 20.0) => task(Flee). An
agent without the attribute is neither above nor below. The guard on owned
tags, owns, is in bevy_mettle_core (Gameplay tags).
use bevy::prelude::*;
use bevy_mettle::prelude::*;
use bevy_mettle::attribute::read::{attribute_above, attribute_below};
#[derive(Attribute)] #[attribute(base = 100.0, min = 0.0, max = MaxLife)] struct Life;
#[derive(Attribute)] #[attribute(base = 100.0)] struct MaxLife;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins.set(MettleCorePlugin::new(Update))))
.add_attribute::<Life>()
.add_attribute::<MaxLife>();
let mage = app
.world_mut()
.spawn((Base::<Life>::new(15.0), Base::<MaxLife>::default()))
.id();
app.update();
let world = app.world_mut();
assert!(world.run_system_cached_with(attribute_below::<Life>, (mage, 20.0)).unwrap());
assert!(!world.run_system_cached_with(attribute_above::<Life>, (mage, 20.0)).unwrap());
On a client
On a client, values with nothing in play are the ones the server serves. A read in context is worked out on the client, from the modifiers replicated to it. So it’s advisory, as in Unreal: the server’s read is the one that decides. See Prediction.
Pitfalls
- Reading
Currentfor a hit.Currentleaves out every modifier with a condition on the read. Read the hit in its context. - A context per frame per actor. A context is cheap, but reading every attribute in context every frame isn’t. Read in context where something happens: a hit, a cast.
- A fact not supplied. A condition on a fact the read doesn’t supply doesn’t hold. Supply every fact your modifiers name.