Modscraper Modscraper Minecraft
Ashen Legion logo

Ashen Legion

Mod

by cleannrooster

Legion/Horde encounters, Data Driven!

Type

Mod

CurseForge Downloads

846

CurseForge ID

1501646

Last Updated

Apr 12, 2026

Description

 

description_c40b86f1-59f8-4385-af23-3bc1ba4031dd.png

 

Ashen Legion

 

Mod Concept and Summary

 

Ashen Legion is a combination API + content mod that adds the titular Ashen Legion encounter, as well as zombie encounters on other situations instead. You can define your own data packs to add your own encounters of mobs to spawn en masse during a night, so that players may either stumble upon them, or they find the player themselves. 

If you are an operator, you can use /legion spawn {encounter-name} to spawn your desired encounter. Encounters spawn up to three nodes of mobs near your location, all of which belong to the same legion. If they are programmed to do special behavior using the API (as the titular Ashen Legion is), they will behave with custom behavior depending on the state of the legion. Otherwise, they will behave as normal, but will generally try to move towards threats. 

 

Technical Details

 

Core Legion Group AI

 

The heart of the mod is a group-level state machine that runs every 10 ticks, coordinating every mob in a legion as a single tactical unit rather than a collection of individuals. The legion aggregates threat from all of its members and transitions through five states:

  • WANDER — mobs drift aimlessly using a smooth directional wander goal that slowly rotates heading over time rather than snapping between random waypoints
  • ALERT — a threat source has been noticed; mobs stalk slowly toward the target and hold at distance, sizing up the situation
  • APPROACH — the target is confirmed; the group sprints toward it
  • ENGAGE — the target is within engagement range; melee mobs attack, ranged mobs hold position and fire
  • REGROUP — casualty losses have exceeded a threshold; mobs pull back and consolidate before re-engaging

State transitions use hysteresis timers (~3 seconds) on all downward transitions so brief threat fluctuations do not cause jitter between states. The APPROACH→ENGAGE transition only requires the target to be within range — it does not impose a secondary threat gate — so mobs don't stall mid-chase waiting for a score they can't reach through proximity alone.

Threat System

 

Each legion member maintains its own local threat table that maps entity UUIDs to float scores. The legion aggregates these per-member tables every update cycle using diminishing returns (each additional member contributes 90% of the previous one's weight) to prevent a pile-on from inflating threat beyond what a single target realistically poses.

Threat events that accumulate score:

Event Base score Damage taken 15× damage amount Ally killed 200 flat Proximity tick Up to 0.8 per cycle, distance-scaled Ranged attack detected 30 Flanking detected 40 Healing observed 25

Each EngageBehavior role modifies these weights — BRACE mobs weigh proximity 2×, RALLY mobs weigh ally deaths 2×, ATTACK_RANGED mobs weigh being approached 1.4×, and so on.

Decay runs every 10 ticks after 5 seconds of no direct hits, but is distance-aware:

  • Entity ≤ 24 blocks away: no decay — still an active threat
  • Entity 24–48 blocks away: 0.92× per cycle
  • Entity beyond follow range or not in world: 0.80× per cycle (clears quickly)

Dead and despawned entity UUIDs are pruned unconditionally every 10 ticks regardless of goal state, preventing ghost scores from stranding the group in APPROACH with no reachable target.

Universal MobEntity Compatibility

 

Any vanilla or modded mob can be inducted into a legion without code changes. A mixin on MobEntity injects a LegionMobData composition object into every mob in the game and makes them implement LegionMob. A second mixin on LivingEntity hooks onDeath and damage to automatically broadcast AllyKilled and DamageDealt threat events to the rest of the group.

Each mob also participates in:

  • Stale member sweeps — the legion prunes dead or discarded members at the top of every update cycle, keeping casualty ratios accurate even when mobs are removed by external means
  • NBT persistence — each mob saves its legion UUID on world save and reconnects to its legion on chunk load via LegionManager.getOrRestore()
  • Goal switching — when the legion transitions state, each member's goal selector is updated via onLegionGoalChanged so AI behaviour matches the group intent

Custom Legion Entities

Two fully-featured entity implementations ship with the mod, both built on direct LegionMob ownership rather than the mixin path, giving them compile-time interface satisfaction and full control over per-state behaviour:

LegionMeleeEntity

 

A melee fighter with distinct AI per state:

State Behaviour WANDER LegionWanderGoal — smooth directional drift, far-ranging ALERT Slow cautious stalk toward target, stops at 8 blocks APPROACH Full-speed sprint, closes to melee range ENGAGE Vanilla MeleeAttackGoal REGROUP Two-phase: flees directly away if threat is within 8 blocks (FLEE animation), then navigates toward the group centre (REGROUP_WALK animation). Max 8-second duration.

LegionBowEntity

 

A ranged archer with bow-specific AI:

State Behaviour WANDER Same smooth drift as melee ALERT Approaches to 18-block stand-off, faster than melee APPROACH Sprints to 10-block effective firing range ENGAGE Holds position, shoots every 1.5 seconds with staggered fire timers; strafes sideways (with slight backward bias, switching direction randomly) if target closes within 8 blocks REGROUP Flees directly away from the primary target only — no regrouping with team. Max 8-second duration.

Both entities broadcast AllyKilled and DamageDealt events to their legion on death and when hit, and sync a DataTracker animation byte to all tracking clients so the renderer can select the correct animation clip per state: IDLE, WALK, ALERT_WALK, RUN, ATTACK, BOW_DRAW, REGROUP_WALK, FLEE.

Regroup & Casualty System

 

When a legion's casualty ratio reaches 60% of its original size, it automatically triggers a REGROUP. The regroup ends when either 80% of surviving members have moved within 8 blocks of the group centre, or an 8-second timeout elapses. On exit, originalMobCount is reset to the current survivor count so the same losses cannot immediately re-trigger another regroup cycle.

Straggler Despawn

 

When a legion is reduced to 2 or fewer members and those members have had no direct combat interaction for 20 seconds, they are silently removed from the world with an end-portal particle burst and an Enderman teleport sound — giving the impression they've slipped away rather than just vanishing.

Data-Driven Encounter Registration

 

Encounters can be defined entirely in JSON at data/<namespace>/legion_encounter/<name>.json:

{ "mobs": [ { "type": "minecraft:zombie", "weight": 3 }, { "type": "minecraft:husk", "weight": 1 } ], "min_size": 6, "max_size": 10, "weight": 2 }
  • mobs — weighted pool; a mob type is rolled proportionally at each spawn slot
  • min_size / max_size — group size is rolled randomly in this range at spawn time
  • weight — relative probability for nightly encounter selection; a weight-3 encounter appears three times as often as a weight-1 encounter

Datapack encounters are cleared and rebuilt on every /reload. If a datapack entry shares an ID with a built-in encounter, the datapack wins. Mod code can also register encounters programmatically via LegionSpawner.register(encounter) — these survive reloads permanently.

Nightly Spawning System

 

Every night (at approximately tick 13000), the spawner schedules one encounter per roughly every two online players (1 player → 1 encounter, 3 players → 2, 5 players → 3, etc.). Encounters are staggered to random points across the 10,000-tick night window so they don't all land at dusk. Each pending spawn is tagged with its world's RegistryKey so cross-dimensional contamination is impossible.

Difficulty scaling:

  • Peaceful: no spawns
  • Easy: spawn count halved (minimum 1)
  • Normal/Hard: full rate

Spawn Safeguards

 

Every spawn attempt checks all of the following before placing any mobs:

Safeguard Threshold Global mob cap No spawn if 120+ legion members already exist Villager proximity Cluster anchors within 16 blocks of a villager are rejected Legion proximity Cluster anchors within 64 blocks of another active legion's centre are rejected Unloaded chunks Anchor candidates in unloaded chunks are skipped (heightmap queries on unloaded chunks return garbage) Queue cap At most 8 pending spawns queued at any time; excess are discarded Solid ground Each mob position requires solid non-fluid ground below and two-block air clearance above

Mobs are distributed across 3–4 tight clusters positioned 24–80 blocks from a randomly chosen anchor player, using uniform disc sampling (radius × √rand) so clusters are truly tight rather than ring-shaped.

Persistence

 

LegionManager extends Minecraft's PersistentState and serialises to the world's data directory. Each legion saves its UUID, current goal state, original/current member counts, and primary target. Individual member entities save their legion UUID in their own NBT. On chunk load, members reconnect to their legion via getOrRestore(), which creates a shell legion if the world file was loaded before the entity NBT was processed, then fills it in as members arrive.

Op Command

 

/legion spawn <id> /legion spawn <id> <min> <max>  

Requires operator permission level 2. Spawns the named encounter centred on the executor's position with an optional custom size range. Tab-completes all registered encounter IDs (datapack entries listed first). On success, broadcasts a count of placed mobs to all operators.

Client-Side Rendering

 

The renderer (AzLegionRenderer) uses AzureLib and selects both model and texture per entity instance:

  • Every other entity (by network ID parity: id % 4 >= 2) uses a _rust texture variant, giving visual variety within a group without requiring separate entity types
  • Headless model variant (_headless.geo.json) is similarly distributed by ID parity
  • The render layer is resolved per entity from its resolved texture identifier so variant textures bind to the correct GPU draw call rather than always falling back to the base texture

Screenshots

Similar Mods

Included in Modpacks

External Resources