Feature Difficulty Scaling - bh679/dungeon-train-mc GitHub Wiki

Difficulty scaling β€” mob gear & villager trades by carriage tier

Since: v0.227.0 (PR #279) Β· latest: v0.416.0 (PR #688)

v0.227.0 β€” PR #279 β€” carriage-index-based mob progression: hostile mobs spawned inside train carriages receive tiered armor / weapons / effects based on how far the run has travelled. Original 5-rung data-driven tiers.json ladder. v0.230.0 β€” PR #283, PR #284, PR #285 β€” rolled equipment gets Adventure Item Names display names + Adventure Item Stats modifiers; the fixed 5-rung ladder is replaced by the procedural tier curve (ProceduralTiers β€” smooth material bands from level 1β†’50 with infinite post-cap effect scaling); progression pauses when no player is boarded; buffs are restricted to hostile (Enemy) mobs β€” passive carriage animals keep their vanilla/variant gear. v0.255.0 β€” PR #317 β€” mob difficulty resets on player death (the tier is computed from max(travelledCarriageIndex) across online players, and each player's counter zeroes on death). v0.271.0 β€” PR #336 β€” PlayerMobs scale on the same tier (difficulty-scaled equipment, no potion effects). v0.290.0 β€” PR #358 β€” villager trade level capped to the mob weapon stage (see Villager trade cap). v0.293.0 β€” PR #364 β€” gentle first level: a one-level progression delay, plus a first-tier-band "easy mobs" swap (carriage hostiles β†’ small slimes / magma cubes) and a starter-loot downgrade (see Gentle first level). v0.296.0 β€” PR #370 β€” variant spawn-egg mobs spawn equipped: carriage block-variant spawn-egg mobs now run vanilla Mob#finalizeSpawn, so they arrive with their natural default gear β€” a pillager crossbow (with the ~6% raid-captain ominous banner), a skeleton / bogged bow β€” even at tier 0; and the difficulty pass now keeps and enchants an existing ranged weapon instead of swapping it for a melee one (see Mob gear scaling). v0.298.0 β€” PR #373 β€” piglins respect the dimension: the first-band easy-mobs swap no longer turns piglins into out-of-place magma cubes on Overworld trains. Zombified piglins are never substituted (Nether-native), and piglins / piglin brutes only become magma cubes in the Nether β€” split out into the new first_band_nether_only_mobs and first_band_no_substitute_mobs tags (see Gentle first level). v0.307.0 β€” PR #405 β€” three-stage gentle onboarding: the single easy-mobs band becomes a configurable ramp β€” the first stretch spawns no hostile mobs at all, the next stretch the existing slimes, then normal. Stage lengths are explicit carriage counts (defaults 10 + 15), decoupled from carriagesPerTier (see Gentle first level). v0.416.0 β€” PR #688 β€” gear keeps scaling past netherite: past the level-50 material cap, hostile gear no longer plateaus β€” each rolled piece gains a per-tier Adventure Item Stats primary-stat bonus (attack damage on weapons, armor on armor), so weapons and armor keep getting stronger the deeper you ride even though the material stays netherite. Reuses the same stat scaling PlayerMobs already receive; tiers ≀ 50 are unchanged. Config difficultyScaleHostileGearPastCap (default on) β€” see Post-cap gear scaling. v0.738.0 β€” PR #1203 β€” the onboarding ramp is halved: the two gentle stages drop from 10 + 15 carriages to 5 + 5, so authored hostiles begin at carriage 10 instead of 25 and the starter-loot window narrows to match (it is derived from the two stage lengths). The difficulty-level curve itself is unchanged β€” gear still switches on at level 1 (β‰ˆ carriage 40). Because both keys are held to their defaults by the config-integrity check, the change ships with a config migration (v3 β†’ v4) that rewrites only files still holding the old values, plus a version-dated grace so an install that hasn't migrated yet isn't reported as deviating (see Gentle first level).

The train gets harder the further you ride. Every hostile mob that spawns in a carriage rolls armor, a weapon, enchantments and potion effects scaled to a difficulty tier derived from how far the furthest-progressed boarded player has travelled this life. As of v0.290.0 the same tier also caps the trade level of carriage villagers, so merchant strength tracks the mobs you're fighting. As of v0.293.0 the opening band is gentled β€” the curve is delayed a level and the first tier band swaps hostiles for small slimes/magma cubes and rich chests for starter loot. All default-on and tunable from the one [difficulty] config.

Progression at a glance

Generated from the live curves by tools/build-difficulty-graphs.py in the wiki repo (re-run it whenever the curves are retuned β€” it mirrors the ProceduralTiers / DifficultyProgression formulas). Everything below keys off the difficulty level (max(0, ⌊carriages Γ· carriagesPerTierβŒ‹ βˆ’ progressionLevelDelay)), so at the defaults level L begins around carriage 20Β·(L + 1).

Difficulty progression overview β€” onboarding bands, mob armour and weapon material curves, equip rates, and the villager trade cap across difficulty levels 0–52

The whole gentle onboarding lives inside level 0 (carriages 0–9); authored hostiles only begin rolling gear at level 1 (β‰ˆ carriage 40), and gear material tops out at netherite around level 50. Past that, difficulty keeps climbing two ways: the gear keeps gaining raw stats (a per-tier attack/armor bonus past the material cap, v0.416.0 β€” see Post-cap gear scaling) and the potion-buff ladder layers on β€” nine infinite-duration buffs from level 15 to level 90:

Potion-buff ladder β€” nine infinite-duration mob buffs entering from Speed at level 15 to Slow falling at level 90, amplifiers ticking every 25 levels

Early game (levels 0–3)

The first ~100 carriages in detail: level 0 is fully vanilla, gear switches on at level 1, and only leather armour + wood weapons appear (chainmail / stone don't enter until level 5). Equip rates ramp gently β€” armour 0 β†’ 8 β†’ 16 β†’ 24 % per slot, weapons 0 β†’ 10 β†’ 20 β†’ 30 % β€” with no potion effects and villagers capped at Apprentice:

Early game, difficulty levels 0–3 β€” onboarding ribbon, equip-rate bars, and leather/wood material weight across the first 100 carriages

Difficulty tier

The tier is a pure function of the leader's progress this life:

// DifficultyProgression
rawTier = abs( max( travelledCarriageIndex over all online players ) ) / max(1, carriagesPerTier)
tier    = max(0, rawTier - progressionLevelDelay)   // effective/displayed tier (delay default 1)
  • travelledCarriageIndex is the signed net carriages a player has traversed while boarded (forward +, backward βˆ’), stored per-player in PlayerRunState and reset to 0 on death. The tier uses the max across online players, so a wipe drops difficulty back to the next live leader (or tier 0 when solo). Walking the train backward lowers the tier.
  • carriagesPerTier (default 20) sets how many carriages a tier is worth β€” tier T β‰ˆ 20Β·T carriages travelled by default. The same value drives the boarding-progress HUD tier and is centralised in DifficultyProgression.tierForTravelled (one formula shared by the mob applier, the HUD packet, and the villager cap).
  • progressionLevelDelay (default 1, since v0.293.0) subtracts from the raw tier β€” tier = max(0, rawTier βˆ’ delay) β€” so the whole curve (mob gear, effects, villager cap, HUD Diff-Level) arrives delay levels later. The raw tier is still used by the first-band features, so the easy-mob / starter-loot opening keeps its length regardless of the delay. 0 = original curve.
  • Tier 0 = vanilla baseline β€” ProceduralTiers.tierFor returns null for tier ≀ 0, so freshly-boarded mobs get no gear, effects, or enchants.

Mob gear scaling

Eligible mobs (Enemy hostiles, carrying the carriage-contents tag, not loaded from disk) roll their tier once at spawn via DifficultyApplier.apply and are stamped with a sticky dungeontrain_difficulty_applied tag so chunk reloads don't re-roll. Pre-existing NBT equipment is respected β€” only empty slots are filled. Rolled gear is AIN-named and AIS-statted, and drops at ~5%.

Default & ranged equipment (v0.296.0). Variant spawn-egg mobs run vanilla finalizeSpawn at spawn (CarriageContentsPlacer.spawnVariantMob), so they carry their natural default weapon β€” a pillager's crossbow, a skeleton's / bogged's bow β€” even at tier 0, before any difficulty roll. Because the tier pass fills only empty slots, a mob already holding a ranged weapon keeps it: the roll enchants the existing bow/crossbow (Power, Quick Charge, …) rather than overwriting it with the tier's melee weapon, so ranged mobs stay ranged as the difficulty climbs. Mobs authored with explicit NBT (anvil-renamed eggs) keep their baked equipment as before.

ProceduralTiers builds the tier from overlapping trapezoidal material curves so transitions are gradual rather than stepped. The dominant material by tier:

Mob armour and weapon material weight by difficulty level β€” leather / wood through netherite, with overlapping bands so transitions are gradual rather than stepped

Tier Armor (most likely) Weapon (most likely) Armor slot chance Weapon chance Enchant chance (max lvl) New effects
0 β€” none (vanilla) β€” 0 0 0 β€”
1–7 Leather Wood up to ~56% up to ~70% up to ~19% β€”
8–17 Leather β†’ Chainmail Stone ~64–100% 100% ~21–45% Speed @15
18–31 Chainmail β†’ Iron Iron 100% 100% ~48–83% + Strength @25
32–49 Iron β†’ Diamond Diamond 100% 100% ~85–100% + Resistance @35, Fire Res @40
50+ Diamond / Netherite Diamond / Netherite 100% 100% 100% (lvl ≀50) + Regen @50, Jump @60, Haste @70, Absorption @80, Slow Fall @90
  • Per-slot chances ramp linearly: armor slot min(1, tier/12.5), weapon min(1, tier/10), enchant min(1, tier/37.5) with table-level min(50, tier). Golden gear sprinkles in at a constant low weight at every tier as a flavour "rare drop".
  • Effects layer in at the tiers above and gain +1 amplifier every 25 tiers, so progression stays meaningful well past netherite gear.
  • Armor is skipped for blob/boss shapes (slimes, magma cubes, ghasts, phantoms, dragon, wither) β€” effects still apply.

Post-cap gear scaling (v0.416.0)

v0.416.0 (PR #688) fixes a difficulty plateau: because gear material caps at netherite and table-level enchants cap at level 50, a hostile's gear used to stop improving once the run passed difficulty level 50 (β‰ˆ 1,500 carriages at carriagesPerTier=30, β‰ˆ 1,020 at the default 20) β€” past that only potion effects scaled. Now each rolled equipment piece additionally gets a flat per-tier primary-stat bonus β€” attack damage on weapons, armor on armor β€” via Adventure Item Stats, the same mechanism PlayerMobs already use.

  • The bonus is scaled off the post-cap tier max(0, tier βˆ’ 50) (ItemStatLevelScaling.pastCapTier), so it is 0 at or below level 50 β€” tiers ≀ 50 are byte-identical to before β€” and climbs from level 51 up at +0.10 per tier (+0.15 for axes). At level 100 that's +5.0 attack / armor (+7.5 axes); the netherite material is unchanged, the numbers on it are higher.
  • Applies only to gear the difficulty pass rolls into empty slots (and rolled melee weapons); a kept default bow/crossbow or author-baked NBT gear is not stat-boosted β€” the same "empty slots only" contract as the rest of the applier.
  • PlayerMobs continue to scale off the full tier (strong from tier 1); regular hostiles use the post-cap tier. Controlled by DifficultyApplier.StatScaling (NONE / FULL / PAST_MATERIAL_CAP).
  • Toggle with difficultyScaleHostileGearPastCap (default true); set false to restore the pre-v0.416.0 behaviour where hostile gear stops improving at netherite.

Experience rewards (v0.796.0)

v0.796.0 (PR #1300) closes the other half of the plateau: gear and buffs kept climbing, but the experience a monster dropped never did, so the reward curve flattened exactly where the difficulty curve steepened β€” a fully-kitted netherite zombie with deep enchants and four infinite buffs paid the same 5 XP as a naked one on carriage one.

A monster's drop is now scored at death from what it actually carries and multiplied by 1 + Ξ£ weighted points:

Component Weight Source
Gear stat points 0.03 each Each equipped item's attack damage, or its armor if it has none (bows/crossbows count 4)
Enchant levels 0.05 each Every enchantment level summed across all six equipment slots
Effect points 0.20 each amplifier + 1 per active beneficial effect
  • The score is read off the live mob at death, not a recorded tier. That keeps it honest about the roll (an unlucky bare mob at tier 40 stays cheap), survives a respawn or progress reset, and needs nothing stored in entity NBT.
  • ItemPowerScore.baseStat reads the ATTRIBUTE_MODIFIERS component first β€” which is where AIS writes the post-cap stat bonus β€” so gear scaled past netherite pays out more without any extra plumbing.
  • Beneficial effects only. Harmful and neutral effects score nothing, so a mob the player has debuffed with Weakness never earns a bonus.
  • Absurd attribute amounts are discarded via BakedItemStats.isAbsurd; a corrupt baked-template item can't become an XP bomb.
  • Applies to any Mob anywhere β€” train, Free Play, editor β€” so a naturally-armoured vanilla mob is rewarded on the same terms. A vanilla full-iron zombie goes 5 β†’ 7 XP; a kitted tier-60 one 5 β†’ 27.
  • Capped at mobExperienceMaxMultiplier (default Γ—10, so a zombie tops out at 50 XP), which bounds the throughput of a late-train mob farm.

Unlike the gear pass this is not gated on difficultyEnabled β€” it has its own switch, mobExperienceScalingEnabled.

Villager trade cap (v0.290.0)

Carriage villagers with a profession have their trade offers re-rolled at spawn by VillagerTrainSpawnEvents on a weighted [1, 5] curve (Novice common, Master rare). As of v0.290.0 that roll is capped to the dominant mob weapon material at the current tier β€” so a Master villager (the strongest enchanted-gear / mending trades) can't appear until the mobs around them have advanced to diamond:

Mob weapon stage Villager max level Difficulty tier Carriages (default cpt=20)
none (vanilla) 1 (Novice) 0 0–19
Wood 2 (Apprentice) 1–7 20–159
Stone 3 (Journeyman) 8–17 160–359
Iron 4 (Expert) 18–31 360–639
Diamond (+ Netherite) 5 (Master) 32+ 640+
// VillagerTrainSpawnEvents
newLevel = min( pickLevel(),                                  // same weighted [1,5] roll
                min(5, ProceduralTiers.dominantWeaponStage(tier) + 1) );
  • The roll itself is unchanged β€” the cap is a ceiling, not a re-scale, so at low tiers most villagers pile up at the cap (e.g. tier 8–17 β†’ mostly Journeyman).
  • The cap is derived from the same weapon curve the mobs use (ProceduralTiers.dominantWeaponStage), so it stays paired automatically if the curve is retuned.
  • Capping a villager to level N also trims their offers to the first N trade tiers β€” low-tier merchants sell weaker goods, not just fewer.
  • Honours difficultyEnabled: with difficulty off, villagers fall back to the full random [1, 5] range.
  • Sticky like the mob path β€” a villager tagged dungeontrain_trades_rerolled keeps its level across reloads; the cap only applies to newly-spawned villagers.

Gentle first level (v0.293.0)

v0.293.0 (PR #364) softened the opening band of every run, and v0.307.0 (PR #405) expanded the mob swap into a three-stage ramp β€” no mobs β†’ slimes β†’ normal β€” so new players and fresh respawns get an even gentler landing. All default-on and tunable in the [difficulty] config:

  • One-level progression delay (progressionLevelDelay, default 1) β€” shifts the whole difficulty curve back one tier (tier = max(0, rawTier βˆ’ delay)); see Difficulty tier. Mob gear, effects, the villager cap and the HUD Diff-Level all arrive a level later. 0 restores the original curve.
  • No-hostiles stage (firstLevelNoHostiles, default true; length firstLevelNoHostilesCarriages, default 5) β€” for the first ~5 carriages of leader progress, every hostile (Enemy) authored into a carriage is suppressed entirely (it simply doesn't spawn), so the opening stretch is combat-free. Slimes count as hostiles here too, so even an authored "slimes" carriage is empty in this stage.
  • Easy-mobs (slimes) stage (firstLevelEasyMobs, default true; length firstLevelEasyMobsCarriages, default 5) β€” for the next ~5 carriages, each authored hostile is replaced at spawn with a small slime (size 1–2), or a small magma cube for nether / pillager-raider mobs. The substitute is tagged + persisted exactly like a normal carriage mob, so kill-ahead, despawn and diagnostics treat it identically. Classification is data-driven via three entity-type tags (tunable without recompiling): first_band_magma_mobs (#minecraft:raiders + the nether roster β†’ magma cube), first_band_nether_only_mobs (piglin, piglin_brute β†’ magma cube only in the Nether; spawn as authored elsewhere, since a magma cube is a Nether creature and an Overworld piglin would just zombify), and first_band_no_substitute_mobs (zombified_piglin β†’ never substituted, in any dimension β€” it's Nether-native and shouldn't be downgraded). Precedence: no-substitute β†’ nether-only β†’ magma vs slime.
  • Starter loot (firstLevelStarterLoot, default true) β€” across the whole opening window (both stages, firstLevelNoHostilesCarriages + firstLevelEasyMobsCarriages carriages), carriage chests linked to the rich loot / loot_irongold loot prefabs instead roll the starter prefab (starter-tier gear, not late-game loot). All other loot prefabs (wood, stone, mining, villager, pot, …) are unchanged. Swapped at the variant-placement seam in CarriageContentsPlacer; editor previews keep the authored loot.

After both combat stages (β‰ˆ carriage 10 by default) authored hostiles spawn normally β€” at tier 0 (vanilla gear) with the default one-level delay β€” and the difficulty curve climbs from there. The two stage lengths are explicit carriage counts (of leader progress), decoupled from carriagesPerTier; set either to 0 to drop that stage. Passive / neutral non-Enemy mobs (villagers, animals, PlayerMobs) are untouched throughout, and editor previews keep the authored mobs.

All of the above key off the raw (un-delayed) tier, so the gentle opening stays the same length regardless of progressionLevelDelay, and β€” like the rest of difficulty β€” they are leader-anchored (the furthest-progressed boarded player decides the current stage) and decided at spawn / placement time. They run independently of difficultyEnabled (each has its own toggle). The combat-stage decision is centralised in DifficultyProgression.onboardingStage (pure, unit-tested) and applied at the single hostile-spawn seam CarriageContentsPlacer.tryHandleOnboardingHostile β€” which suppresses, slime-substitutes, or spawns as authored per the current stage.

Nether ghast escalation per lap (v0.483.0)

v0.483.0 (PR #802) makes the Nether band's ghast density climb every time the train laps back around to the Nether. This is a separate axis from the carriage tier documented above: it scales spawn density, not gear, and keys off the world-gen cycle rather than travelledCarriageIndex.

Nether mobs aren't in any Overworld biome's spawn list, so the natural spawner never proposes them β€” NetherMobSpawner actively spawns the roster around players inside the band core (netherRamp β‰₯ 0.5), 4 attempts per player every 40 ticks. Before v0.483.0 ghasts were a flat 1-in-3 of those attempts in soul sand valley / basalt deltas and 1-in-6 elsewhere, with no ceiling of their own β€” they swarmed and crowded the ground rosters out of the shared 10-mob cap.

Two levers now escalate together:

Nether pass Soul sand valley / basalt deltas Other Nether biomes Ghast cap
0 (first Nether) 1-in-4 1-in-8 3
1 1-in-3 1-in-7 5
2 1-in-2 1-in-6 8
3+ 1-in-2 ramps to 1-in-2 by pass 6 15 (see limitations)
  • Odds (ghastDenomFor) step one denser per completed lap from a per-biome base, floored at 1-in-2.
  • Ghast cap (ghastCapFor) is a lookup table holding its last entry for every lap beyond it. It escalates alongside the odds deliberately: with a fixed cap the odds barely matter, since the cap fills within seconds and becomes the real ceiling β€” raising the odds alone would change respawn latency rather than felt difficulty.

The lap comes from NetherBand.netherPassIndex, the 0-based world-gen cycle repeat β€” which doubles as the Nether pass index because the Nether band is the first special band of every period, and already drives the Nether Return Again advancement. Being positional (derived from world-X rather than travel history) it needs no saved state, is reproducible across reloads, and can't be farmed by re-entering the same band.

A capped-out ghast roll falls through to the ground-mob path rather than wasting the attempt, so the band's overall mob density is unchanged at every lap β€” only the ghast share moves. The ramp math lives in two pure, param-in helpers covered by NetherGhastRampTest.

Unlike everything else on this page there is no config toggle β€” the curve is compile-time constants in NetherMobSpawner.

Configuration

The knobs live in the [difficulty] table of the server config (config/dungeontrain-server.toml, also editable from Esc β†’ Mods β†’ Dungeon Train β†’ Config while in a world β€” title-screen edits no-op because the server config isn't loaded there):

Key Default Range Effect
difficultyEnabled true bool Master switch for mob gear/effects and the villager trade cap.
carriagesPerTier 20 [1, 1000] Carriages per tier step. Set to 1 for fast-paced / testing progression (tier == carriages travelled).
progressionLevelDelay 1 [0, 100] Delay the whole difficulty curve by this many levels (tier = max(0, rawTier βˆ’ this)). 0 = original curve.
difficultyScaleHostileGearPastCap true bool Keep scaling hostile gear past the netherite / level-50 material cap via a per-tier attack/armor bonus (see Post-cap gear scaling). false = gear stops improving at netherite (pre-v0.416.0 behaviour).
mobExperienceScalingEnabled true bool Scale a monster's experience drop with its gear and beneficial effects (see Experience rewards). false = vanilla drops. Independent of difficultyEnabled.
mobExperiencePerStatPoint 0.03 [0, 5] Multiplier added per point of the mob's gear stat total. 0 disables the gear component.
mobExperiencePerEnchantLevel 0.05 [0, 5] Multiplier added per enchantment level across all equipment. 0 disables the enchant component.
mobExperiencePerEffectPoint 0.20 [0, 5] Multiplier added per level of each beneficial active effect. 0 disables the effect component.
mobExperienceMaxMultiplier 10.0 [1, 100] Hard ceiling on the experience multiplier. 1.0 caps every mob at its vanilla drop.
firstLevelNoHostiles true bool Stage 1. Suppress authored carriage hostiles entirely (they don't spawn) for the opening carriages.
firstLevelNoHostilesCarriages 5 [0, 1000] Length (carriages of leader progress) of the no-hostiles stage. 0 disables it.
firstLevelEasyMobs true bool Stage 2. Replace authored carriage hostiles with small slimes (small magma cubes for nether / raider mobs per first_band_magma_mobs; piglins / piglin brutes only in the Nether; zombified piglins never).
firstLevelEasyMobsCarriages 5 [0, 1000] Length (carriages of leader progress) of the slimes stage, following the no-hostiles stage. 0 disables it.
firstLevelStarterLoot true bool Across the whole opening window (both stages), swap rich loot / loot_irongold carriage chests for the starter loot prefab.

difficultyAffectsBabyMobs (default false) also lives here β€” when true, baby mobs receive gear/effects too. The firstLevel* swaps and progressionLevelDelay are evaluated independently of difficultyEnabled.

PlayerMobs

PlayerMobGroupSpawner routes PlayerMobs through the same tier with applyEffects=false, so they get difficulty-scaled (and AIN-named / AIS-statted) equipment but no potion buffs. Their gear stats scale off the full tier (StatScaling.FULL) from tier 1 β€” unlike regular hostiles, whose stat bonus only kicks in past the level-50 material cap (PAST_MATERIAL_CAP; see Post-cap gear scaling).

Files

  • DifficultyProgression.java β€” leader-progress β†’ tier math (maxTravelledCarriageIndex, rawTier/effectiveTier, tierForTravelled, currentTier) + onboarding gates (onboardingStage/onboardingStageFor, inOnboardingWindow, downgradeLootId/effectiveLootPrefabId)
  • ProceduralTiers.java β€” procedural material/effect curves; tierFor, dominantWeaponStage, MATERIAL_CAP_LEVEL (netherite full-weight level, the post-cap-scaling anchor)
  • ItemStatLevelScaling.java β€” per-tier AIS primary-stat bonus (bonusFor, pastCapTier, primaryStatBonus); the post-cap gear-scaling policy
  • DifficultyTier.java β€” immutable per-tier armor/weapon/effect/enchant spec
  • DifficultyApplier.java β€” rolls + applies a tier to a mob (dungeontrain_difficulty_applied sticky tag); StatScaling enum (NONE / FULL / PAST_MATERIAL_CAP) picks the stat-bonus frame
  • ItemPowerScore.java β€” the single definition of "how powerful is this item": headline combat stat + total enchant levels, shared by experience scaling and the echo encounter story
  • MobExperienceScaling.java β€” gear/effect score β†’ experience multiplier (scaledXp, beneficialEffectPoints)
  • MobExperienceEvents.java β€” LivingExperienceDropEvent hook
  • MobDifficultyEvents.java β€” hostile-only spawn hook
  • CarriageContentsPlacer.java β€” onboarding hostile gate (tryHandleOnboardingHostile: suppress / slime-substitute / spawn-as-authored by stage) + loot-prefab swap at the variant-placement seam
  • first_band_magma_mobs.json β€” entity-type tag: first-band hostiles that become magma cubes (nether roster + #minecraft:raiders)
  • first_band_nether_only_mobs.json β€” piglin / piglin_brute: magma cube only in the Nether, else spawn as authored
  • first_band_no_substitute_mobs.json β€” zombified_piglin: never substituted, in any dimension
  • VillagerTrainSpawnEvents.java β€” trade re-roll + weapon-stage level cap
  • PlayerRunState.java β€” travelledCarriageIndex per-life counter
  • NetherMobSpawner.java β€” Nether band roster + the per-lap ghast ramp (ghastDenomFor / ghastCapFor)
  • NetherBand.java β€” netherPassIndex, the positional lap counter the ghast ramp keys off
  • DungeonTrainConfig.java β€” difficultyEnabled, carriagesPerTier, progressionLevelDelay, difficultyScaleHostileGearPastCap, firstLevelNoHostiles, firstLevelNoHostilesCarriages, firstLevelEasyMobs, firstLevelEasyMobsCarriages, firstLevelStarterLoot, mobExperienceScalingEnabled, mobExperiencePerStatPoint, mobExperiencePerEnchantLevel, mobExperiencePerEffectPoint, mobExperienceMaxMultiplier
  • tools/build-difficulty-graphs.py (wiki repo) β€” regenerates the progression graphs above (matplotlib; mirrors the ProceduralTiers / DifficultyProgression formulas)

Known limitations

  • Hostile-only effects. Only Enemy mobs get the full gear+effects pass; passive carriage animals are untouched (villagers are handled solely by the trade cap).
  • Leader-anchored tier. Difficulty follows the single furthest-progressed boarded player, not a per-mob or per-region value β€” in multiplayer everyone fights the leader's tier.
  • Experience is scored, gear is not. The experience multiplier reads the mob's live equipment, so it reflects a mob geared at any tier β€” but it can't distinguish gear the difficulty pass rolled from gear an author baked into a carriage variant, or from a player-thrown item the mob picked up. A mob handed a netherite sword drops more XP for it.
  • Sticky rolls. Tier is locked at spawn; a mob/villager that spawned at tier 2 stays tier-2 even if the run later climbs. Only newly-spawned entities reflect a higher tier.
  • Villager floor of 1. The cap never drops a villager below Novice, so profession villagers always trade something even at tier 0.
  • Ghast escalation plateaus at pass 3. NetherMobSpawner.NEARBY_CAP (10) bounds all band mobs near a player and is checked before any ghast logic, so ghast-cap entries above 10 never bind β€” passes 3 and 4 behave identically, and the table's 15s are the intended ceiling should that cap ever rise. At pass 2 (cap 8) ghasts may already occupy 8 of the 10 slots, leaving the ground rosters thin.