Skip to content

Mob Scaling

okereke-dev edited this page Jul 17, 2026 · 6 revisions

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.


What triggers scaling

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 a Monster (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.


The level formula

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 — from mob_scaling.base-levels.<ENTITY_TYPE> (default 1 for 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 many step_distance blocks (default 180) the mob is from world spawn. The first step doesn't add a level (only radialLevel - 1 counts), so mobs right around spawn stay near the floor.
  • biomeBonus — from mob_scaling.biome-bonuses.<BIOME> (default 0).
  • structureBonus — from mob_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 contributing players_bonus_per_player (default 1). 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.
  • situationalBonusmob_scaling.night_bonus (default 2) if it's currently vanilla night (world time 1300023000) where the level is being computed, plus mob_scaling.thunder_bonus (default 2) 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 1 and max-level (default 40).

Structure bonus and performance

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.


Stat scaling

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 (default 0.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 (default 8), the multiplier hits exactly 1.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 roughly 1.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.


Knockback resistance & attack speed {#knockback-resistance--attack-speed}

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.

Boss hard curve {#boss-hard-curve}

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-levels entry (ELDER_GUARDIAN: 14, WITHER: 18, WARDEN: 20, ENDER_DRAGON: 22), the multiplier is exactly 1.0 — vanilla stats, never weaker.
  • Each level above that adds growth_per_level_above_base (default 0.12) to the multiplier, capped at max_multiplier (default 4.0).
  • Boss armor uses its own boss_scaling.armor_per_level (default 0.75) instead of the regular mob_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).

Smart aggro {#smart-aggro}

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-levelomniscient-min-level - 1 (default 1529) 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.

Dynamic archery {#dynamic-archery}

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 17) 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 814) 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.

Advanced AI {#advanced-ai}

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.

Seasonal combat {#seasonal-combat}

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.

Elite glow {#elite-glow}

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.

Elite equipment {#elite-equipment}

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

Elite environment effects {#elite-environment-effects}

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, default 25) — 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, default 25) — Spiders/Cave Spiders hit by a player's arrow have a chance (spider-dodge-chance-per-level per level above the minimum, capped at spider-dodge-max-chance, defaults 0.02/0.35) to cancel the hit entirely and dash sideways instead, on a 2-second per-spider cooldown.

Rewards

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.


Particle auras

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.


High-level spawn announcements

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.


Spawn protection

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.


Affixes

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.


Exposing level to other plugins

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).

Clone this wiki locally