Magic Realms
An Iron's Spells addon adding spellcasting mobs and hireable mercenaries that level up and develop personalities.
Magic Realms is an Iron's Spells and Spellbooks addon for Minecraft NeoForge 1.21.1 that aims to expand the amount of spellcasting mobs in the world, with a strong focus on hireable mercenary NPCs that earn titles, develop personalities, and fight alongside the player.
Player guide
Mercenaries
Mercenaries are spellcasting humans that wander the world and can be hired to fight alongside you. Each Mercenary has one of five playstyles with it's own spell list:
- Mage - Ranged spellcaster, can wield a Staff and Spellbook. Has inate Spell Power in one to four Schools of Magic. Avaliable Schools and Spells are set via Datapack through Tags.
- Warrior - Melee fighter, can wield Swords. Gains bonus Armor from their Class and Star Level.
- Tank - Melee fighter who can wield a Shield in addition to Swords.
- Archer - Ranged fighter wielding a bow. Can use modded bows, and if any bow doesn't natively work you can add it to the "bows" tag from Magic Realms.
- Assassin - Melee skirmisher with high Crit Chance and a chance to Dodge attacks.
Mercenaries earn Titles through their deeds - see Titles below.
Mercenaries can become immortal by giving them a Hell's Pass, which is dropped from the Dead King and Tyros. Mercenaries with a Hell's Pass who run out of Health will get Stunned for a few seconds (configurable in magic_realms-common.toml).
Each mercenary spawns with:
- A Gender (male / female), which influences their Name and Appearance.
- A randomly generated Name drawn from a configurable list (
magic_realms-common.tomlmaleNames/femaleNames). - A Personality - an Archetype, a Hobby, a Hometown, and zero to three Quirks. (See Custom archetypes below.)
- A unique Appearance - either a randomly generated combination of skin / clothes / eyes / hair textures, or a complete preset skin (with about a 10% chance to roll a preset).
Players in Creative Mode can use the Skin Customizer to customize the Appearance of randomly generated Mercenaries.
Hiring Mercenaries - Taverns and Tavernkeepers
Mercenaries hang out in Taverns, structures that generate naturally in plains biomes. Each tavern is staffed by one Tavernkeeper, who sells useful items.
Interacting with a Mercenary will tell you who they are and how long contract lasts.
To hire a Mercenary, simply right-click them with a Contract item.
From the Tavernkeeper you can buy:
- Contracts, which are needed to hire Mercenaries. There are two kinds: a Temporary Contract (hires for a fixed stretch of time and can be used again to extend) and a Permanent Contract (binds a Mercenary to you for good, but only once they've served you long enough).
- Maps to structures from Iron's Spells & Spellbooks
- Food and drink
- A Room for the night, you can buy a Sleeping Pass to sleep in one of the Tavern's beds. Sleeping in the Tavern grants you Absorption and Resistance effects
Players can also interact with the Tavernkeeper giving him an emerald to get either useful information or some chit chat.
Working with a contracted Mercenary
Once a Mercenary is contracted to you, interacting with them will tell you how much longer they will remain contracted for and triggers some chit chatting related to their Personality. When shift + right clicking them a menu will pop up, and inside it you can:
- Change their equipment - armor, weapons, off-hand items. Mercenaries can use spells from their equipment if they are on their Class' spell list.
- View their combat stats - full Attribute breakdown (HP, armor, spell power per school, resistances, vanilla stats) plus their Personality traits.
- See their inventory - what they've picked up off the ground, plus what they're carrying for you.
- Give Orders - there's a button to switch between Following you and Patrolling the area.
- Choose their displayed Title - a selector showing every Title they've earned. Pick one to display above their name, or none at all.
Mercenaries also react to other Mercenaries you've contracted nearby. Hired Mercenaries interact with eahch other acording to their their Personalities, which will apply a temporary buff to them based on their Archetypes (rivals, friends, etc.). archetype interactions can be configured via Datapack: see Custom archetype interactions.
A Mercenary's Quirks also drive in-world behavior, for example: a Mercenary with the cant_swim Quirk will not be able to swim in water, a Mercenary with the animal_friend Quirk receives Stat Bonuses when around Animals, a Mercenary who is afraid_of_the_dark becomes weaker and slower in dark places, and so on.
Titles
Instead of levelling, Mercenaries earn Titles for the things they actually do. Killing two hundred skeletons, walking into an Ancient City, surviving ten thousand points of damage, or simply staying under contract long enough will each earn something.
Every Title carries its own rewards. Some are straightforward Attribute boosts; others are more characterful - extra damage against a particular type of monsters, a passive effect, a chance to inflict something on hit, immunity to a status effect, or a bonus that only applies while wielding a certain kind of weapon.
Titles are permanent once earned, and a Mercenary can hold as many as they qualify for. Their effects all apply at once, so a well-travelled veteran ends up meaningfully tougher than a fresh hire - but only in the ways they earned.
When a contracted Mercenary earns a Title you'll be told about it. One Title at a time is displayed above their name, and as their contractor you choose which one from the contract screen. By default the most prestigious one is shown.
Titles are entirely datapack-driven, so packs can add, retune, or replace the whole set - see Custom titles.
Other Mobs
Magic Realms adds several magic Mobs to spice up the world:
- Chuchu - Slimes who absorb Magic. Whichever school it absorbs first determines its element going forward, and they can grow absorbing magic. They rarely drop the "Slime Rain" Spell.
- Fizzles - Sharply dressed Creepers in wizard hats. They don't explode like normal Creepers - instead they cast spells. Be careful: a well-dressed creeper is a dangerous one.
- Endermages - a conclave of enlightened Endermen living in the Outer End Islands. They have learned magic and use it to teleport across the End freely, no longer bound by the limits of their kin.
- Tim - a familiar undead lurks in the depths of the Overworld. There are some who call him… Tim.
There's also a small roster of exclusive mercenaries - handcrafted named characters with unique behavior, dialogue, titles, and sometimes special quirks (Eden, Aliana, Amadeus, Catas, Jara, Lilac, Alshanex, GojoMojo). These can appear in Taverns, like normal Mercenaries, identified by their unique skin and name. They are unique to a world: once Amadeus is hired in your save, no other mercenary will ever be Amadeus. They also can't be permanently contracted, they like freedom.
Built-in compatibility
Magic Realms has built-in compatibility for a growing list of other Mods.
Spell Compatability
Spells from these addons are integrated into Mercenary spell lists and can be cast by Fizzles:
- Tunes 'n Tomes: a Bard's Journey
- Ender's Spells and Stuff: Requiem
- Discerning the Eldritch
- Magic From The East
- SnackPirate's Aeromancy Additions
- Cataclysm: Spellbooks
As both Mercenary spell lists and Fizzle spell lists are handled via tag, additional spell compat can be added via Datapack.
Tavern integration
Taverns can include blocks and items from these other mods:
- Farmer's Delight
- Brewin' & Chewin'
- Hearth and Harvest
- My Nether's Delight
- Supplementaries
- Handcrafted
- Create
The kitchen, bar, and table jigsaw pools dynamically swap to mod-specific variants when those mods are detected. The Tavernkeep will sell food and drink items from these mods, should they be installed.
Developer guide
This section is for modpack authors, datapack authors, and other modders who want to extend Magic Realms' mercenaries with their own personalities, skins, or named characters.
Almost everything personality and appearance-related in Magic Realms is datapack-driven.
Also includes all the config options and what are they for.
Config options
Magic Realms ships with a single common config file at config/magic_realms-common.toml, generated automatically on first run.
Contracts
Controls the timing of the contract / hire system.
| Key | Default | Range | Purpose |
|---|---|---|---|
minutesPerContract | 10 | 1 – ∞ | How long a single contract lasts before it expires and the mercenary returns to neutral. |
minutesUntilPermanent | 200 | 1 – ∞ | Total contracted time (across renewals) needed before a mercenary can accept a permanent contract. |
Spawning and population
| Key | Default | Range | Purpose |
|---|---|---|---|
customTextureChance | 0.1 | 0.0 – 1.0 | Probability that a newly spawned mercenary uses a complete skin preset instead of compositing skin parts. 0.1 = 10%. Set to 0.0 to disable presets, 1.0 to make every mercenary use one (requires enough presets in the catalog or some will share). |
maxMercenariesInRadius | 8 | 1 – ∞ | Limits how many mercenaries can occupy chairs / bar stools inside a 20-block radius. Prevents tavern overcrowding inside taverns. |
Behavior tuning
| Key | Default | Range | Purpose |
|---|---|---|---|
immortalStunDuration | 10 | 1 – 60 | Seconds an immortal entity (e.g. a Mercenary with a Hell's Pass) stays stunned after being knocked out, before recovering. |
attemptCastUnclassifiedSpells | false | bool | If true, Mercenaries will try to cast spells from addons that haven't been categorized into Magic Realms' class spell tags (classes/mage/*, classes/warrior/*, classes/archer/*, classes/assassin/*). Set at your own risk - uncategorized spells may not work as expected, may target nothing useful, or may break combat AI. |
Name, hometown, and tip lists
These string lists are fully editable in the config - you can replace, add to, or trim them without writing a datapack. Translation keys in tavernTips need matching language entries in any active resource pack.
| Key | Type | Default size | Purpose |
|---|---|---|---|
maleNames | list of strings | ~140 entries | Pool used to name male mercenaries when no name comes from a fixed personality or skin preset. |
femaleNames | list of strings | ~120 entries | Same, for female mercenaries. |
hometowns | list of strings | 35 entries | Pool used to roll a hometown string for the personality panel ("Westhollow", "Stormkeep", etc.). Picked once per mercenary at spawn. |
tavernTips | list of strings | 5 entries | Translation keys the Tavernkeeper picks from when offering advice (message.magic_realms.tavernkeep_tip.1 through …tip.5 by default). Each entry must be a key, not the literal text - extend your language file in parallel to add new tips. |
Datapack folder layout
All of the following live under data/magic_realms/:
data/magic_realms/
├── mercenaries/
│ ├── personality/
│ │ ├── archetypes/ # one .json per archetype
│ │ ├── fixed_personalities/ # one .json per named character
│ │ ├── hobbies/ # one .json per hobby
│ │ └── archetype_interactions/ # one .json per archetype pair relationship
│ ├── skin_parts/ # one .json per skin / clothes / eyes / hair piece
│ ├── skin_presets/ # one .json per complete preset texture
│ ├── titles/ # one .json per earnable title
│ └── bandit_profiles/ # one .json per bandit (hostile mercenary) preset
├── tags/
│ └── … (spell tags, item tags, biome tags, etc.)
└── … and, outside the mercenaries tree:
data/<your_namespace>/
└── neoforge/
└── biome_modifier/ # one .json per bandit spawn ruleEvery catalog is loaded via a vanilla SimpleJsonResourceReloadListener, so /reload will pick up changes without restarting the server (with the caveat that already-spawned mercenaries keep whatever they originally rolled - only newly spawned mercenaries see the updated catalog).
Custom archetypes
An archetype is a personality bucket like "stoic", "jovial", "ruthless", and so on. Every mercenary has exactly one archetype. The archetype:
- Drives a mercenary's chat tone (combined with their hobby).
- Pairs with other archetypes via archetype interactions to apply attribute buffs / debuffs.
- Biases the random class roll: archetypes can favor mages, warriors, or rogues.
File location
data/magic_realms/mercenaries/personality/archetypes/<id>.jsonThe file's path is the archetype's stable identifier. The catalog also indexes archetypes by the value inside the JSON's id field, which is what every other JSON file in the mod (fixed_personalities, archetype_interactions) refers to. Keep both in sync to avoid confusion.
Schema
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | - | Required. Short id used by every other file referring to this archetype (e.g. "stoic"). |
display_key | string | - | Required. Translation key for the human-readable name (e.g. "archetype.magic_realms.stoic"). |
class_weights | object: string→int | {} | Per-class weight bonus on top of base_weight. Keys must be lower-case class names: "mage", "warrior", "rogue". |
base_weight | int | 10 | Base weight before class bonus. |
in_random_pool | bool | true | If false, this archetype is only selectable through fixed personalities and is never rolled by a normal mercenary spawn. Useful for archetypes that only make sense on named characters. |
The probability of an archetype being rolled is max(0, base_weight + class_weights[entity_class]).
Example
A "stoic" archetype that's slightly more common on warriors and mages, slightly less common on rogues:
{
"id": "stoic",
"display_key": "archetype.magic_realms.stoic",
"base_weight": 10,
"class_weights": {
"mage": 2,
"warrior": 4,
"rogue": -2
},
"in_random_pool": true
}A "lone_wolf" archetype that exists only as a reference for an exclusive named character - never rolled randomly:
{
"id": "lone_wolf",
"display_key": "archetype.magic_realms.lone_wolf",
"base_weight": 0,
"in_random_pool": false
}Don't forget to add the matching archetype.magic_realms.stoic (or whatever) entry to your language file.
Custom fixed personalities
A fixed personality is a pre-rolled set of personality traits - archetype, hobby, hometown, quirks, optional override name - that can be stamped onto a mercenary instead of doing a fresh random roll.
There are three ways a fixed personality reaches a mercenary:
- Exclusive mercenary override - entities like Amadeus and Catas declare their personality directly in code via
getFixedPersonality(). They reference a fixed personality id from the catalog withFixedPersonality.fromCatalog("magic_realms:amadeus"). - Skin preset locking - a skin preset's JSON can specify
fixed_personality_id. Whenever that preset is rolled for a mercenary, the linked personality is also stamped on. Useful for characters whose identity is "this exact skin and this exact personality, always together". - Random pool roll - when a mercenary spawns, there's a flat 10% chance (
FIXED_POOL_ROLL_CHANCE) that they roll from the fixed-personality random pool instead of doing a normal random personality roll.
File location
data/magic_realms/mercenaries/personality/fixed_personalities/<id>.jsonSchema
| Field | Type | Default | Notes |
|---|---|---|---|
archetype | string | - | Required. Short id of an archetype in the catalog (e.g. "stoic"). |
hobby | string | "" (none) | Short id of a hobby. May be omitted. |
hometown | string | "" (none) | Free-form text shown in the personality panel. May be omitted. |
quirks | string array | [] | List of quirk ids. |
override_entity_name | string | absent | If present and non-empty, the mercenary is renamed to this when the personality is applied. |
in_random_pool | bool | true | Whether this entry can be rolled by the random-pool roll. Set to false for preset-locked characters who should only appear on their matching skin. |
weight | int (≥ 1) | 1 | Weight inside the random pool. |
unique | bool | true | If true, once this id has been assigned to any mercenary in the world (even one that has since died), no other mercenary can ever roll it. Used for one-of-a-kind named characters. |
Example: a generic random-pool entry
A friendly bookworm tinker named "Old Marek" who can show up on any mercenary:
{
"archetype": "jovial",
"hobby": "tinkering",
"hometown": "Eastfield",
"quirks": ["bookworm", "early_riser"],
"override_entity_name": "Old Marek",
"in_random_pool": true,
"weight": 1,
"unique": true
}Example: a preset-locked named character
A unique character "Vex" who only appears when their dedicated skin preset rolls - never as a random personality:
{
"archetype": "ruthless",
"hobby": "swordplay",
"hometown": "the Hollow Reach",
"quirks": ["claustrophobic"],
"override_entity_name": "Vex",
"in_random_pool": false,
"unique": true
}You'd then point a skin preset's fixed_personality_id at this entry. (See Custom skin presets.)
Custom hobbies
A hobby is a chat reaction package. When a player types a message near a contracted mercenary, the mercenary checks their hobby's keyword list and may respond from a hobby- and archetype-specific pool.
File location
data/magic_realms/mercenaries/personality/hobbies/<id>.jsonSchema
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | - | Required. Short id used by fixed_personalities JSONs (e.g. "bookworm"). |
display_key | string | - | Required. Translation key for the hobby's human-readable name. |
responses | object: string→string array | - | Required. Map of archetype id → list of translation keys. The special key "default" is used when no matching archetype-specific pool exists. |
in_random_pool | bool | true | Whether this hobby can be picked by the random hobby roll. |
When a mercenary speaks, the resolver looks up responses[archetypeId] first; if the archetype isn't in the map (or its list is empty), it falls back to responses["default"]. If neither exists, the mercenary stays silent.
This is the mechanism that lets a "Bookworm Stoic" sound different from a "Bookworm Jovial" while still sharing the same hobby.
Example
A "tinkering" hobby with a default voice plus dedicated lines for stoic and jovial mercenaries:
{
"id": "tinkering",
"display_key": "hobby.magic_realms.tinkering",
"in_random_pool": true,
"responses": {
"default": [
"speech.my_pack.tinkering.default.1",
"speech.my_pack.tinkering.default.2"
],
"stoic": [
"speech.my_pack.tinkering.stoic.1",
"speech.my_pack.tinkering.stoic.2"
],
"jovial": [
"speech.my_pack.tinkering.jovial.1",
"speech.my_pack.tinkering.jovial.2"
]
}
}The translation keys (speech.my_pack.tinkering.stoic.1, etc.) need matching entries in your language file. Each entry is the actual line the mercenary will say, with %s available for the speaker's name (the formatter splices it in colored).
Custom archetype interactions
An archetype interaction defines what happens when two contracted mercenaries with a specific pair of archetypes are near each other while both contracted by the same player. The most common use is rivalry / friendship buffs and debuffs.
The pair is unordered: archetype_a = "stoic", archetype_b = "jovial" matches both a stoic-near-jovial and a jovial-near-stoic.
File location
data/magic_realms/mercenaries/personality/archetype_interactions/<id>.jsonThe file's full ResourceLocation becomes the interaction's id.
Schema
| Field | Type | Default | Notes |
|---|---|---|---|
archetype_a | string | - | Required. Short id of the first archetype. |
archetype_b | string | - | Required. Short id of the second archetype. May be the same as archetype_a (a self-pair). |
attribute_modifiers | array of entries | [] | List of attribute modifiers applied to both mercenaries while in range. See structure below. |
radius | double | 12.0 | Search radius in blocks. Capped at 64.0. |
kind | string | "" | Optional tag (e.g. "rival", "friend") read by chat banter code. Purely informational; the attribute_modifiers list is the source of truth for stat effects. |
Each entry in attribute_modifiers is:
{
"attribute": "minecraft:generic.movement_speed",
"id": "magic_realms:stoic_jovial.movespeed",
"amount": -0.1,
"operation": "add_multiplied_total"
}operation accepts the vanilla values: add_value, add_multiplied_base, add_multiplied_total. The id field on each entry is purely an authoring label - at runtime, the tick handler synthesizes a stable per-mercenary modifier ResourceLocation from the interaction id and the entry's index, so collisions with other modifiers are impossible.
Example: stoic vs. jovial rivalry
The two are on the same team but don't get along - both lose 10% movement speed and 5% damage while standing near each other:
{
"archetype_a": "stoic",
"archetype_b": "jovial",
"kind": "rival",
"radius": 8.0,
"attribute_modifiers": [
{
"attribute": "minecraft:generic.movement_speed",
"id": "magic_realms:stoic_jovial.movespeed",
"amount": -0.1,
"operation": "add_multiplied_total"
},
{
"attribute": "minecraft:generic.attack_damage",
"id": "magic_realms:stoic_jovial.damage",
"amount": -0.05,
"operation": "add_multiplied_total"
}
]
}Example: stoic-stoic synergy
Two stoics near each other inspire calm - both gain 10% knockback resistance:
{
"archetype_a": "stoic",
"archetype_b": "stoic",
"kind": "friend",
"radius": 12.0,
"attribute_modifiers": [
{
"attribute": "minecraft:generic.knockback_resistance",
"id": "magic_realms:stoic_self.kbr",
"amount": 0.1,
"operation": "add_value"
}
]
}Modifiers are applied as transient modifiers: when the mercenaries move out of range, when one of them is unloaded, when their contract ends, or when the datapack is reloaded, the modifiers come off.
Quirks reference
Quirks are not datapack-driven - they're a hardcoded enum because each one has a corresponding piece of behavior code (a goal, a mood reaction, a dialogue hook). You can't add new quirks, but you can reference any of these in your fixed_personalities:
| Quirk id | Effect summary |
|---|---|
afraid_of_the_dark | Becomes weaker at dark places. |
hates_rain | Becomes weaker when rainy. |
cant_swim | Can't swim in water. |
claustrophobic | Becomes weaker at narrow spaces. |
night_owl | Active and happy at night. Mutually exclusive with early_riser. |
early_riser | Active and happy in the morning. Mutually exclusive with night_owl. |
heat_intolerant | Weaker in hot biomes. Mutually exclusive with cold_intolerant. |
cold_intolerant | Weaker in cold biomes. Mutually exclusive with heat_intolerant. |
height_scared | Weaker at high altitude. |
animal_friend | Stronger when surrounded by animals. |
bookworm | Stronger when carrying books in the inventory. |
glutton | Stronger when carrying food in the inventory. |
A random mercenary rolls 0 to 3 quirks at spawn (15% / 40% / 35% / 10% odds), filtered to remove conflicting pairs. A fixed personality can declare any subset (the conflict filter is not applied to fixed personalities - author beware).
Custom skin parts
Mercenaries that don't roll a preset are built up from four texture layers stacked on top of one another:
skin- base skin tone (face, hands, neutral body).clothes- outfit, armor visuals, robes, etc.eyes- overlaid eye texture.hair- overlaid hair texture.
Each layer is a skin part loaded from a JSON file. At spawn, Magic Realms picks one part of each category, weighted, filtered by the mercenary's gender and entity class.
File location
data/magic_realms/mercenaries/skin_parts/<your_id>.jsonThe texture itself goes in assets/magic_realms/textures/... (or wherever you point texture to).
Schema
| Field | Type | Default | Notes |
|---|---|---|---|
texture | string | - | Required. ResourceLocation of the texture (e.g. "my_pack:textures/entity/human/skin/dark.png"). |
category | string | - | Required. One of "skin", "clothes", "eyes", "hair". |
gender | string | "any" | One of "any", "male", "female". |
entity_class | string | "any" | One of "any", "common", "mage", "rogue", "warrior". "common" is also a wildcard (eligible for every class) but is conventionally used to mark "neutral" pieces like plain shirts. |
weight | int (≥ 1) | 1 | Weight inside its category pool. Higher = more common. |
Example: a female-only mage robe
{
"texture": "my_pack:textures/entity/human/clothes/violet_robes.png",
"category": "clothes",
"gender": "female",
"entity_class": "mage",
"weight": 2
}Example: a generic skin tone usable everywhere
{
"texture": "my_pack:textures/entity/human/skin/freckled.png",
"category": "skin",
"weight": 1
}(Both gender and entity_class default to "any", so they don't need to be listed.)
When the catalog has no matching parts for a given (category, gender, class) combination, the mercenary spawns with that layer missing - Magic Realms logs a warning and continues. So if you're filtering aggressively, make sure each (category, gender, class) triple still has something eligible (or rely on "any" fallbacks).
Custom skin presets
A skin preset is a single complete texture used as-is, instead of compositing the four-layer skin parts. They're meant for handcrafted / iconic looks where you want every pixel to be exactly right.
When a mercenary spawns, there's a chance (configurable via customTextureChance, default ~15%) that a preset is rolled instead of building from parts. If the preset specifies fixed_personality_id, the linked fixed personality is also stamped on the mercenary, so identity (look + behavior) stays welded together.
File location
data/magic_realms/mercenaries/skin_presets/<your_id>.jsonSchema
| Field | Type | Default | Notes |
|---|---|---|---|
texture | string | - | Required. ResourceLocation of the full preset texture. |
display_name | string | absent | Optional. If set, the mercenary is named this. |
gender | string | "any" | One of "any", "male", "female". Used to filter the pool by the mercenary's gender. |
weight | int (≥ 1) | 1 | Weight inside the preset pool. |
fixed_personality_id | string | absent | Optional. Full ResourceLocation of a fixed personality to lock to this preset (e.g. "my_pack:vex"). |
added_to_pool | boolean | true | Optional. When false, the preset is loaded but excluded from the random roll pool. Useful for future bandit profiles. |
If both display_name and the linked fixed personality's override_entity_name are present, the preset's display_name wins. If only the personality has an override name, that override is used as the mercenary's display name.
Example: a generic preset
A finely-detailed female ranger that can roll on any random mercenary:
{
"texture": "my_pack:textures/entity/human/preset/ranger_alia.png",
"display_name": "Alia",
"gender": "female",
"weight": 3
}Example: an identity-locked preset
A preset for the named character "Vex" defined earlier - appearing only when this preset rolls, and bringing their full personality with them:
{
"texture": "my_pack:textures/entity/human/preset/vex.png",
"gender": "female",
"weight": 1,
"fixed_personality_id": "my_pack:vex"
}Notice we don't need display_name here - the override name comes from the linked fixed personality.
Custom titles
A title is an award a mercenary earns for something they did - a kill count, a place they visited, a stretch of service, a beating they survived.
Titles apply to all mercenaries, contracted or hostile. Bandits can be given titles outright through a bandit profile, and can also earn them through play like anyone else.
File location
data/magic_realms/mercenaries/titles/<id>.jsonThe file's path is the title's stable identifier (e.g. data/magic_realms/mercenaries/titles/dragonslayer.json becomes magic_realms:dragonslayer).
Schema
| Field | Type | Default | Notes |
|---|---|---|---|
display_key | string | required | Translation key for the title's name. Needs a matching entry in your language file. |
description_key | string | "" | Translation key for the tooltip line shown under the name in the title selector. |
color | string | gold | "#RRGGBB" or a vanilla colour name ("gold", "dark_aqua"…). Colours the nameplate text. |
priority | int | 0 | Higher wins the automatic nameplate pick when a mercenary holds several. Also the sort order in the selector. |
requirements | list | [] | Conditions to earn it. An empty list means the title can never be earned naturally - see Unobtainable titles. |
requirement_mode | "all" / "any" | "all" | Whether every requirement must be met, or just one. |
rewards | object | {} | What holding the title grants. See below. |
announce | bool | true | Whether the contractor gets a chat message when it's earned. |
hidden | bool | false | If true, the title still applies its rewards but never appears in the selector and is never auto-displayed. Useful for gating chains and for secrets. |
Requirement types
Each entry in requirements is { "type": …, "target": …, "amount": … }. target and amount are optional and mean different things per type.
type | target | amount |
|---|---|---|
total_kills | - | kills of any hostile mob |
boss_kills | - | kills of mobs in the Magic Realms bosses tag |
kill_entity | entity type id | kills of that type |
kill_entity_tag | entity type tag id | kills summed across the tag |
visit_structure | structure id | separate visits |
visit_structure_tag | structure tag id | visits summed across the tag |
contract_minutes | - | cumulative minutes under contract |
damage_dealt | - | cumulative damage points dealt |
damage_taken | - | cumulative damage points taken |
star_level | - | minimum star level |
entity_class | "mage" / "warrior" / "rogue" | - |
has_title | another title's id | - |
A leading # on a tag id is accepted and ignored, so "minecraft:undead" and "#minecraft:undead" both work.
Structure visits count the entry, not the time spent - standing in a fortress doesn't inflate the counter, but leaving and returning does.
Reward types
Everything in rewards is optional. A purely cosmetic title just leaves it out.
| Field | Type | Notes |
|---|---|---|
attribute_modifiers | list | Standard AttributeModifier entries. Same format as bandit attribute boosts. |
passive_effects | list | { "effect", "amplifier", "show_particles" } - kept permanently refreshed on the mercenary. |
immune_effects | list of effect ids | The mercenary cannot receive these effects from any source, and any already active are cleared. |
on_hit_effects | list | { "effect", "duration", "amplifier", "chance" } - rolled onto the victim on each hit. |
bonus_damage | double | Flat damage added to every attack. |
damage_bonus_vs | list | { "entity_tag", "multiplier" } - Bane-of-Arthropods style scaling against a family of mobs. |
weapon_bonus | list | { "item", "item_tag", "multiplier", "bonus" } - scaling that applies while holding a matching mainhand item. Covers ranged attacks, since an arrow's damage belongs to the shooter. |
incoming_damage_multiplier | double | 0.9 = 10% less damage taken. Applied before armor. |
natural_regen | bool | Grants out-of-combat health regeneration. |
For weapon_bonus, set item, item_tag, or both - matching either is enough. Keep them in one entry rather than two, or a weapon matching both gets the multiplier applied twice.
How rewards stack
A mercenary holding several titles gets all of their rewards at once, and multipliers multiply:
incoming_damage_multipliercompounds downward, so five titles at0.9give0.59(41% reduction), not 50%. This is self-limiting - each title takes a share of what's left - so stacking approaches immunity without reaching it.damage_bonus_vsandweapon_bonusmultipliers compound upward, and this is where over-tuning bites. A title at1.5×against undead, plus another at3×, plus a1.3×axe bonus, is5.85×against a zombie holding an axe.
Flat values (bonus_damage, add_value attribute modifiers) simply sum. As a rule of thumb, keep early titles flat and save multiplicative rewards for capstones.
Example: a kill milestone
The simplest useful shape - one requirement, one flat reward.
{
"display_key": "title.magic_realms.veteran",
"description_key": "title.magic_realms.veteran.desc",
"color": "gold",
"priority": 40,
"requirements": [
{ "type": "total_kills", "amount": 1000 }
],
"rewards": {
"attribute_modifiers": [
{
"attribute": "minecraft:generic.armor",
"id": "magic_realms:veteran_armor",
"amount": 3.0,
"operation": "add_value"
}
],
"incoming_damage_multiplier": 0.95
}
}Example: a class-locked weapon specialist
Two requirements in "all" mode, rewarding a fighting style rather than raw stats.
{
"display_key": "title.magic_realms.axemaster",
"description_key": "title.magic_realms.axemaster.desc",
"color": "#C97B4A",
"priority": 65,
"requirement_mode": "all",
"requirements": [
{ "type": "entity_class", "target": "warrior" },
{ "type": "total_kills", "amount": 400 }
],
"rewards": {
"weapon_bonus": [
{ "item_tag": "c:tools/axe", "multiplier": 1.3, "bonus": 1.0 },
{ "item": "minecraft:netherite_axe", "bonus": 2.0 }
]
}
}The netherite axe matches both entries, so it picks up the tag bonus and its own - that's the intended way to give one item extra weight on top of a family.
Example: exploration, with two routes to the same reward
"any" mode, plus a status immunity and a passive effect.
{
"display_key": "title.magic_realms.unclouded",
"description_key": "title.magic_realms.unclouded.desc",
"color": "#A8D8E8",
"priority": 60,
"requirement_mode": "any",
"requirements": [
{ "type": "visit_structure", "target": "minecraft:ancient_city" },
{ "type": "damage_taken", "amount": 4000 }
],
"rewards": {
"immune_effects": [
"minecraft:blindness",
"minecraft:darkness",
"minecraft:nausea"
],
"passive_effects": [
{ "effect": "minecraft:night_vision", "show_particles": false }
]
}
}Example: a title chain
has_title lets a capstone require others first. Because evaluation cascades within a single pass, a chain resolves the moment its last prerequisite is met.
{
"display_key": "title.magic_realms.living_legend",
"description_key": "title.magic_realms.living_legend.desc",
"color": "#FFE066",
"priority": 200,
"requirement_mode": "all",
"requirements": [
{ "type": "has_title", "target": "magic_realms:veteran" },
{ "type": "has_title", "target": "magic_realms:sworn_sword" },
{ "type": "boss_kills", "amount": 10 }
],
"rewards": {
"attribute_modifiers": [
{
"attribute": "minecraft:generic.attack_damage",
"id": "magic_realms:legend_damage",
"amount": 0.15,
"operation": "add_multiplied_total"
}
],
"natural_regen": true
}
}Unobtainable titles
A title with no requirements is never earned naturally. It can only be granted deliberately - by command, by a bandit profile, or by code. This is the shape to use for story rewards, signature titles belonging to a named character, or anything you want to hand out on your own terms.
{
"display_key": "title.magic_realms.mace_lord",
"description_key": "title.magic_realms.mace_lord.desc",
"color": "#B08D57",
"priority": 150,
"announce": false,
"requirements": [],
"rewards": {
"weapon_bonus": [
{ "item": "minecraft:mace", "item_tag": "c:tools/mace", "multiplier": 1.5 }
]
}
}Set "hidden": true as well if the title should apply its rewards without ever appearing on the nameplate or in the selector.
Managing titles in game
/human title list <target>
/human title progress <target>
/human title grant <target> <title>
/human title revoke <target> <title>
/human title display <target> <title>
/human title clear <target>progress is the useful one while authoring: it lists every unearned title with a completion percentage and a per-requirement breakdown, so you can see exactly how far off a mercenary is. grant bypasses requirements entirely, which is how you test a title without grinding for it.
Custom bandit profiles
A bandit profile is a preset stamped onto a hostile mercenary (bandit) at spawn time, customizing its class, titles, gear, scale, and so on. Profiles let you build varied bandit encounters - a giant brute leading a raid, a fire-mage miniboss in a tower, a hooded archer ambush - and everything data driven.
Bandit profiles are applied to hostile mercenaries only (HostileRandomHumanEntity), the bandit / evil-mercenary variant. Regular contracted mercenaries are unaffected. Each field of a profile is optional: anything you leave out falls back to the random-roll behavior an unprofiled bandit would have, so a profile only constrains what it explicitly sets.
File location
data/magic_realms/mercenaries/bandit_profiles/<id>.jsonThe file's path is the profile's stable identifier (e.g. data/magic_realms/mercenaries/bandit_profiles/giant_warrior.json becomes magic_realms:giant_warrior).
Schema
| Field | Type | Default | Notes |
|---|---|---|---|
weight | int | 1 | Weighted-random selection weight, used when calling pickRandomFiltered. |
entity_class | "mage" / "warrior" / "rogue" | random | Pins the class. Without this, the class is rolled normally. |
gender | "male" / "female" | random | Pins the gender. |
has_shield | bool | random | If you pined warrior, this sets if it should have shield. |
is_archer | bool | random | If you pined rogue, this sets if it should be an archer or an assassin. |
star_level | int 1–3 | random | Stat tier, affects the starting attributes. |
titles | list of resource locations | [] | Titles granted outright at spawn, bypassing their normal requirements. All of their rewards apply exactly as if earned. Unknown ids are skipped with a warning. |
displayed_title | resource location | - | Which of the granted titles shows above the bandit's name. Must be one of them. If omitted, the highest-priority non-hidden granted title is used. |
entity_scale | float | 1.0 | Resizes the bandit, useful for tiny or giant bandits. 1.5 = 50% bigger. |
override_name | string | - | Replaces the rolled name (e.g. "Hulking Brute"). |
skin_preset | resource location | - | Id of an entry in your skin_presets/ catalog (the resource location of its JSON file, e.g. "magic_realms:vex" for data/magic_realms/mercenaries/skin_presets/vex.json). The full preset is applied - texture, optional display name, and optional fixed_personality_id binding all carry over. Unknown ids log a warning and fall back to layered random skin. |
magic_schools | list of resource locations | [] | Mage-only. Explicit list of schools. |
magic_schools_tag | resource location | - | Mage-only. Pulls schools from a tag (used if magic_schools is empty). |
explicit_spells | list of resource locations | [] | Exact spells the bandit casts. Highest priority. |
spells_tag | resource location | - | Pull spells from a tag (used if explicit_spells is empty). |
spells_tag_pick_count | int | full tag size | When spells_tag is set, randomly pick this many spells from the tag instead of taking all. |
equipment | object: slot→item id | {} | Slot keys: "mainhand", "offhand", "head", "chest", "legs", "feet". |
attribute_boosts | list | [] | Flat attribute modifiers applied at the end of init, on top of class and level scaling. See below. |
loot_table | resource location | - | Overrides the bandit's loot table on death (e.g. "magic_realms:entities/chuchu"). |
is_mini_boss | bool | false | Marker flag readable as profile.isMiniBoss() for possible features (boss bar, drops, etc.). |
immortal | bool | false | Sets the immortal flag - bandit will be stunned instead of dying. |
fixed_personality_id | string | - | References an entry from your fixed_personalities catalog. Replaces the random archetype roll. |
in_random_pool | bool | true | If false, the profile can only be invoked by id (commands, structures, code) and never selected by random pickers. |
Difficulty is expressed through star_level, attribute_boosts, equipment and titles rather than a level range. Granting titles is usually the better lever of the two: the buff shows up on the bandit's nameplate, so a player who sees Unbroken floating over a raid leader understands why it isn't dying, instead of running into an invisible stat multiplier.
The spell rules in priority order: explicit_spells first, then spells_tag (with optional spells_tag_pick_count), then fall through to the regular class-driven generator.
Attribute boost format
Each boost uses Mojang's standard AttributeModifier codec. The id must be unique per boost - convention is to namespace it under your pack and the profile id:
{
"attribute": "minecraft:generic.max_health",
"id": "magic_realms:bandit/giant_warrior/health",
"amount": 0.5,
"operation": "add_multiplied_base"
}Operations: add_value (flat), add_multiplied_base (% of base), add_multiplied_total (% of post-modifier total). Boosts apply after class/level attribute scaling, so add_multiplied_base of 0.5 on generic.max_health cleanly adds 50% on top of whatever the entity would normally have at its level.
Example: a giant warrior raid leader
A warrior bandit scaled up 1.5×, with full iron armor, 50% more HP, 50% knockback resistance, +3 attack damage, and two granted titles doing further work:
{
"weight": 5,
"entity_class": "warrior",
"has_shield": false,
"titles": [
"magic_realms:veteran",
"magic_realms:unbroken"
],
"displayed_title": "magic_realms:unbroken",
"entity_scale": 1.5,
"override_name": "Hulking Brute",
"equipment": {
"mainhand": "minecraft:netherite_axe",
"head": "minecraft:iron_helmet",
"chest": "minecraft:iron_chestplate",
"legs": "minecraft:iron_leggings",
"feet": "minecraft:iron_boots"
},
"attribute_boosts": [
{
"attribute": "minecraft:generic.max_health",
"id": "magic_realms:bandit/giant_warrior/health",
"amount": 0.5,
"operation": "add_multiplied_base"
},
{
"attribute": "minecraft:generic.knockback_resistance",
"id": "magic_realms:bandit/giant_warrior/kb_resist",
"amount": 0.5,
"operation": "add_value"
},
{
"attribute": "minecraft:generic.attack_damage",
"id": "magic_realms:bandit/giant_warrior/damage",
"amount": 3.0,
"operation": "add_value"
}
],
"loot_table": "magic_realms:entities/giant_warrior",
"is_mini_boss": true
}Example: a fire-mage miniboss
A three-star mage bandit casting a hand-picked fire and lightning kit, in netherite mage armor with a blaze manual spellbook in the offhand, plus double base health:
{
"weight": 2,
"entity_class": "mage",
"star_level": 3,
"titles": [
"magic_realms:archmage"
],
"override_name": "Pyromancer Initiate",
"magic_schools": [
"irons_spellbooks:fire",
"irons_spellbooks:lightning"
],
"explicit_spells": [
"irons_spellbooks:fireball",
"irons_spellbooks:firebolt",
"irons_spellbooks:lightning_bolt",
"irons_spellbooks:teleport",
"irons_spellbooks:ball_lightning"
],
"equipment": {
"head": "irons_spellbooks:netherite_mage_helmet",
"chest": "irons_spellbooks:netherite_mage_chestplate",
"legs": "irons_spellbooks:netherite_mage_leggings",
"feet": "irons_spellbooks:netherite_mage_boots",
"offhand": "irons_spellbooks:blaze_spell_book"
},
"attribute_boosts": [
{
"attribute": "minecraft:generic.max_health",
"id": "magic_realms:bandit/mage_miniboss/health",
"amount": 1.0,
"operation": "add_multiplied_base"
}
],
"is_mini_boss": true
}Spawning bandits with a profile
Command - right now it's only available via commands for testing around:
/bandit summon "magic_realms:giant_warrior"
/bandit summon "magic_realms:mage_miniboss" 100 64 -200
/bandit listCustom bandit spawns
Bandit profiles describe what a bandit is. This describes where they come from: magic_realms:add_bandit_spawns is a custom NeoForge biome modifier that adds natural bandit spawns to a set of biomes, and optionally pins those spawns to a weighted pool of profiles.
The entity type is implicit - this modifier only ever spawns bandits, so there's no entity field to get wrong.
File location
Biome modifiers are a NeoForge datapack registry, so they don't live under mercenaries/ with the rest:
data/<your_namespace>/neoforge/biome_modifier/<id>.jsonSchema
| Field | Type | Default | Notes |
|---|---|---|---|
type | string | required | Always "magic_realms:add_bandit_spawns". |
biomes | biome id, list, or tag | required | Which biomes gain the spawn entry. A tag reference needs the leading #. |
weight | int ≥ 0 | required | Spawn weight, relative to every other entry in the monster category for that biome. Vanilla overworld zombies sit around 95, skeletons 100, so a value in the tens makes bandits uncommon but not rare. 0 disables the entry. |
minCount | int ≥ 1 | 1 | Minimum group size per spawn attempt. |
maxCount | int ≥ 1 | 1 | Maximum group size. Must be ≥ minCount - the codec rejects the file otherwise, with an error naming both values. |
profiles | list | [] | Weighted pool of bandit profiles to pin spawns to. Omit it and bandits spawned here roll from the normal random pool instead. |
Each entry in profiles is:
| Field | Type | Default | Notes |
|---|---|---|---|
id | resource location | required | A profile id from your bandit_profiles/ catalog. Unknown ids log a warning and the bandit falls back to a random roll. |
weight | int ≥ 1 | 1 | Relative weight within this modifier's pool only. |
How a profile gets chosen
Worth understanding, because it explains why weight does double duty.
Vanilla's spawner doesn't record which biome modifier produced a given spawn - it merges every entry into one weighted list and rolls once. So when a bandit spawns, Magic Realms reconstructs the choice in two steps:
- Which modifier? Every
add_bandit_spawnsmodifier that has a non-empty profile pool and covers the bandit's biome becomes a candidate, weighted by its ownweight. This means two modifiers overlapping the same biome divide spawns in proportion to their spawn weights, which is what you'd intuitively expect. - Which profile? A second weighted roll inside that modifier's
profileslist.
Example: plain spawns, no profiles
Bandits appear in the biomes in your tag, in groups of one to three, using the normal random profile pool.
{
"type": "magic_realms:add_bandit_spawns",
"biomes": "#magic_realms:bandit_spawns_in",
"weight": 15,
"minCount": 1,
"maxCount": 3
}Example: a weighted profile pool
Highwaymen are common, the captain is a rare escort. With these weights the captain is one spawn in six.
{
"type": "magic_realms:add_bandit_spawns",
"biomes": "#magic_realms:bandit_spawns_in",
"weight": 20,
"minCount": 2,
"maxCount": 4,
"profiles": [
{ "id": "magic_realms:highwayman", "weight": 5 },
{ "id": "magic_realms:bandit_captain", "weight": 1 }
]
}Example: biome-specific flavour
Targeting a small set of biomes directly rather than through a tag, to give the desert its own bandit type:
{
"type": "magic_realms:add_bandit_spawns",
"biomes": [
"minecraft:desert",
"minecraft:badlands"
],
"weight": 10,
"minCount": 1,
"maxCount": 2,
"profiles": [
{ "id": "magic_realms:sand_raider", "weight": 1 }
]
}Targeting biomes that might not exist
Referencing an unregistered biome id directly crashes on world load. If you want to target a biome from a mod that may not be installed, put it in a biome tag as an optional entry and point biomes at the tag:
{
"replace": false,
"values": [
"minecraft:plains",
{ "id": "biomesoplenty:lavender_field", "required": false }
]
}Turning spawns off
Because biome modifiers are a datapack registry, a pack can override any of Magic Realms' spawn modifiers by placing a file at the same path and name with NeoForge's no-op type:
{
"type": "neoforge:none"
}Setting "weight": 0 on your own modifier achieves the same thing for files you control.
