promotional bannermobile promotional banner

Ore Lizards

Ore Lizards: a rare cave mob hidden invisibly in stone/deepslate. It erupts and flees when you approach, then burrows away for good after 13s. Kill it with a pickaxe first for its secret ore drop (coal to diamond/emerald).
Back to Files

orelizards-1.3.0+mc1.21.11.jar

File nameorelizards-1.3.0+mc1.21.11.jar
Uploader
birdsprimebirdsprime
Uploaded
Sep 5, 2026
Downloads
8
Size
56.4 KB
Mod Loaders
Fabric
File ID
8810439
Type
R
Release
Supported game versions
  • 1.21.11

Curse Maven Snippet

Fabric

modImplementation "curse.maven:ore-lizards-1679787:8810439"

Learn more about Curse Maven

What's new

## 1.3.0

Ore Lizards are now placed for you rather than left to chance. Everything below follows from one
measurement: vanilla spawning was working correctly and players still never met the mob.

### Added

- **An encounter director** (`EncounterDirector`), server-side, that tracks how long each player
  spends genuinely exploring underground and, on a randomised 20-60 minute budget, places one
  dormant lizard ahead on their path, in the cave they are standing in, so they walk into it.

  The motivation was measured, not guessed. A headless run against real worldgen produced **43 valid
  lizard placements per 400,000 simulated attempts**, present in the plains, dripstone-cave and
  lush-cave spawn lists - which is exactly what a weight of 1 in a rare category is meant to look
  like. The registration, the placement predicate and the biome entries were all correct. Spawning
  worked; discovery did not, for four reasons that compound. `MobCategory.AMBIENT` allots roughly
  **15 spawn slots across the ~289 loaded chunks** around a player and shares them with bats. A
  dormant lizard is invisible, silent and emits no particles, so it has no discovery affordance
  beyond being stood next to. The wake radius is 5 blocks. And worldgen puts some lizards inside
  sealed pockets of stone, where they are unreachable forever while still holding a cap slot. No
  weight fixes any of that, because a spawn weight cannot express "somewhere the player will
  actually walk".

  The cadence is deliberately wide rather than tight. A narrow band produces a rhythm players
  pattern-match, and the moment somebody works out the interval, every encounter they have already
  had retroactively reads as scripted; a 3x spread cannot be felt as a schedule. The first budget of
  each server run is seeded with a uniform 0-20 minute head start, so a short session is not a
  guaranteed miss - expected first encounter is around 30 minutes of underground time rather than
  40, while the long-run rate is unchanged. Underground time only accrues while the player is
  actually moving (0.5 blocks/s, so sneaking counts and AFK does not) and actually below ground, so
  the budget measures exploring rather than wall-clock time.

  Placement is a **bounded flood fill** from the player's feet: a breadth-first search through
  passable blocks (air, or anything with no collision shape and no fluid), six-connected, over a
  65x33x65 grid, capped at 2048 admitted nodes and a depth of 32. A node the fill reaches is in the
  same uninterrupted air volume the player is standing in, and its depth is the walking distance to
  it. Nodes 16-32 deep are candidates if they pass the site checks, cheapest first - not within 12
  blocks of anywhere the player has recently stood, chunk inhabited time under ten minutes, a
  passable dry block overhead, a standable floor, and the placement rule itself - and nodes outside
  the band are never checked. The fill runs to the node cap, the depth limit or the end of the reachable
  volume - never stopping early on a candidate count, because breadth-first order finds sites in
  nondecreasing depth and the first handful would all sit on the 16-block minimum - and every
  qualifying site is scored the moment it is found
  on how well they line up with the player's smoothed heading, how close their depth is to 24, how
  far they are vertically, and whether they sit 2-4 blocks *off* the path line; the best wins. That
  lateral offset is not decoration: the trigger range is still 5 blocks, so a head-on placement
  means a sprinting player is on top of the lizard before the 20-tick `appear` animation has
  finished. The eruption has to read as something coming at them from the side. Water is impassable
  for the fill on purpose: a site beyond a flooded stretch is a swim, not a walk-in, and refusing
  to fill through water also keeps the fill out of cave lakes that would otherwise eat the node
  budget without yielding a dry floor.

  **Two sight-based rules came before this, and both tested the wrong property.** The design said
  "never in the player's line of sight", on the theory that the player should walk into the
  encounter rather than watch it appear on screen. The first playtest placed two lizards, and the
  player saw neither: both landed hidden from view, both were underwater - one under three blocks of
  it - and both drowned within seconds of being placed. Underground, "16-32 blocks away and out of
  view" is very often exactly "in a different cave pocket behind a wall", somewhere the player will
  never walk, and a flooded pocket is as hidden as any. So the rule was inverted: a candidate needed
  a *clear* ray from the player's eye, on the reasoning that open air between the two proves the same
  cave. The next playtest showed why that is just as wrong. The log has the ray blocked within 2-4
  blocks of the eye on every candidate, sweep after sweep - a 16-32 block straight line essentially
  never exists in a winding cave, so a perfectly walkable passage twenty blocks along was rejected
  forever and nothing was placed at all. Occlusion and visibility are both answers to "what can the
  player see", and the encounter does not care. It cares whether the player can *get there* without
  going through a wall or a flooded stretch, which is connectivity, which is what a flood fill
  through air measures directly - and it excludes the pocket that is near through a wall, because
  the fill has to go round. The sight test, its `ClipContext`, and the `skipSightCheck` debug
  property that existed to bypass it are gone. Concealment was never buying anything either way,
  because `finalizeSpawn` runs `setInvisible(true)` before `addFreshEntity`, so no client is ever
  sent a visible frame wherever the lizard goes.

  The fill's cost is bounded by construction and by arithmetic, and the header comment states both
  so nobody optimises them later. Nothing is allocated per node: the visited set is a static bitset
  over the grid (2,179 longs, cleared with `Arrays.fill`), the queue is a static `int[]` of
  bit-packed coordinates and depth, and one static `MutableBlockPos` serves every read. Visited
  marks *examined* cells, walls included, so every block in the grid is read at most once. Chunk
  loading is checked once per chunk column - a lazily filled 5x5 cache of `hasChunksAt` verdicts,
  which is exact for a 32-block radius - and an unloaded column is treated as solid, so every block
  read the fill or its site checks make is inside a column that passed. A capped fill in open cave
  examines at most 1 + 6 x 2048 cells plus the in-band probes - twelve to fifteen thousand block
  reads on already-loaded chunks, about a millisecond at the very worst and usually a small
  fraction of that, since a passage is not a volume and a corridor is exhausted long before the cap - and it can
  run at most once per tick server-wide, once per ten seconds per armed player, with a player
  armed once per 20-60 minutes. Under `orelizards.director.debug` every fill logs its nodes
  expanded, candidates found, whether the cap was hit and its elapsed microseconds, so the cost is
  measured rather than assumed. `NEARBY_LIZARD_SCAN_RADIUS` drops from 64 to 48 in the same
  spirit: the entity scan runs before every fill over a cube of side twice the radius, it was the
  heaviest recurring call the director made, and 48 matches the abandon and despawn radius, so a
  leftover about to be culled is still detected.

  A site must also be dry, and that is now enforced where the floor is found rather than only
  where it is accepted. Vanilla's `LiquidBlock.isPathfindable` is `!fluid.is(LAVA)` regardless of
  the path type asked for, so `CaveTerrain.isStandable` was passing a column of water over stone as
  walkable floor; it now requires an empty fluid state at the feet and the head as well.
  `isDirectorSiteValid` keeps its own fluid check as belt-and-braces, so the placement rule is safe
  whichever floor-finder feeds it. `FleeAndBurrowGoal` shares `isStandable` and gains from the
  change: its sweep used to be perfectly willing to send a fleeing lizard *into* a pool, where a
  0.6-block mob wades and reads as having given up. A dormant lizard placed underwater takes its
  first drowning tick 320 ticks (16 s) after placement and is dead by 400, which is what turned
  the playtest's placements into the accounting problem below.

  Each player has at most one pending lizard, and the order its fate is decided in matters. The
  plan checked "has it left BURIED" *before* "is it alive", so that erupting a lizard and then
  killing it would count as a hit. The playtest showed what else that ordering counted: a drowning
  lizard's final damage tick goes through `panicFromDamageIfDormant`, which found a survival player
  within 16 blocks - through a wall - and flipped the corpse-to-be into FLEEING, so the next sample
  saw "not dormant" and recorded a delivered encounter nobody had. `!isAlive()` is now checked first
  and is a miss in any state; only a *living* lizard that has left BURIED counts as delivered. The
  case that gives up - a player killing the lizard inside the one-second window between eruption and
  the next sample - is rare (10 HP behind armour, in under 20 ticks) and harmless when it happens,
  since the abandon path leaves a non-dormant lizard alone and merely refunds the player five
  minutes of budget for an encounter they in fact had. Otherwise a pending lizard is abandoned on a
  dimension change, a three-minute lease, or the player getting 48 blocks away. Abandoning culls the
  lizard and refunds the budget to five minutes short of its threshold, so a miss costs about five
  minutes rather than another full wait. The guarantee that follows is the entire point: even if
  every single placement were missed, an armed player receives a fresh attempt every five
  underground minutes indefinitely, where the measured status quo was never.

  The debug lines were sharpened along the way: the fill summary reports its in-band rejects by
  reason (recently visited, explored chunk, no headroom, no floor, failed the site rule), a fill
  with no candidates says whether the node cap was hit or the reachable volume ran out - two
  different problems, an open cavern versus a sealed pocket - and every abandon reason names its
  branch, the lizard's state, distance and age, so a lizard that died in the floor is told apart
  from one that was unloaded or one that panicked out of the ground before dying.

  Two system properties, read once at startup and wired into `build.gradle` beside the existing
  `geckolib.disable_examples`, make this testable in a single sitting:
  `orelizards.director.budgetSeconds` collapses the whole 20-60 minute loop into seconds, and
  `orelizards.director.debug` logs every decision, the per-fill cost line, and a running hit/miss
  tally. That tally exists because the hit rate is the one number the cadence arithmetic cannot
  derive from the code, and it is what the budget bounds should be retuned against.

- **`CaveTerrain`**, holding the `findFloor` / `isStandable` pair that used to be private to
  `FleeAndBurrowGoal`. The director needs the same answer, and a spawn director reaching into an AI
  goal is the wrong dependency direction. It also confines the `isPathfindable` arity split (three
  arguments up to 1.20.4, one from 1.20.6) to a single file for the port branches. One rule was
  added on the way through - a standable block must be dry, see above - and otherwise only the
  shared `MutableBlockPos` cursor moved from an instance field into the calls.

- **`OreLizardEntity.spawnDormant`**, now the only supported way to create a lizard on the server. It
  goes through `EntityType.spawn`, and that ordering is load-bearing: `create`, then position, then
  `finalizeSpawn`, then add to the level. `finalizeSpawn` reads `blockPosition().getY()` to pick the
  deepslate flag and the ore variant, so constructing the entity and calling `finalizeSpawn` yourself
  hands back a stone coal lizard wherever you put it, Y=-50 deepslate included. This mod has shipped
  that bug once already.

### Changed

- **Natural spawning is disabled**, behind `NATURAL_SPAWNING_ENABLED = false` rather than deleted.
  Nothing was wrong with the code; it simply cannot express what the mob needs, and keeping it makes
  the comparison one boolean away. Both registrations sit inside the guard, and the comment there
  records why they have to move together: removing only `SpawnPlacements.register` makes the mob
  spawn *more*, not less, and with none of the depth or block rules, because for a type with no
  registered placement data `SpawnPlacements.checkSpawnRules` returns `true` and `getPlacementType`
  returns `NO_RESTRICTIONS`. The biome entry is what makes the mob a spawn candidate at all; the
  placement registration is only the filter applied afterwards.

- **A dormant lizard only erupts for a player it can see - by a rule fitted to a mob half a block
  tall.** `tickBuried` finds the nearest survival player within the 5-block trigger range as before,
  and now also requires that player to be *witnessable* before erupting: within 2.0 blocks the
  answer is yes unconditionally, and beyond that a `Level.clip` from **one block above the lizard's
  feet** to the player's eye (`ClipContext.Block.COLLIDER`, `Fluid.NONE`) must be a miss. The
  nearest-player fallback in `panicFromDamageIfDormant` applies the same rule when the damage had
  nobody behind it. The trigger is a sphere, and underground a 5-block sphere routinely reaches
  through a wall into the next pocket: a lizard sealed in stone four blocks away would erupt and
  flee where nobody could see it, spending the encounter on nothing. The director's first playtest
  lost placements to exactly this, but the rule lives in the entity because it protects natural
  spawns just the same.

  It was first written as vanilla's `LivingEntity.hasLineOfSight`, and that proved too strict in a
  way specific to this mob. That ray starts at the lizard's own eye, roughly half a block off the
  floor, so a one-block lip, a stair or the rim of the hollow the lizard sits in blocked a perfectly
  legitimate approach from a player who could plainly see the spot. Starting the ray a full block
  up puts its origin at the height of the lizard as it *appears*, not of the buried mob, and the
  2-block unconditional radius covers a player standing over it, where any wall between them would
  be one they are leaning on. A direct `Level.clip` rather than `Mob.getSensing()` for the same
  reason `hasLineOfSight` was picked over it before: a dormant lizard is `NoAi`, `Sensing` is only
  refreshed from `serverAiStep`, and a cached answer would be stale for as long as the lizard was
  buried. The cost is not worth a thought - it only runs once a player is already inside the
  trigger range, a handful of ticks in a dormant lizard's life, and the ray is at most five blocks
  long.

- **A dormant lizard no longer runs its AI.** `becomeDormant` sets `setNoAi(true)` and
  both routes out of BURIED (`beginErupting`, and `beginFleeing` for the panic-from-damage path)
  clear it, so the hours a lizard spends buried no longer cost a sensing pass, a
  goal-selector tick and a navigation tick each. `FleeAndBurrowGoal` is inert without a flee target
  and the two look goals only matter while the mob is visible, so nothing was being achieved by any
  of it. Three things this deliberately does not touch, each of which would have been a regression:
  `tickBuried` runs from the entity's own `tick()` override, so proximity triggering is unaffected;
  `checkDespawn` is called by `ServerLevel` directly rather than from the AI step, so dormant lizards
  are still culled; and falling is governed by `NoGravity`, a separate flag.

- **`DORMANT_DESPAWN_RADIUS` drops from 128 to 48, and the "nearest player is still underground"
  keep-clause is gone.** Both were written to protect a rare natural spawn from being culled out of a
  cave somebody was working through. Under the director the incentive inverts: every lizard in the
  world was deliberately placed a short walk ahead of one specific player, so both clauses are true
  by construction for exactly the lizards that most need collecting, and an unencountered one would
  be effectively immortal - and a leftover like that suppresses the next placement through the
  director's own nearby-lizard check. 48 matches the director's abandon radius so the two cleanup
  paths agree on when an encounter has been walked away from instead of each waiting on the other.
  `setPersistenceRequired()` was considered and rejected for the director's own placements: it
  short-circuits `checkDespawn` outright, which would make any lost lizard permanent.

- **`MobCategory.AMBIENT` stays, but for a different reason.** The original justification was purely
  about population caps, and with natural spawning off that argument is moot. The category is kept
  because it is baked into the registered `EntityType`, it is what `/data` and mob-cap tooling report,
  and the obvious alternative - `MISC` - is wrong on its own terms, being the category for entities
  that aren't `Mob`s. Only the comment changed.

### Removed

- **`OreLizardEntity.canSpawn` and its 30% rejection roll.** The roll only ever existed because
  vanilla spawn weights are integers and ours was already at the floor of 1; the director sets its
  cadence in minutes, so a dice roll on top would add nothing but noise. The rules themselves survive
  as `isDirectorSiteValid` (Y < 50, at least 8 blocks below the `WORLD_SURFACE` heightmap, on
  `BASE_STONE_OVERWORLD`, not in a fluid), which the disabled `SpawnPlacements.register` now reaches through a
  lambda. The method did not survive, because its signature names `MobSpawnType` - which is
  `EntitySpawnReason` from 1.21.3 on - and that would drag a per-version type into a director call
  path that is otherwise identical on all twenty branches. A lambda's parameter types are inferred,
  so they never have to be written down.

This mod has no additional files