Packs and migration
Loose zombies clump into packs, packs walk the world on their own errand, and a pack
nobody is looking at is dematerialised until someone comes near. None of it is aimed at a player: a pack
picks where to go from its own heading and a seeded random walk, never from where anyone is standing. The
rule is enforced by a test, PackNoPlayerAccessTest, which fails the build if
anything under pack/ so much as reaches for a player position.
The consequence is the one worth knowing before reading the numbers: a pack that arrives at your base did not come for you. It was passing.
Forming, holding, breaking
A lone zombie only considers a pack when it has at least
packMinNeighbours neighbour, and it takes that decision one activation out of
packDecisionDivisor, inspecting at most packScanCap
neighbours. Those three numbers are the whole cost of the system in a dense horde.
A brand-new pack needs 3 zombies in the neighbourhood to appear out of nothing. Once it exists a zombie joins it whenever the pack centroid is within 24 blocks, up to a hard 24 members.
Leaving is deliberately harder than joining. A member has to drift past
40 blocks from the centroid, well beyond the 24 it joined at, and stay out there for
packStrayActivations activations in a row before it actually leaves. Without that
hysteresis a zombie walking around a wall would lose its pack. Below 2 members a pack
dissolves, but only after 200 ticks of grace.
Merging
Two centroids closer than 32 blocks are merge candidates, and they only
fuse if their headings agree: the dot product of the two directions has to reach
packMergeHeadingDot. Without that second test, packs crossing in opposite
directions would still fuse, and the whole population of the world collapses into one mass.
Migration
A pack picks a destination between 96 and 384 blocks away, turning at most 45 degrees from its current heading. Bounding the turn is what makes the path read as a migration; unbounded it reads as brownian motion. It counts as arrived within 8 blocks, then pauses 200 ticks plus a jitter of up to 100 so packs do not all move in lockstep.
Off view the pack is simulated coarsely at packVirtualSpeed,
about 40 percent of a zombie's walking speed, which pays back the terrain detours a straight line never
takes. Members are aimed at a waypoint 24 blocks ahead of the centroid rather than at the
destination itself: the level-of-detail system classifies on the distance to the point being walked to, so
a destination hundreds of blocks away would freeze the very zombies it was meant to move.
When there is simply no path, packStuckActivations activations
without headway make the pack give up and pick somewhere else. And by default packs travel by night:
packMigrateAtDay is off, so the mod's own day-sleep wins and a pack does not burn
under the sun below the immunity phase.
Out of sight, dematerialised
A pack nobody can see stops existing as entities and keeps existing as a pack. Every 20 ticks a sweep looks at each pack, not at each zombie, and a pack found not entity-ticking for 40 consecutive sweeps is dematerialised. That is a grace period rather than a timer on purpose: chunk unloading has been measured in this project at 2, 35, 272 and once over 1200 ticks, so a single observation proves nothing.
On approach the members come back scattered over 6 blocks so the pack does not reappear in one column, and a returning member re-joins its pack from up to 64 blocks away. A ghost that fails to restore 5 times is given up on.
Its own switch. packVirtualEnabled is kept separate from
packMigrationEnabled because nothing else in the system creates and destroys
entities. An operator who suspects it has to be able to stop it alone, without also stopping migration.
Off simply leaves packs fully materialised, which costs more.
What survives a restart
Packs are written to the world save, both when it closes and at every autosave, so a crash no longer loses them. Membership, centroid, heading and destination come back as they were.
Configuration
| Field | Description | Default |
|---|---|---|
| packEnabled | Master switch. Off, no pack is ever formed and the per-zombie decision returns immediately. | true |
| packCohesionRadius / packBreakRadius | Join distance to the centroid, and the far larger distance a member has to pass before it starts drifting away. | 24.0 / 40.0 |
| packFormMinSize / packMinSize / packMaxSize | Zombies needed to form a pack out of nothing, membership below which it dissolves, hard cap on members. | 3 / 2 / 24 |
| packMinNeighbours / packDecisionDivisor / packScanCap | Neighbours before a lone zombie even considers a pack, one decision out of this many activations, neighbours inspected per decision. | 1 / 8 / 16 |
| packStrayActivations / packDissolveGraceTicks | Consecutive out-of-range activations before a member leaves, and how long a pack may stay undersized. | 3 / 200 |
| packsPerTick | Packs visited per server tick, round-robin: centroid, merge probe, dissolve check. | 4 |
| packMergeRadius / packMergeHeadingDot | Distance between two centroids to be merge candidates, and how much their headings must agree. | 32.0 / 0.5 |
| packMigrationEnabled | Off, packs form and stay cohesive but never pick a destination. | true |
| packLegMin / packLegMax / packTurnDegrees | Shortest and longest leg of the random walk, and the maximum heading change per leg. | 96 / 384 / 45.0 |
| packVirtualSpeed / packMarchLead | Blocks per tick off view, and how far ahead of the centroid the shared waypoint is planted. | 0.09 / 24.0 |
| packArriveDistance / packDwellTicks / packDwellJitterTicks | How close counts as arrived, the pause there, and the spread applied to that pause. | 8.0 / 200 / 100 |
| packStuckActivations / packMigrateAtDay | Activations without headway before a new destination is picked, and whether packs keep migrating in daylight. | 8 / false |
| packVirtualEnabled / packMaterializeInterval / packDematGraceTicks | Dematerialise packs nobody can see, ticks between two sweeps, sweeps a pack must be unseen before it goes. | true / 20 / 40 |
| packSpawnSpread / packMaterializeRetries / packRejoinRadius | Radius the restored members are scattered over, failed restores before a ghost is given up on, distance a returning member may re-join from. | 6 / 5 / 64.0 |