Attributes & Modifiers¶
The Attributes Subsystem (de.gurkenlabs.litiengine.attributes) provides a numerical property framework for handling base values, calculated modified values, value ranges, and stacking modifiers. It powers entity statistics (health, mana, movement speed), combat calculations, and particle lifetimes across LITIENGINE.
flowchart LR
Base["Base Value\n(e.g., 100.0)"] --> Mod1["1. ADD (+20)\n= 120.0"]
Mod1 --> Mod2["2. SUBTRACT (-10)\n= 110.0"]
Mod2 --> Mod3["3. MULTIPLY (* 1.5)\n= 165.0"]
Mod3 --> Mod4["4. DIVIDE (/ 2)\n= 82.5"]
Mod4 --> Mod5["5. SET (= 50.0)\n(Overrides all)"]
Mod5 --> Final["getModifiedValue()"]
Attribute API Method Reference¶
| Class / Method | Return Type | Description |
|---|---|---|
Attribute(T initialValue) |
Constructor | Creates a new attribute with an initial base value. |
getValue() / getBaseValue() |
T |
Returns the raw, unmodified base value. |
get() / getModifiedValue() |
T |
Computes and returns the value after all active modifiers are applied. |
addModifier(AttributeModifier<T> mod) |
void |
Attaches an active modifier to this attribute. |
removeModifier(AttributeModifier<T> mod) |
void |
Detaches a modifier from this attribute. |
modifyBaseValue(AttributeModifier<T> mod) |
void |
Permanently alters the base value using the given modifier. |
RangeAttribute(T val, T min, T max) |
Constructor | Creates a bounded attribute clamped between min and max. |
getMin() / getMax() |
T |
Returns the base minimum or maximum bounds. |
getModifiedMin() / getModifiedMax() |
T |
Returns the range bounds after boundary modifiers are applied. |
1. Basic Attributes (Attribute<T>)¶
An Attribute<T extends Number> encapsulates a numeric value. While getBaseValue() returns the baseline number, getModifiedValue() (or get()) calculates the final value dynamically on demand.
import de.gurkenlabs.litiengine.attributes.Attribute;
import de.gurkenlabs.litiengine.attributes.AttributeModifier;
import de.gurkenlabs.litiengine.attributes.Modification;
// 1. Create an attribute with a base attack power of 50
Attribute<Double> attackPower = new Attribute<>(50.0);
// 2. Attach a +15 flat damage bonus (e.g. from an equipped iron sword)
AttributeModifier<Double> swordBonus = new AttributeModifier<>(Modification.ADD, 15.0);
attackPower.addModifier(swordBonus);
System.out.println("Base Damage: " + attackPower.getBaseValue()); // 50.0
System.out.println("Total Damage: " + attackPower.getModifiedValue()); // 65.0
// 3. Remove the modifier when unequipped
attackPower.removeModifier(swordBonus);
System.out.println("After Unequip: " + attackPower.getModifiedValue()); // 50.0
2. Modification Types & Evaluation Order¶
Modifiers implement Comparable<AttributeModifier<T>> and are automatically sorted and applied in a fixed mathematical order regardless of the order in which they were attached:
Modification Type |
Order | Mathematical Operation | Typical Gameplay Use Case |
|---|---|---|---|
ADD |
1 |
result + modifyValue |
Flat gear bonuses (+20 Attack, +10 Armor). |
SUBTRACT |
2 |
result - modifyValue |
Flat penalties (-5 Defense debuff). |
MULTIPLY |
3 |
result * modifyValue |
Percentage buffs (Berserk potion: +50% -> 1.5). |
DIVIDE |
4 |
result / modifyValue |
Slow effects (Frost nova: 50% slow -> 2.0). |
SET |
5 |
modifyValue |
Total overrides (Stun/Root: sets velocity to 0). |
Attribute<Float> moveSpeed = new Attribute<>(100f);
// Attach a +20 flat boots bonus and a 50% haste multiplier in arbitrary order
moveSpeed.addModifier(new AttributeModifier<>(Modification.MULTIPLY, 1.5));
moveSpeed.addModifier(new AttributeModifier<>(Modification.ADD, 20.0));
// ADD (100 + 20 = 120) is evaluated before MULTIPLY (120 * 1.5 = 180)
System.out.println("Speed with Boots + Haste: " + moveSpeed.getModifiedValue()); // 180.0
3. Bounded Range Attributes (RangeAttribute<T>)¶
RangeAttribute<T extends Number & Comparable<T>> extends Attribute<T> to guarantee that calculated values remain strictly within minimum and maximum limits.
import de.gurkenlabs.litiengine.attributes.RangeAttribute;
// Bounded health attribute: current=100, min=0, max=100
RangeAttribute<Integer> health = new RangeAttribute<>(100, 0, 100);
// Clamp behavior is automatic
health.set(150);
System.out.println("Clamped to Max: " + health.get()); // 100
health.set(-25);
System.out.println("Clamped to Min: " + health.get()); // 0
Both min and max bounds can also receive independent modifiers via addMinModifier() and addMaxModifier():
// Increase maximum health cap by 25 from a relic
AttributeModifier<Integer> relicCap = new AttributeModifier<>(Modification.ADD, 25);
health.addMaxModifier(relicCap);
System.out.println("New Max Health: " + health.getModifiedMax()); // 125
4. Temporary Buffs & Timed Modifiers¶
Because AttributeModifier exposes an active state (setActive(boolean)), modifiers can be toggled without detaching them:
import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.attributes.Attribute;
import de.gurkenlabs.litiengine.attributes.AttributeModifier;
import de.gurkenlabs.litiengine.attributes.Modification;
public class SpeedPotion {
public static void apply(Attribute<Float> velocity) {
AttributeModifier<Float> haste = new AttributeModifier<>(Modification.MULTIPLY, 1.35); // +35%
velocity.addModifier(haste);
// Expire modifier after 5000 milliseconds (5 seconds)
Game.loop().perform(5000, () -> {
velocity.removeModifier(haste);
});
}
}
5. Listening to Attribute Changes¶
Attribute uses Java's standard PropertyChangeSupport to emit change events when base values or modifiers change:
Attribute<Double> mana = new Attribute<>(100.0);
mana.addListener(event -> {
System.out.println("Mana changed from " + event.getOldValue() + " to " + event.getNewValue());
});
See Also¶
- Entity Framework - Entity architectures and components
- Creature Entities - Using attributes for velocity and combat
- Game Loop - Managing timed buff expirations