Skip to the text

The row first drawn for a zombie can differ from the row where it becomes visible. An ordinary action creates its batch, applies spacing, emits the base placement event, and then lets subclass placement continue. Row history records the earlier draw; later movement does not rewrite it.

A batch becomes a spatial queue

The ordinary jittered action creates its instructions in order, then performs a spacing pass over the created entities. For common right-side entry on the five-row Board,

X=820+xstage+HitRect.mWidth⁡+80k+J,Y=222+76r,J∈{0,…,29}.\begin{aligned} X&=820+x_{\mathrm{stage}}+\operatorname{HitRect.mWidth}\\ &\quad+80k+J,\\ Y&=222+76r,\qquad J\in\{0,\ldots,29\}. \end{aligned}

Here rr is the zero-based row, xstagex_{\mathrm{stage}} is the stage’s entry offset, and kk counts earlier entities assigned to that row in the same action. Each action starts fresh counters, so the separate flag action does not continue the ordinary roster’s spacing count.

This creates horizontal distance rather than releasing one instruction on a fixed timer. Width can even reverse the expected spatial order. With stage offset zero, a first air_missile_launcher of width 270 starts at X 1090–1119; a following skycity of width 42 in that row starts at X 942–971 after its 80-unit increment. The later instruction is closer to the lawn before any movement.11Creation, width, and per-action spacingThe ordinary jittered action creates its entity batch, then applies per-row spacing. Its empty custom-offset vector selects the default increment of 80 plus an integer draw in 0–29. Initial right-entry X adds HitRect.mWidth to the stage's result. Fifteen standard stages use the common entry method; Dark conditionally selects a qualifying gravestone shortcut and otherwise falls back to that method.0x1014c40c00x1014c47e80x1014c544c0x1023aa5b8

Dark can instead use a qualifying gravestone shortcut’s X minus 50 as the stage entry, then add width and jitter. The ordinary declaration starts with no gravestones and leaves its declared gravestone designers inactive; the conditional shortcut must actually exist for that branch to apply. All coordinates here are game coordinates.

Eligibility supplies the base rows

Instruction Row = 5 requests the random selector; Row = 6 reuses the preceding row. Fixed row values 0–4 take another branch. The following gates belong to random selection, not every possible later placement:

  1. The row is within Board.rowCount and enabled.
  2. The anchor satisfies GridExtents.mY - 1 <= row.
  3. The stage’s gate accepts that row and exact zombie type.

The property constructor defaults GridExtents to (1, 1). Larger Y extents restrict the anchor row; they do not directly define a hitbox or damage coverage.

Context / typePlayer-facing rows with positive eligibility
Pirate basic walkers2, 4
Pirate seagull, swashbuckler1, 3, 5
Pirate cannon1–5
Future disco_mech2–4
Sky City invisible_plane, extent Y = 33–5
Sky City skycity_twinsplane, extent Y = 22–5

Pirate’s ordinary planks are declared at zero-based rows [1, 3]. Its stage gate checks names, so these rules cannot be generalized to every apparent flying type. Future excludes disco_mech from the outer rows. The other standard stage gates accept all rows at this stage, while the common Board and extent gates still apply.22Eligibility gates and the zero endpointThe common predicate checks row count, enabled state, GridExtents.mY, and the stage gate. Pirate uses exact type-name tests; Future restricts disco_mech at the outer rows. Zero-weight records remain in the sampler. Its inclusive cumulative comparison can select the first zero-weight record at an exact-zero draw, as confirmed by a controlled native helper check.0x101595f3c0x1014fd55c0x1015110100x10156ac7c

Base weights, then choice history

For a row’s saved timestamp trt_r, let d=t−trd=t-t_r. Its base weight is

ur={0,ineligible,0.01,0<d<5,d/45,5≤d<45,1,d≤0  ∨  d≥45.u_r=\begin{cases} 0,&\text{ineligible},\\ 0.01,&0<d<5,\\ d/45,&5\le d<45,\\ 1,&d\le0\;\lor\;d\ge45. \end{cases}

The timestamp begins at FLT_MAX, so a fresh Board takes the weight-1 branch for eligible rows. The traced mower write requires WaveGeneratorModule; ordinary Endless lists WaveManagerModuleProperties. This timestamp is not reset by each ordinary spawn.

Normalize pr=ur/∑jujp_r=u_r/\sum_j u_j. Let ar,bra_r,b_r be the counters since the latest and previous choices. For pr≥10−6p_r\ge10^{-6}, the history adjustment is algebraically

fr=pr(1.5ar+0.25br+1.75)−1,p~r=prclamp⁡(fr,  0.01,  100).\begin{aligned} f_r&=p_r(1.5a_r+0.25b_r+1.75)-1,\\ \widetilde p_r&=p_r\operatorname{clamp}(f_r,\;0.01,\;100). \end{aligned}

Smaller prp_r gives zero adjusted weight. The next draw’s nominal share is p~r/∑jp~j\widetilde p_r/\sum_j\widetilde p_j. Both counters start at zero. After choosing a row, the update is:

  • Increment both counters for every row with positive base weight.
  • For the chosen row, copy its incremented ara_r into brb_r, then set ar=0a_r=0.

Thus an eligible unchosen row changes from (a,b)(a,b) to (a+1,b+1)(a+1,b+1), while the chosen row changes to (0,a+1)(0,a+1). The counters advance by selections, not seconds.33History counters, not a spawn timerThe Board constructor starts both historical counters at zero. The sampler adjusts normalized base weights using float32 operations and mutates the eligible row histories after selection. The article gives its algebraic form and rounded nominal shares. The separate timestamp starts at FLT_MAX; the traced mower write requires WaveGeneratorModule, while standard ordinary Endless uses WaveManagerModuleProperties.0x10156ab200x10156aba00x10157f4180x1016246ac

With five equally eligible rows, the first two draws have equal row weights. Before the third draw, two different previous rows have shares 5.88% each while untouched rows have 29.41% each; two repeated choices give that row 4.76% and each other row 23.81%. The view lets you reconstruct these states and compare terrain restrictions.

  1. Row 1a 2 · b 229.41%
  2. Row 2a 1 · b 25.88%
  3. Row 3a 2 · b 229.41%
  4. Row 4a 0 · b 25.88%
  5. Row 5a 2 · b 229.41%
Plate 5.1Egypt: basic zombie. Each lane’s bar is that row’s share of the next draw after 2 choices, the draws numbered on the lanes they chose; a and b count the selections since the row’s latest and previous choices. A row the type may not enter weighs nothing.Next draw: 29.41% · 5.88% · 29.41% · 5.88% · 29.41%

Zero weight does not remove a record. The sampler uses draw <= cumulative weight, so an exact-zero draw can accept its first record even when that record is ineligible.2

The base event runs before the subclass finishes

By the time jitter is applied, the whole batch is already in the Board zombie list. Setting an entity’s position is followed by its placement hook. The base hook synchronously emits OnZombieAddedToBoard; subclass code can continue afterward.

An active Animnuts Ghost Horse circle can move an eligible newcomer to (744,374,0)(744,374,0), player column 9 and row 3. It reacts to newly added events, not by repeatedly pulling every existing zombie in a radius. Its destination is fixed rather than taken from the horse’s position.

The callback admits runtime small/mid sizes and has state, flag, Zomboss, elite, and elite-immunity exclusions. Runtime size is the relevant value, not apparent sprite size. The ordinary leader condition is distinct from elite status and is not directly tested by this callback.44Ghost Horse's added-to-Board eventThe base placement hook synchronously emits OnZombieAddedToBoard. An active circle accepts eligible runtime small/mid entities, subject to flag, state, and elite checks, and writes its fixed destination. It neither chooses a fresh weighted row nor rewrites row history. The localization identifies animnuts as 球果训练家 and the horse as 幽林马. King/Fisherman subclass code continues after the base event.0x100ccab7c0x100cd9ea00x1019958d4

King and Fisherman then perform their own same-class occupancy scans. They see other members of the newly created batch at their current positions, including positions not yet finalized.

NewcomerSame-class rows removed from candidatesConsequence of self handling
dark_kingAll matching Board-list objects, including itselfIts own valid current row is removed
beach_fishermanOther matching objects, explicitly excluding itselfKeep its current row when no other matching object occupies it

If the current row is unavailable, the helper shuffles the remaining candidates and chooses one. Successful placement uses the last column, X 744. The scan has no living-only filter: a dead or removal-pending object can occupy a row until Board-list erasure.

When no row remains, the newcomer is rejected. The normal King path enters ZS_Die; Fisherman dispatches removal immediately. Neither selects an older victim, creates a retry, nor refunds the generated instruction. Both still reach ordinary loot checks, and their rejection occurs before the initial wave-health snapshot.55Two related row scans with different self handlingBoth hooks scan same-class objects already in the Board zombie list, including the newly created batch. Fisherman excludes the current object; King does not. Neither scan filters only living objects. With no row, normal King handling enters ZS_Die, while Fisherman's flagged branch requests removal immediately. Both reach base loot handling, and neither retries or refunds the spawn instruction.0x10238803c0x10151e4ec0x1023893000x101520cac

The circle has one shared lifetime

The Board keeps a shared circle, rather than one circle per horse. First creation fixes the destination and whether the costume auxiliary effect exists. With that effect, accepted newcomers also receive a stun request and have their references retained; this does not bypass the normal condition-immunity machinery.

The supplied sequence is a start animation, a 15-second loop, and an end animation. A repeat activation restarts the loop with a fresh 15 seconds; it does not add time to the old remainder. Reuse preserves the captured references and does not change the first creation’s costume effect.

Cleanup clears the tracked stun condition, but stores no original coordinates and restores no positions. It also does not move a captured zombie back to the circle continuously. In particular, a later successful King/Fisherman placement can supersede the circle’s position while the original sampled row remains in the history records.66One shared circle, refreshed rather than stackedThe first creation fixes the circle destination and costume auxiliary effect. A later activation reuses the shared circle and restarts its 15-second loop, preserving captured references. Cleanup clears the tracked stun condition but stores no original positions to restore. Start and end animations are separate from the timed loop.0x100ccab7c0x100ccb0a40x100cda0cc

Notes

The evidence behind each claim: the code path, and the native addresses where it was traced.

  1. 1

    Creation, width, and per-action spacing

    The ordinary jittered action creates its entity batch, then applies per-row spacing. Its empty custom-offset vector selects the default increment of 80 plus an integer draw in 0–29. Initial right-entry X adds HitRect.mWidth to the stage's result. Fifteen standard stages use the common entry method; Dark conditionally selects a qualifying gravestone shortcut and otherwise falls back to that method.

    0x1014c40c00x1014c47e80x1014c544c0x1023aa5b8
  2. 2

    Eligibility gates and the zero endpoint

    The common predicate checks row count, enabled state, GridExtents.mY, and the stage gate. Pirate uses exact type-name tests; Future restricts disco_mech at the outer rows. Zero-weight records remain in the sampler. Its inclusive cumulative comparison can select the first zero-weight record at an exact-zero draw, as confirmed by a controlled native helper check.

    0x101595f3c0x1014fd55c0x1015110100x10156ac7c
  3. 3

    History counters, not a spawn timer

    The Board constructor starts both historical counters at zero. The sampler adjusts normalized base weights using float32 operations and mutates the eligible row histories after selection. The article gives its algebraic form and rounded nominal shares. The separate timestamp starts at FLT_MAX; the traced mower write requires WaveGeneratorModule, while standard ordinary Endless uses WaveManagerModuleProperties.

    0x10156ab200x10156aba00x10157f4180x1016246ac
  4. 4

    Ghost Horse's added-to-Board event

    The base placement hook synchronously emits OnZombieAddedToBoard. An active circle accepts eligible runtime small/mid entities, subject to flag, state, and elite checks, and writes its fixed destination. It neither chooses a fresh weighted row nor rewrites row history. The localization identifies animnuts as 球果训练家 and the horse as 幽林马. King/Fisherman subclass code continues after the base event.

    0x100ccab7c0x100cd9ea00x1019958d4
  5. 5

    Two related row scans with different self handling

    Both hooks scan same-class objects already in the Board zombie list, including the newly created batch. Fisherman excludes the current object; King does not. Neither scan filters only living objects. With no row, normal King handling enters ZS_Die, while Fisherman's flagged branch requests removal immediately. Both reach base loot handling, and neither retries or refunds the spawn instruction.

    0x10238803c0x10151e4ec0x1023893000x101520cac
  6. 6

    One shared circle, refreshed rather than stacked

    The first creation fixes the circle destination and costume auxiliary effect. A later activation reuses the shared circle and restarts its 15-second loop, preserving captured references. Cleanup clears the tracked stun condition but stores no original positions to restore. Start and end animations are separate from the timed loop.

    0x100ccab7c0x100ccb0a40x100cda0cc