-
Notifications
You must be signed in to change notification settings - Fork 0
Mob Scaling
Hostile mobs gain levels based on distance from spawn, biome, nearby structures, how many players are nearby, and whether it's currently night or a thunderstorm. A mob's level scales its health, attack damage, armor, movement speed, follow (aggro) range, knockback resistance, and attack speed, and shows in its name (&cLv. {level} {name} by default). Higher-level mobs also fight smarter — see Smart aggro, Dynamic archery, and Advanced AI below — and the 4 major bosses use an entirely separate scaling curve with their own visual/equipment flourishes — see Boss hard curve and the Elite sections below.
Use /rpgmood info in-game to see the zone and mob levels at your current location — the fastest way to sanity-check your tuning.
Every mob spawn (CreatureSpawnEvent, except SpawnReason.CUSTOM) is checked against mob_scaling.enabled and mob_scaling.hostile-only:
-
hostile-only: true(default) — only mobs that are aMonster(zombies, skeletons, spiders, creepers, endermen, etc.) get scaled -
hostile-only: false— all living entities get scaled, including passive mobs
A mob is scaled at most once — a scoreboard tag (rpgmood_scaled) marks it as already processed.
radialLevel = floor(distanceFromSpawn / step_distance)
adjustedBase = max(0, baseLevel - 2)
total = adjustedBase
+ max(0, radialLevel - 1)
+ biomeBonus
+ structureBonus
+ nearbyPlayers * players_bonus_per_player
+ situationalBonus
level = clamp(total, 1, max-level)
-
baseLevel— frommob_scaling.base-levels.<ENTITY_TYPE>(default1for unlisted types). This is a difficulty seed per mob type — e.g.ZOMBIE: 1,WITHER_SKELETON: 5— not the mob's actual displayed level. -
radialLevel— how manystep_distanceblocks (default180) the mob is from world spawn. The first step doesn't add a level (onlyradialLevel - 1counts), so mobs right around spawn stay near the floor. -
biomeBonus— frommob_scaling.biome-bonuses.<BIOME>(default0). -
structureBonus— frommob_scaling.structure-bonuses.<structure_key>if the mob is near a matching vanilla structure (see below). -
nearbyPlayers— count of players within ~30 blocks (900 blocks²) of the mob, each contributingplayers_bonus_per_player(default1). This means grinding spots with several players standing close together will push mob levels up — by design, but worth knowing before you build a shared farm near spawn. -
situationalBonus—mob_scaling.night_bonus(default2) if it's currently vanilla night (world time13000–23000) where the level is being computed, plusmob_scaling.thunder_bonus(default2) if that world is currently thundering — both can apply at once. Checked live (World#getTime()/World#isThundering()) every time a level is computed, not just baked in at mob spawn, so/rpgmood info, the sidebar scoreboard, and/rpgmood zones's danger column all reflect the current moment. - The result is always clamped between
1andmax-level(default40).
structure-bonuses keys (stronghold, desert_pyramid, jungle_temple, woodland_mansion, ocean_monument, fortress, ancient_city) must match Minecraft's real StructureType registry keys. When a mob spawns, RPGMood checks whether it's near any configured structure using Bukkit's locateNearestStructure — one of the more expensive calls in the API.
Fixed in v1.9.0: the shipped config used jungle_pyramid and nether_fortress, neither of which is a real registry key (the actual keys are jungle_temple and fortress) — mobs near a Jungle Temple or Nether Fortress silently never got their configured bonus. ancient_city was added later as a new structure key (Deep Dark's structure, tying into the new subzone and elite-environment content below).
To keep this cheap at scale, results are cached per 128×128-block grid cell (capped at 4096 cached cells, LRU-evicted). The first mob to spawn in a given cell pays the lookup cost; every subsequent mob in that same cell reuses the cached bonus. Since vanilla structures don't move once generated, this cache never needs to be invalidated.
Given a resolved level, applied via Bukkit attributes:
| Attribute | Formula | Floor / Cap |
|---|---|---|
| Max Health | vanillaMaxHealth * multiplier(level) |
floor 4.0
|
| Attack Damage | vanillaAttackDamage * multiplier(level) |
floor 0.1
|
| Follow Range |
vanillaFollowRange * multiplier(level), then hard-capped |
floor 4.0, cap mob_scaling.follow_range_cap (default 36) |
| Armor | (level - 1) * armor_per_level |
floor 0.0
|
| Movement Speed | base speed + (level - 1) * speed_per_level
|
floor 0.01
|
| Knockback Resistance | see Knockback resistance & attack speed | cap 1.0
|
| Attack Speed | see Knockback resistance & attack speed | floor 0.1
|
Health, damage, and follow range are read from each mob's own vanilla default (AttributeInstance# getDefaultValue()) rather than a flat number shared by every mob type — a scaled Enderman is
tankier (and, now, more alert) than a scaled Zombie at the same level, exactly like their unscaled vanilla selves.
Follow range is aggro distance — how far away a mob notices and starts chasing a player. It shares the exact same multiplier(level) curve as health/damage: a weak level-1 mob notices players from a bit less far away than vanilla (early_game_fraction of vanilla range), while a strong mob at or past parity_level notices from just as far as vanilla, or farther. Unlike health/damage there's no separate "rewards" angle here — it's purely about a scaled mob's danger feeling proportionate to its numeric level, not just its stats once a fight starts.
As of v1.15.0, the scaled follow range is additionally hard-capped at mob_scaling.follow_range_cap (default 36 blocks) after the multiplier is applied — a pathfinding safety net so a very high-level mob's aggro range never scans an absurd distance, regardless of how high max-level or the multiplier curve go.
multiplier(level) = early_game_fraction + (level - 1) * perLevelGrowth
perLevelGrowth = (1.0 - early_game_fraction) / (parity_level - 1)
- At level 1, a mob is at
early_game_fraction(default0.85) of its own vanilla health and damage together — weaker than vanilla, evenly, without one stat crashing far harder than the other. - At
parity_level(default8), the multiplier hits exactly1.0— the mob is fully vanilla-equivalent in both stats at the same time. - Beyond that it keeps climbing at the same linear rate — at the level cap (
40, default) the multiplier is roughly1.7, i.e. about 70% stronger than vanilla in health and damage alike.
Armor and movement speed are unaffected by this curve and keep their own simple per-level
bonus. Defaults: armor_per_level: 0.25, speed_per_level: 0.0015.
Before this rework, health and damage used two disconnected flat formulas that reached "vanilla-equivalent" at wildly different levels (health around level 4, damage not until around level 21) — a mid-level mob could have full vanilla health but still hit noticeably softer than vanilla. The shared multiplier fixes that.
Added in v1.15.0, both only apply if the mob's entity type actually exposes the attribute in the first place (many vanilla monsters don't expose ATTACK_SPEED at all):
knockbackResist(level) = clamp(0, (level - min-level) * per-level, max)
attackSpeedBonus(level) = clamp(0, (level - min-level) * per-level, max-bonus)
| Attribute | Config | Default min-level
|
Default per-level
|
Default cap |
|---|---|---|---|---|
| Knockback Resistance | mob_scaling.knockback_resist |
25 (bosses: 15) |
0.02 |
0.85 (bosses get +0.15 more at level 25+, capped at 0.85 total) |
| Attack Speed | mob_scaling.attack_speed |
20 |
0.02 |
0.4 |
Below min-level, neither bonus applies at all — a level-1 zombie takes knockback exactly like vanilla. The four bosses get a lower knockback-resistance threshold (15 instead of 25) since they're already balanced around never actually being weak.
Added in v1.18.0. Warden, Wither, Elder Guardian, and Ender Dragon no longer use the shared early_game_fraction/parity_level multiplier — instead they get their own separate curve, mob_scaling.boss_scaling:
bossMultiplier(level) = clamp(1.0, 1.0 + max(0, level - baseLevel) * growth_per_level_above_base, max_multiplier)
- At the boss's own
base-levelsentry (ELDER_GUARDIAN: 14,WITHER: 18,WARDEN: 20,ENDER_DRAGON: 22), the multiplier is exactly1.0— vanilla stats, never weaker. - Each level above that adds
growth_per_level_above_base(default0.12) to the multiplier, capped atmax_multiplier(default4.0). - Boss armor uses its own
boss_scaling.armor_per_level(default0.75) instead of the regularmob_scaling.armor_per_level. - This applies to health and melee attack damage exactly like the regular curve (same attributes, different multiplier source) — plus each boss's special, non-attribute-driven attacks are scaled by the same multiplier: Warden sonic boom, Wither skulls, and Ender Dragon breath/fireballs (all normally deal a fixed vanilla amount regardless of any attribute). This closes the gap where a fully-scaled boss could still be trivialized by Legendary netherite gear at level 30+ because its "special" attacks weren't scaling at all.
Set mob_scaling.boss_scaling.enabled: false to fall back to the regular shared curve for bosses too (not recommended — bosses' base-levels are set well above parity_level specifically so they only ever get buffed under this curve).
Added in v1.15.0. A synchronous scan every second (SmartAggroTask) iterates online players → nearby living entities (never the whole world) and force-targets high-level mobs that don't already have a valid target, governed by mob_scaling.smart_aggro:
| Level range | Behavior | Radius |
|---|---|---|
Below los-min-level (default 15) |
Untouched — normal vanilla aggro only | — |
los-min-level–omniscient-min-level - 1 (default 15–29) |
Aggros a nearby player only with line of sight |
los-radius (default 28 blocks) |
omniscient-min-level+ (default 30+) |
Aggros a nearby player unconditionally, no line of sight required |
omniscient-radius (default 36 blocks) |
Budgeted at max-retargets-per-tick (default 40, server-wide per scan) so a crowded area can't cause a pathfinding storm, plus a per-mob cooldown-ticks (default 40) between forced retargets. This is on top of — not a replacement for — each mob's normal vanilla targeting; it only steps in when a qualifying mob currently has no living target.
Added in v1.15.0 (BowAimListener), recalculates a skeleton/stray/pillager's arrow velocity on EntityShootBowEvent based on the shooter's level, via mob_scaling.archery:
| Level range | Aim behavior |
|---|---|
1–clumsy-max-level (default 1–7) |
Clumsy — random spread added to the shot (clumsy-spread, default 0.35), even if it has no target |
clumsy-max-level+ to laser-min-level − 1 (default 8–14) |
Untouched vanilla aim |
laser-min-level+ (default 15+) |
Laser — aims precisely at the target player's eye location |
predictive-min-level+ (default 30+) |
Predictive lead — aims ahead of the target based on their current velocity and estimated arrow flight time, with sculk-soul particle tracers (tracer-particles, on by default) following the arrow in flight |
Only fires when the shooter has a Player as its current target — a mob with no target or a non-player target only gets the clumsy-spread treatment (if low enough level) and nothing else. Separately, any arrow fired by a scaled mob that hits a player during Winter applies a short Slowness (mob_scaling.seasonal.winter.arrow-slowness-ticks/arrow-slowness-amplifier) — see Seasonal combat.
Three independent behaviors under mob_scaling.advanced_ai, applied at spawn time alongside the rest of scaling:
Pack assist (pack, PackAssistListener) — when a level min-level+ (default 20) scaled mob is hit by a player, up to max-allies (default 5) same-entity-type allies within radius blocks (default 20) that don't already have a target are set to target that player. Gated by a per-chunk cooldown (chunk-cooldown-seconds, default 3) so a crowd being farmed doesn't retrigger a pathfinding storm on every single hit.
Archer kite (kite, ArcherKiteGoal) — Skeletons, Strays, and Pillagers at level min-level+ (default 30) get a Paper MobGoal that backs away while keeping distance (keep-distance, default 8 blocks) whenever their target player is closer than that and wielding a sword/axe/trident/mace (or empty-handed) — i.e. an actual melee threat, not just "target is near." They keep shooting while retreating; a target with a ranged weapon out doesn't trigger the kite.
Relentless creepers (creeper) — level min-level+ (default 25) creepers get a shorter max fuse: base-fuse-ticks (default 30) at min-level, shrinking by 1 tick per level above that down to a floor of min-fuse-ticks (default 12). If advance-while-ignited (default true), an ignited creeper keeps pathfinding toward its target instead of just standing and hissing — no more sidestepping a lit creeper by walking backward.
Added in v1.15.0, mob_scaling.seasonal applies effects based on the current farming season at spawn time (or on hit, for the arrow effect):
| Season | Effect |
|---|---|
| ❄️ Winter | Scaled mobs get +winter.armor-bonus (default 2.0) armor and take 15% less damage; their arrows apply Slowness on hit (arrow-slowness-ticks/arrow-slowness-amplifier, default 40 ticks / amplifier 0) |
| 🍂 Autumn |
+autumn.swift-chance-bonus (default 0.20) forced additional chance of rolling the Swift affix on top of the normal level-based odds |
| ☀️ Summer | Fire/desert-affiliated mobs (Blaze, Magma Cube, Hoglin, Piglin(s), Wither Skeleton, Ghast, or any mob in a desert/badlands/Nether biome) have a summer.burn-chance (default 0.35) to set an attacking player on fire (summer.burn-ticks, default 60) when they land a hit |
Set mob_scaling.seasonal.enabled: false to disable all three at once.
Added in v1.18.0 (EliteGlowService), mob_scaling.elite_glow gives level min-level+ (default 35) mobs and all 4 bosses a colored glowing outline (visible through walls, like vanilla Glowing) via scoreboard teams, only while in combat — applied on any damage exchange between the mob and a player, and automatically cleared combat-timeout-seconds (default 10) after the last hit, or immediately on death. Capped at max-active (default 64) glowing mobs server-wide at once to bound the visual/scoreboard-team cost.
Color is chosen by, in priority order: Poisonous affix → green, Bleeding affix → red, boss or Warden → purple, Nether/Desert/Badlands biome → red, Deep Dark/Dripstone/Lush Caves biome → purple, otherwise → gold.
Added in v1.18.0 (EliteEquipmentService), rolls an offhand item for qualifying mobs once, at spawn, under mob_scaling.elite_equipment:
| Roll (checked in order) | Condition | Chance | Effect |
|---|---|---|---|
| Totem of Undying | Zombie-family (Zombie/Husk/Drowned/Zombie Villager), level totem-min-level+ (default 30) |
totem-chance (default 0.08) |
Vanilla resurrect behavior — consumed automatically like a player's totem, then never re-rolled on that mob |
| Shield | Zombie-family, level shield-min-level+ (default 20) — only rolled if no totem was given |
shield-chance (default 0.25) |
Reduces incoming damage by shield-damage-reduce (default 30%) when the attack comes from roughly in front of the mob (frontal arc only — attacking from behind/the side bypasses it entirely) |
| Soul Torch | Any qualifying mob, level torch-min-level+ (default 15), in a deep biome (Deep Dark/Dripstone Caves/Lush Caves, or Y < 0) |
torch-chance (default 0.40) |
Purely visual/flavor — no mechanical effect |
Added in v1.18.0 (EliteEnvironmentListener), under mob_scaling.environment (plus mob_scaling.corrupt_sounds):
-
Corrupt hurt sounds (
corrupt_sounds.enabled) — level 20+ zombies/skeletons taking damage in a deep biome (Deep Dark/Dripstone Caves/Lush Caves, or Y < 0) play a quiet Warden-hurt sound instead of their normal hurt sound. -
Fire/lava immunity (
environment.fire-immune-min-level, default25) — mobs at or above this level in the Nether or a desert/badlands biome are immune to lava, fire, fire-tick, and hot-floor damage entirely. -
Spider projectile dodge (
environment.spider-dodge-min-level, default25) — Spiders/Cave Spiders hit by a player's arrow have a chance (spider-dodge-chance-per-levelper level above the minimum, capped atspider-dodge-max-chance, defaults0.02/0.35) to cancel the hit entirely and dash sideways instead, on a 2-second per-spider cooldown.
Killing a scaled mob grants bonus XP on top of its vanilla drop: level * mob_scaling.bonus_xp_per_level (default 2). A level 20 mob gives +40 bonus XP. Set bonus_xp_per_level: 0 to disable.
A mob's level also tracks toward your /rpgmood leaderboard level entry (highest-level mob you've ever killed), and cumulative scaled-mob kills unlock the Slayer/Danger Seeker achievements — see Commands.
Scaled mobs show a subtle, colour-tiered particle aura at their feet — visible only to players within ~30 blocks, so it doesn't clutter the view from a distance:
| Level | Aura |
|---|---|
| 1–7 | None |
| 8–14 | Barely-visible blue shimmer |
| 15–24 | Tiny warm yellow glow |
| 25–34 | Small orange ember |
| 35+ | Modest red glow |
Toggle with mob_scaling.particle_aura in config.yml (default true). See Configuration.
When a mob's computed level crosses mob_scaling.announcement.min-level (default 25), RPGMood broadcasts a server-wide message (default min-level: 25, cooldown-seconds: 120 between announcements, so heavy spawn activity near the edge of the map doesn't spam chat):
mob_scaling:
announcement:
enabled: true
min-level: 25
cooldown-seconds: 120
message: "&c⚠ A Level {level} {name} has emerged in {biome}!"Supports {level}, {name} (mob type, e.g. "Wither Skeleton"), and {biome} placeholders. Set enabled: false to disable entirely.
Hostile mobs never naturally spawn within spawn_protection.radius blocks (default 64) of
world spawn — keeps the immediate spawn area/starter base safe without delaying the first real
encounter with danger once you leave it. Only blocks natural spawns; passive mobs and
plugin/command-summoned entities are unaffected. See Configuration.
Scaled mobs can additionally roll 1–2 RPG-flavored modifiers on top of their numeric level —
pure variety, not a second difficulty system. Odds scale with level (mob_scaling.affixes. chance-by-level/second-affix-chance-by-level), starting at level 10 and maxing out at level
40 (50% chance of at least one, 20% chance of a second). Below level 10, mobs never roll an
affix.
| Affix | Effect |
|---|---|
| Swift | +25% movement speed (+40% at level 25+) |
| Wraith | Spawns invisible |
| Bleeding | On-hit chance to inflict a damage-over-time bleed on the player |
| Poisonous | On-hit chance to apply Poison |
| Regenerating | Passive health regeneration + bonus armor toughness |
| Chilling | On-hit chance to apply Slowness |
Since name tags are off by default, affixed mobs are signaled two
other ways: a small secondary particle marker (in addition to the level-color aura above), and
a one-time action-bar warning to the first player who gets within mob_scaling.affixes. warning-radius blocks (default 12) — a second player approaching later only sees the
particle, not a repeat warning. Configurable per-affix odds/values live under mob_scaling. affixes.roster in config.yml — see Configuration.
Every scaled mob's level is stored on the entity's PersistentDataContainer under the key rpgmood:level (an Integer), alongside the existing rpgmood_scaled scoreboard tag. Any other plugin can read it without a hard dependency on RPGMood:
NamespacedKey levelKey = new NamespacedKey("rpgmood", "level");
Integer level = entity.getPersistentDataContainer().get(levelKey, PersistentDataType.INTEGER);This is intended for exactly the kind of cross-plugin hook where a loot plugin (e.g. RPGLoot) wants to scale drop rarity by RPGMood's difficulty level — the tag is present whenever level is non-null, absent otherwise (mob wasn't scaled, or scaling is disabled).