Build a Spell — Cross-Mod Integration Guide
This document describes how other NeoForge mods can interact with Build a Spell’s player attribute system to modify mana, spell power, and casting behavior.
Dependency Setup
Add buildaspell as a dependency in your neoforge.mods.toml:
[[dependencies.yourmodid]]
modId = "buildaspell"
type = "required" # or "optional" if you check at runtime
versionRange = "[1.0,)"
ordering = "AFTER"
side = "BOTH"
For compile-time access, add the Build a Spell jar to your build.gradle:
dependencies {
compileOnly files("libs/buildaspell-<version>.jar")
}
Casting Spells
buildaspell.api.BuildASpellAPI is the stable entry point for building and casting spells from another mod. Its package, class name and method signatures are versioned API and will not be renamed; everything else in the mod (SpellExecutor, the attachments, the component enums) is internal and may change without notice. Every method is server-side, and bad input returns false or null with a log line rather than throwing across the mod boundary.
import buildaspell.api.BuildASpellAPI;
import buildaspell.spell.Spell;
// One ordered list of component ids: a modifier binds to the effect before it.
Spell spell = BuildASpellAPI.createSpell("cast", List.of("damage"));
if (spell != null) {
// consumeMana = false skips Build a Spell's mana check, so your mod can
// charge its own resource instead. Config enable/disable gates still apply.
BuildASpellAPI.cast(serverPlayer, spell, false);
// Or run the spell's effects at an explicit position, bypassing the delivery's targeting:
BuildASpellAPI.castAtLocation(serverPlayer, spell, new Vec3(x, y, z), false);
}
createSpell(deliveryId, componentIds)takes a delivery id and one interleaved, ordered list of component ids: an effect-bound modifier attaches to the effect that precedes it. Delivery-level modifiers (double,split,accelerate,pierce,bounce,return,chain,delay,echo,linger,duration) attach to the delivery as a whole, so their position in the list does not matter. Each id resolves as a modifier first, then an effect; an id starting withcompat:is passed through verbatim; anything else fails the whole build withnulland a log line. The list is capped at the server’smaxSpellComponentsand a non-stackable duplicate modifier is dropped, so an API-built spell can never exceed what a player could assemble by hand.cast(caster, spell, consumeMana)dispatches the spell through its own delivery method. WithconsumeManatrue the cast checks and spends the Build a Spell mana pool; with it false only that check (and its refund-on-failure path) is skipped, and the caller charges whatever resource it likes.castAtLocation(caster, spell, pos, consumeMana)runs the spell’s effects at an explicit world position instead of the delivery’s own targeting.deliveryIds(),effectIds()andmodifierIds()return the live id lists to build from.
Current mana is deliberately not exposed
The attributes below govern the pool’s maximum, its regeneration rate and spell power: the current contents of the pool are not part of the API. A mod that wants to run spells on its own resource system casts with consumeMana = false and charges that resource itself.
Custom Attributes
All three attributes are registered on EntityType.PLAYER and live in the buildaspell namespace. They are standard vanilla Attributes backed by RangedAttribute, so any system that applies AttributeModifiers (equipment, potions, mob effects, events) works out of the box.
| Registry ID | Default | Min | Max | Description |
|---|---|---|---|---|
buildaspell:mana_pool |
100.0 | 0.0 | 10,000.0 | Maximum mana capacity |
buildaspell:mana_regen |
5.0 | 0.0 | 1,000.0 | Mana regenerated per second |
buildaspell:spell_power |
10.0 | 0.0 | 1,000.0 | Spell damage and effectiveness scaling |
Enchantment bonuses from Build a Spell’s own enchantments are applied on top of the attribute value, so attribute modifiers and enchantments stack naturally.
Accessing Attributes at Compile Time
If you depend on Build a Spell directly, you can reference the DeferredHolder fields:
import buildaspell.registry.ModAttributes;
// Read current effective value (includes all modifiers)
double mana = player.getAttributeValue(ModAttributes.MANA_POOL);
double regen = player.getAttributeValue(ModAttributes.MANA_REGEN);
double power = player.getAttributeValue(ModAttributes.SPELL_POWER);
Accessing Attributes by Registry ID (No Hard Dependency)
If you want to keep Build a Spell as an optional dependency, look up the attribute holders at runtime using the registry:
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.ai.attributes.Attribute;
Holder<Attribute> manaPool = BuiltInRegistries.ATTRIBUTE
.get(Identifier.parse("buildaspell:mana_pool"))
.orElse(null);
if (manaPool != null) {
double value = player.getAttributeValue(manaPool);
}
Adding Attribute Modifiers
Transient Modifiers (Runtime Only)
Transient modifiers are not saved and are removed when the player logs out or the source is cleared. Useful for temporary buffs from events, abilities, or auras.
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
Identifier modifierId = Identifier.parse("yourmod:mana_boost");
AttributeInstance instance = player.getAttribute(ModAttributes.MANA_POOL);
if (instance != null && instance.getModifier(modifierId) == null) {
instance.addTransientModifier(new AttributeModifier(
modifierId,
50.0, // +50 mana
AttributeModifier.Operation.ADD_VALUE // flat addition
));
}
Permanent Modifiers (Saved with Player)
Permanent modifiers persist across sessions. Useful for progression systems or permanent unlocks.
AttributeInstance instance = player.getAttribute(ModAttributes.MANA_POOL);
if (instance != null && instance.getModifier(modifierId) == null) {
instance.addPermanentModifier(new AttributeModifier(
modifierId,
50.0,
AttributeModifier.Operation.ADD_VALUE
));
}
Removing Modifiers
AttributeInstance instance = player.getAttribute(ModAttributes.MANA_POOL);
if (instance != null) {
instance.removeModifier(modifierId);
}
Modifier Operations
All three vanilla AttributeModifier.Operation types work:
| Operation | Effect | Example |
|---|---|---|
ADD_VALUE |
Flat addition to base | +50 mana pool |
ADD_MULTIPLIED_BASE |
Multiplied by base after flat additions, then added | +0.2 = +20% of base |
ADD_MULTIPLIED_TOTAL |
Multiplies the running total | +0.1 = ×1.1 on the final value |
Evaluation order: (base + ADD_VALUE) * (1 + sum(ADD_MULTIPLIED_BASE)), then each ADD_MULTIPLIED_TOTAL modifier multiplies the running total by (1 + amount) in turn: two +0.1 modifiers give ×1.21, not ×1.2.
Common Integration Patterns
Custom Item That Boosts Mana Pool
Apply the modifier when the item is equipped, remove it when unequipped. The standard way is to add it to the item’s AttributeModifiers component via data or in code:
// In your item's constructor or via data generation
ItemAttributeModifiers modifiers = ItemAttributeModifiers.builder()
.add(ModAttributes.MANA_POOL,
new AttributeModifier(
Identifier.parse("yourmod:staff_mana"),
25.0,
AttributeModifier.Operation.ADD_VALUE),
EquipmentSlotGroup.MAINHAND)
.build();
Mob Effect That Increases Spell Power
public class SpellPowerEffect extends MobEffect {
public SpellPowerEffect() {
super(MobEffectCategory.BENEFICIAL, 0x7B68EE);
addAttributeModifier(
ModAttributes.SPELL_POWER,
Identifier.parse("yourmod:spell_power_effect"),
5.0, // +5 per level
AttributeModifier.Operation.ADD_VALUE
);
}
}
Notes
- Modifier IDs must be unique
Identifiers scoped to your mod (e.g.,yourmod:my_modifier). Adding a modifier whose ID is already applied to the same attribute instance throws anIllegalArgumentException: checkgetModifier(id) == nullfirst, as the examples above do. - Attribute values are automatically clamped to the min/max range defined by the
RangedAttribute. - These attributes are only registered on players. They are not present on other
LivingEntitytypes. - All attribute reads happen server-side. On the client, synced attribute data is available through the same
player.getAttributeValue()calls.