Smart Enemy AI Enhancement


Smarter biter AI with adaptive resistance, breach reinforcement, scheduled raids, wall regen, and overhaul-mod compatibility.

Overhaul
a day ago
2.0
522
Combat

Changelog

Version: 8.5.6
Date: 2026-05-11
  Bugfixes:
    - UPS stutter spikes between raids on large biter maps are
      eliminated. The v8.4.0 raid pipeline replaced the v8.3-era
      bounded spawner scan (find_entities_filtered with
      position+radius+limit, gated to fire once per raid_interval)
      with an unbounded surface-wide find_entities_filtered that
      returned every enemy spawner on Nauvis. The v8.5.2 isolation
      classifier added a per-spawner count_entities_filtered on top
      of that. Together those run every phase-2 tick (~twice per
      second) for the duration of the multi-minute cooldown after
      each raid, because the cooldown gate moved from the top of
      tick_slice down into try_promote_to_spawning. On dense biter
      networks with thousands of generated chunks the per-tick cost
      blew the scheduler's 2 ms budget for a single tick at a time,
      producing visible stutter (the rolling 60-tick shed-window
      average never tripped because each spike was isolated). The
      surface-wide scan is now bounded with position+radius — the
      engine spatial index does the prefilter in C instead of
      handing the full list to Lua — and the resulting candidate
      list is cached per target chunk with a 10-second TTL so it
      isn't rebuilt twice per second across the cooldown. Same
      candidates are still found and evaluated; the spawner-valid
      gate in try_promote_to_spawning still re-checks each
      selected spawner before SPAWNING begins.
    - Retreat now fires correctly on large maps where the biter
      nest is far from the player base. RETREAT_WINDOW_TICKS was
      30 seconds (1800 ticks) measured from when the group
      finished gathering — shorter than a typical nest-to-base
      march on a large map, so the window expired during the
      march and retreat was never assessed even at 50%+ losses.
      Window raised to 5 minutes (18000 ticks).
  Changes:
    - Breach ping radius doubled (Normal 64 → 128 tiles, Easy
      96, Hard 192). The original 64-tile radius covered small
      bases; on large wall networks the redirected biters were
      often not in range of the breach they should reinforce.
Version: 8.5.5
Date: 2026-05-11
  Bugfixes:
    - Mod no longer fails to load alongside biter/enemy mods
      whose attack chain references a projectile prototype that
      is also fired by a player ammo (e.g. uranium-shotgun-pellet
      from Combat Tweaks and Additions, when an enemy mod also
      uses that projectile name for a biter attack). The data-
      stage projectile-block patcher used to mutate the
      projectile prototype in place when its substring-based
      heuristic failed to recognize it as a player projectile,
      and a hard validate.lua check refused to load the mod
      stack at all. The patcher now always clones the
      projectile into <original>-smart-enemy-ai-blockable and
      rewires only the enemy's attack delivery to the clone;
      originals are never touched. The validate check is
      removed (unreachable under the new rule). Player turret
      firing the original projectile is unaffected.
  Minor Features:
    - Breach pings (the redirect biters get when a player
      turret dies inside an active attack) are now rate-
      limited to one broadcast per 60 seconds globally.
      Heavy raids kill many turrets in succession; without
      the cap, every death rebroadcast a redirect and
      nearby biters snap-converged on each successive
      turret, looking like permanent retargeting jitter.
      One ping per minute communicates "the perimeter is
      under sustained pressure" without the constant
      churn. Per-squad cooldown (30 s) is unchanged. The
      force_breach_ping debug remote bypasses the gate
      for instant manual testing but still stamps the
      cooldown clock.
  Internal:
    - Removed the maintainer-only "Debug: Command Trace"
      runtime setting, the debug_trace_squad remote, the
      lib/debug/trace.lua module, and every trace call
      site across the squad/raid/breach/path subsystems.
      The trace channel served its purpose during the
      jitter investigation that produced the holding-
      restamp and stale-waypoint-prune fixes; it has no
      runtime use for players. set_command call paths are
      simpler and faster as a result.
  Bugfixes:
    - Mod no longer fails to load alongside enemy mods
      (e.g. Fulgoran Enemies) whose biter/spitter variants
      reuse a vanilla player projectile prototype in their
      attack chain. Previously the data-stage scan mutated
      the shared projectile prototype in place to add the
      projectile-block layer, which would have broken the
      player's shotgun/turret firing the same projectile;
      the load-time validate caught the invariant violation
      and aborted load. The scan now detects when an enemy
      attack delivery references a projectile that is also
      reachable from player ammo and clones the prototype
      under a mod-namespaced name, rewiring only the enemy
      delivery to the clone. The original prototype stays
      clean for player weapons and the cloned copy carries
      the block layer so the modded enemy's attack still
      collides with walls/gates as intended.
Version: 8.5.2
Date: 2026-05-10
  Major Features:
    - Raid candidate selection is now pollution-aware. Each
      candidate spawner's chunk pollution is sampled at
      selection time, and the candidate list is sorted
      pollution-ascending (distance-ascending tiebreaker).
      Spawners in low-pollution chunks are preferred because
      the engine's pollution-attack AI is not actively
      coordinating attack waves from them, so members of
      our raid groups don't get siphoned into competing
      vanilla attack groups during the 30-second warning
      window. In-game verification on a dense Space-Age save
      where hold_loss had been 20-60% of assembled members:
      hold_loss=0 across the test sample once pollution-
      aware selection landed.
    - Raids now spawn from two different unit-spawner
      prototypes when a candidate of a different type is
      available within 96 tiles of the primary spawner. The
      group split is 50/50 between primary and secondary,
      so vanilla biter-spawner + spitter-spawner pairs
      produce mixed melee/ranged waves automatically. The
      comparison is by entity.name, so modded enemy stacks
      with additional spawner prototypes get the same
      mixed-composition behavior without prototype-name
      hardcoding. Falls back to single-spawner raids when
      no different-type spawner is nearby.
  Minor Features:
    - Spawner-isolation filter during candidate selection.
      Candidates with more than 4 spawners within a 16-tile
      radius are deprioritized and used only as a fallback
      when no isolated candidates exist. Dense biter
      networks were causing the engine's coordination logic
      to compete for biter membership during HOLDING;
      isolated candidates avoid that competition.
    - Unreachable-target cache. When candidate selection
      exhausts every spawner without finding a valid path
      to the target chunk, the chunk is marked unreachable
      for 7200 ticks (2 minutes). pick_target_chunk skips
      marked chunks during that window so the selection
      pipeline doesn't thrash for ~6 minutes re-trying the
      same impossible target every phase-2 tick.
    - HOLDING and DISPATCHED log lines now break the loss
      accounting into add_member_failures (engine refused
      the add), drift_loss (added but disappeared before
      HOLDING), and hold_loss (left during the warning
      window). Selection accept-log includes pollution and
      distance for post-hoc correlation analysis.
    - Path obstacles now only veto a candidate when the
      destroy-set includes a cliff. Trees, rocks, neutral
      entities, and enemy spawners are no longer reasons to
      reject a candidate — biters chew through them. Water
      remains impassable via the pathfinder's collision
      mask (not via the destroy-set check).
  Bugfixes:
    - scatter_radius is now clamped at
      max_group_radius - 4 (default cap: 26 tiles). Without
      the clamp, raids of ~900+ biters scatter members at
      sqrt(total) >= 30 tiles from the spawner, outside the
      group's coordination radius; the engine accepts the
      add_member call but releases the unit on the next
      tick, and the engine's pollution-attack AI scoops it
      into a vanilla attack group. Clamping eliminates this
      bleed (drift_loss=0 on raids that previously lost
      hundreds of biters during SPAWNING).
    - add_member failures are now detected via the member-
      count delta and the failed biter is destroyed rather
      than left loose, so engine pollution-attack groups
      can't form around abandoned spawn-residue. A new
      bail-out cap promotes the spawn_job to HOLDING when
      add_member failures hit half the target, preventing
      multi-minute spin loops on adversarial saves.
    - Two-spawner raids correctly survive when one spawner
      dies mid-spawn — the spawn loop continues sampling
      from the survivor's result_units curve and using its
      position as the jitter origin.
  Changes:
    - RAID_SPAWNER_MAX_DISTANCE raised from 512 to 1024
      tiles. Wider candidate pool gives pollution-aware
      selection room to reach low-pollution spawners that
      are further from the player base on real maps where
      water and cliffs partition the landmass.
    - Dispatch waypoints subsampled to a minimum 24-tile
      spacing. The pathfinder's raw output had ~50+ way-
      points for a typical raid path, each producing a
      brief sub-command transition pause during the march
      (visible as 2-second stop-go-stop micro-stutter).
      Subsampling collapses transition count to ~5-10
      while preserving turn points and final attack
      command, so the wave moves smoothly and only
      reorients at meaningful path corners.
    - Selection candidate records now carry pollution and
      distance fields used by the new sort comparator and
      the accept log line.
Version: 8.5.0
Date: 2026-05-10
  Bugfixes:
    - Large raids no longer arrive as a strung-out line of
      loose biters with no warning. The pipeline now splits
      raids of more than 150 biters across multiple unit
      groups (one group per ~150 biters, round-robin
      distributed). Previously a single oversized scripted
      group was siphoned apart by the engine's pollution-
      attack AI mid-HOLDING — biters were reassigned into
      a vanilla attack group and walked to the player's
      base under engine AI, defeating both the 30-second
      warning window and the cached path. With the split,
      each group stays well below the engine's
      max_unit_group_size cap and the wave dispatches
      coherently after the full warning window.
Version: 8.4.1
Date: 2026-05-10
  Minor Features:
    - Raid chart-tag skull is now bright red (was yellow), so
      the alert reads more clearly at low zoom.
    - Raid chart tags now auto-clean after 60 seconds — the
      mod parks each tag in storage.raids.expiring_tags on
      creation and the alerts tick slice destroys expired
      entries. Previously the tag persisted until the player
      removed it manually.
Version: 8.4.0
Date: 2026-05-10
  Major Features:
    - Mega-raid pipeline rebuilt around a single out-of-sight
      enemy spawner. The scheduler picks one spawner within
      pollution range of the most-threatened chunk where no
      player vision currently reaches, validates a walkable path
      (player-force walls/buildings ok; trees, rocks, and enemy
      spawners blocking the route veto the candidate), then
      spawns max(round(evolution * factor), 30) biters at that
      spawner across multiple ticks into a single cohesive group.
      Sample distribution matches the spawner's own result_units
      curve at the current evolution, so unit composition feels
      native to that nest. Factor is 700 / 1000 / 1300 per
      Easy / Normal / Hard preset. Hard-serial: one raid in
      flight at a time. Replaces the previous all-spawners-fire-
      simultaneously cluster dispatch.
    - Raid warning is strictly serialized: spawn silently → fire
      the alert when the wave is fully assembled at the spawner →
      hold for 30 seconds → start moving. The player gets a
      consistent 30-second warning window regardless of raid
      size, rather than a window that overlapped with spawn-up
      and varied with evolution.
    - Raid chart-tag icon is a yellow-tinted skull (custom
      virtual signal). "Raid incoming" chat warning uses bold
      rich text, and the skull's chart-tag label is bold too —
      easier to spot when scanning the map at low zoom.
  Bugfixes:
    - Small biter groups no longer jitter when retreating. The
      hysteresis lock kept assess() returning true (correct), but
      refresh_command was issuing a fresh flee command every
      slice during the lock, and the destination could flip
      between iterations of the spawner search, causing visible
      back-and-forth. Command issuance is now gated behind the
      same lock check that gates the lock setting: one flee
      command per fresh retreat, no re-issuance until the lock
      expires.
    - Alerts no longer fire for raid waves whose biters all died
      before arrival. The alert pipeline checks the group's live-
      member count at firing time and silently drops phantom
      entries.
    - UPS-budget shedding actually engages now. The profiler
      reading was treated as microseconds when it was already in
      seconds, so the rolling average came in ~10^6 too small and
      the budget threshold never tripped. The mod now correctly
      converts seconds to microseconds before averaging.
    - Wall regen and projectile blocking are guaranteed never
      shed. Wall regen had been listed as the level-5 shed
      target, contradicting its "never shed" contract; that entry
      is gone and the regen tick slice runs unconditionally.
    - First-raid grace period now scales with difficulty (Easy
      90 min, Normal 60 min, Hard 30 min). Previously the first
      raid on any preset fired at 5 minutes regardless.
    - Pathing requests rejected with try_again_later now fall
      back to attack_area like the other failure branches, so
      squads no longer stall after pathfinder overload.
    - Path cache hits clear any in-flight request id, so a late
      callback for an abandoned earlier request can no longer
      overwrite the freshly-installed cached waypoints.
    - Held raid waves wiped during the warning window (e.g. by
      artillery) abandon cleanly instead of issuing set_command
      on an invalid group at dispatch.
    - Raid spawn counter tracks actual placements rather than
      attempts, with a 3x attempt cap so locked terrain (water,
      cliffs, packed entities) cannot pin the spawn pipeline
      forever.
    - Wall regen no longer leaks destroy-tracking entries.
      Repeated damage/heal cycles previously stranded one
      destroy_map entry per cycle until the wall was destroyed;
      the dequeue path now clears the entry alongside the regen
      record.
    - Adaptive resistance variant swaps and unwind-on-disable
      now run find_non_colliding_position before destroying the
      original entity, restoring the v7.2.3-class safety against
      losing biters when a neighbor briefly occupies the tile.
    - Stream-to-projectile conversion handles both single-action
      and array-action ammo definitions, so modded units that
      use array form get projectile blocking too.
    - Resistance decay slice no longer relies on next() against
      a key just deleted from the iterated table; empty protos
      are collected during traversal and removed after the slice
      finishes.
    - Offensive AI is fully disabled on non-Nauvis surfaces:
      group-gather and squad-refresh now check the surface index
      and skip non-Nauvis squads (with proper pending-request
      cleanup). Defensive features (wall regen, projectile
      blocking) remain surface-agnostic.
    - Surface-deletion squad cleanup uses the Squad method
      directly instead of a metatable-truthy guard that always
      passed, eliminating the misleading dead branch.
  Minor Features:
    - New diagnostic remotes diag_selection_job() and
      diag_spawn_job() expose the in-flight raid pipeline state
      (selection candidates, spawn progress, hold/dispatch phase)
      for triage. diag_dump() summary extended with selection /
      spawn progress fields.
  Changes:
    - Storage schema bumped from v2 to v3 with a no-op migration
      that ensures storage.raids.selection_job and spawn_job
      substructures exist on existing saves. Pre-8.4 saves load
      and run normally.
    - Old cluster-dispatch logic in lib/raids/scheduler.lua
      replaced; size_per_spawner constants removed. Dead
      compute_eta and on_path_finished_for_raid hooks removed
      now that the raid pipeline owns its own path lifecycle.
Version: 8.3.0
Date: 2026-05-08
  Bugfixes:
    - Breach reinforcement: surface.find_units expected a single
      ForceID, not an array. v8.2.0 passed an array which crashed
      every force_breach_ping call and every real turret-death event.
      Replaced with surface.find_entities_filtered{type="unit", ...}
      to match the established codebase pattern.
    - Breach reinforcement: LuaEntity.unit_group access throws in
      Factorio 2.0 (the property was reorganized). Replaced with a
      tracked-members set built from storage.squads — narrower
      semantics but no throwing field access.
  Major Features:
    - Test suite reworked for release verification. Eight new
      scenarios cover previously-untested features: T7 (retreat
      hysteresis, revived from v6 deletion), T13 (flanking),
      T14 (breach cooldown), T15 (gate regen), T16 (raid alerts),
      T17 (difficulty preset scaling), T18 (master toggle off),
      T19 (player gun-turret data-stage patch safety). Existing
      T5/T6/T10/T11 tightened: event-driven path-callback waits,
      narrower outcome accept set, raise_built on chunk-threat
      walls, directional evolution check via the spawner's own
      result_units curve.
    - New docs/release-check.md ties scenarios + manual checks
      (UPS shed, multiplayer determinism, welcome message, preset
      hot-reload) into a step-by-step pre-portal-upload checklist.
  Minor Features:
    - New diagnostic remotes diag_squad_kind(squad_id) and
      diag_pending_alerts(). Used by the new scenarios; safe for
      manual triage.
    - diag_squad_state(squad_id) extended with alive_count.
  Changes:
    - ARCHITECTURE.md pitfall list: added entries for the
      LuaEntity.unit_group Factorio-2.0-throw and the
      on_unit_group_finished_gathering duplicate-Squad pattern.
Version: 8.2.0
Date: 2026-05-08
  Major Features:
    - Breach reinforcement: when a player turret dies during an attack,
      nearby enemy squads and loose biters are pulled toward the breach
      via a position "ping." Radius scales with the difficulty preset
      (Easy 48 tiles, Normal 64, Hard 96). Per-recipient 30-second
      cooldown prevents ping-pong when several turrets fall in quick
      succession. Disabled with the rest of the offensive AI when the
      Enemy Advanced AI master toggle is OFF; sheds at the same load
      level as flanking when the UPS budget tightens.
  Minor Features:
    - New diagnostic remote `diag_squad_state(squad_id)` returns
      `{cooldown_until, fleeing_until, last_path_outcome}` for triage
      of squad behavior.
    - `diag_dump` includes a `breach` block with `pings_fired` and
      `recipients_total` counters.
    - Debug remote `force_breach_ping(position [, surface_index])`
      fires a ping without needing a turret to die — used by the new
      T12 scenario and available for player-side console debugging.
  Changes:
    - Storage schema bumped from v1 to v2 with a no-op migration that
      ensures `storage.coord` exists. No save data is rewritten;
      existing saves load and run normally.
Version: 8.1.1
Date: 2026-05-07
  Changes:
    - Welcome messages rewritten for distinct per-preset tone and
      player-facing language. Each difficulty now reads in its own
      voice: Easy is welcoming, Normal is matter-of-fact, Hard is a
      warning. Replaced engineer-speak ("Path-around-walls AI, squad
      retreat, adaptive resistance") with player-language ("biters
      now path around your walls when they can, retreat when bloodied,
      and grow resistant to weapons you over-rely on"). Dropped the
      diag_dump command from every welcome — it lives in the README
      for the small fraction of players who need it.
    - Hard-difficulty mid-game warning rewritten to match the new
      welcome tone.
    - Settings path arrows upgraded from `>` to `→` for visual
      polish, matching the README and mod portal copy.
Version: 8.1.0
Date: 2026-05-07
  Major Features:
    - New "Difficulty Preset" runtime-global setting (Easy / Normal / Hard).
      Easy: raids half as frequent, 15% resistance cap, level-1 variant
      restriction, fast wall regen (5s window). Normal: defaults
      unchanged from prior versions. Hard: frequent raids, 40% cap,
      slower regen (20s window), aggressive flee threshold.
      Players can change the preset mid-save without restarting; effective
      values are computed on every read so the change takes effect at
      the next subsystem tick. Multipliers live in the new
      lib/difficulty.lua module.
    - One-time in-game welcome message on first mod load. Briefly
      describes which features are active and points at the settings
      panel. Content adapts to the active difficulty preset and the
      umbrella toggle state. Fires once via storage._welcomed flag;
      subsequent loads are silent.
    - New "Enemy Advanced AI" master startup toggle. When OFF, all
      offensive enemy-AI features (smarter pathing, squad retreat,
      flanking, target priority, mega-raids, adaptive resistance) are
      disabled in one place. Defensive features (wall regeneration,
      projectile blocking) continue to work. Use this for a
      "vanilla biters + wall QoL" experience or to stack with another
      AI mod that handles enemy AI. With the umbrella OFF, the
      offensive event handlers are not registered at all and the
      on_entity_damaged filter drops the unit-type subscription —
      zero per-event dispatch cost on megabases.
    - Mega-raid unit selection now uses a three-strategy fallback chain
      that supports modded enemies without registration:
      (1) weighted pick from the spawner's result_units curve at the
      current evolution; (2) evolution-indexed pick from result_units
      treated as tier-ordered when weights are unreadable or all zero;
      (3) vanilla biter chain only as a last resort. Strategy 2 uses
      Factorio's standard biter evolution thresholds (0.2 / 0.5 / 0.9)
      so the "feel" of evolution progression matches vanilla regardless
      of how many tiers the spawner declares.
    - Adaptive resistance variants now respect parent vulnerability.
      Overhaul mods (Pyanodons, Space-Age-extras, etc.) that declare
      biters as vulnerable to a damage type (negative percent — e.g.
      cold spitter at fire = -100%) get correctly-adapted variants:
      the generator clamps the parent's resistance at 0 before adding
      the adaptation level, so a fire-adapted cold spitter spawns
      fire-RESISTANT (matching the design intent) rather than just
      slightly-less-fire-vulnerable.
    - Dynamic damage-type discovery. Variant prototypes are now
      generated for every damage type registered by the mod stack
      (laser, electric, poison, cold, custom Bob's/Pyanodons types,
      etc.) instead of the fixed vanilla four. Modded damage types
      participate in adaptive resistance with no registration.
  Bugfixes:
    - Save/load no longer crashes with "Detected modifications to the
      'storage' table" CRC mismatch. on_load now snapshots stale
      path-request IDs into a module-local; the first scheduler tick
      after load consumes the snapshot and removes only those specific
      IDs, preserving any new requests submitted between load and that
      tick.
    - Mega-raid biters no longer spawn stuck inside their own spawner.
      The previous loop jittered each unit's spawn position by ±2 tiles,
      but spawners occupy a ~3x3 footprint, so create_entity placed
      biters at colliding positions — friendly-collision (same enemy
      force) prevented physical pushout, leaving the entire raid wedged
      inside its own spawner cluster, unable to move or be targeted.
      Spawn positions now jitter ±4 tiles (clearing the footprint) and
      snap to a non-colliding tile via surface.find_non_colliding_position.
    - Mega-raids and flank-split sub-squads now reliably advance toward
      their target. Scripted unit groups created via
      surface.create_unit_group sit in "gathering" state and ignore
      set_command alone; group.start_moving() is now called after
      pathing.request for every freshly-created scripted group.
    - Raid-incoming chart-tag alert now fires for every raid, not just
      those whose path was accepted by the pathfinder. Rejected paths
      (detour, destroy-required, queue saturation) used to silently
      drop the alert; the handler now falls back to a straight-line
      ETA from the squad's average position to the target so every
      raid produces a chart tag and chat message.
    - Raid unit selection no longer crashes on modded spawners whose
      result_units curves use the Factorio 2.0 runtime field name
      (.weight) instead of the data-stage name (.spawn_weight). The
      interpolator now reads through tolerant accessors that try every
      known shape (.spawn_weight / .weight / pt[2]) and fall back to 0
      if all fail.
    - Raid alerts no longer crash with "LuaEntityPrototype doesn't
      contain key movement_speed" when a raid contains a non-Unit
      member or a unit subtype that omits movement_speed. The speed
      read is now wrapped in pcall and gated on prototype.type ==
      "unit" — non-units contribute nothing to the slowest calculation,
      ETA falls through to the default if every member fails to expose
      a speed.
    - pick_unit_for_spawner's result_units access is similarly
      pcall-guarded against modded spawner prototypes that don't
      expose the typed field.
    - Adaptive resistance variant generation no longer aborts mod load
      when an overhaul mod declares biter prototypes with negative
      resistance percent (vulnerability). The variant generator clamps
      the parent at 0 before adding adaptation; the validate-variants
      pass relaxes the lower bound check (only enforces the 30% upper
      cap) as defense in depth.
    - validate-variants no longer rejects modded damage types whose
      names contain hyphens (e.g. Bob's "bob-pierce"). The parse pattern
      `<base>-smart-resist-<dmg>-<level>` was already unambiguous via
      the end-anchored level digit run; the over-cautious explicit
      hyphen-rejection check has been removed. Modded enemy stacks
      with hyphenated damage type names load cleanly.
    - on_built_entity / on_robot_built_entity / script_raised_built no
      longer count enemy-force walls, turrets, or buildings as
      player-side threat. The previous filter checked only
      type == unit, so PvE-with-enemy-base scenarios could see raids
      targeted at enemy infrastructure.
    - flank.maybe_split now snapshots the unit-group members table
      before calling add_member. add_member re-parents the unit and
      mutates the source list; iterating the live list could skip
      members and produce uneven splits.
    - Chunk eviction and raid alerts now resolve the Nauvis surface
      through compat.nauvis_index() instead of a hardcoded surface
      index of 1. Scenarios that reorder surfaces no longer silently
      target the wrong surface.
    - Welcome message uses game.print (visible chat) instead of log
      (factorio-current.log only) so players actually see the
      orientation message. Earlier draft made the message invisible
      to the player and defeated the whole point of the C2 component.
    - Squad:alive_count() now checks per-member validity the same way
      avg_position() does, preventing inflated health ratios from
      skewing retreat decisions when group members go invalid.
    - compat.nauvis_index() returns nil instead of 1 when Nauvis is
      absent from the surface table; call sites are guarded against
      nil so non-Nauvis-only setups don't accidentally target surface 1.
    - on_entity_spawned variant swap now checks the create_entity
      return value; falls back to the base prototype if variant
      creation fails instead of silently losing the unit.
    - storage.raids initialization uses per-field defaults so partial
      tables loaded from old saves cannot leave pending_alerts nil and
      crash the alert tick_slice.
    - diag_resist_cap remote now returns the difficulty-scaled
      effective cap value rather than the constants.lua base.
  Changes:
    - RESIST_CAP difficulty multiplier limits variant level selection:
      Easy restricts pick_variant_for to level 1 (10% resist max),
      Normal/Hard allow up to level 3 (30%).
    - Strategy 2 (index-based fallback) and Strategy 3 (vanilla
      hardcoded last resort) for raid unit selection share a single
      threshold function (evolution_to_tier_index). Future tuning of
      evolution breakpoints applies uniformly to both paths.
    - Storage now carries a schema version (storage._schema_version
      = 1) to support future save migrations. Existing saves are
      treated as version 1 on first load. No migrations exist yet.
    - The Nauvis-only design assumption is now codified via a
      compat.nauvis_index() helper instead of scattered
      surface-index/name compares.
    - Squad metatable now carries a _kind field
      ("vanilla" / "raid" / "scripted") so consumers know which groups
      need explicit start_moving() handling.
    - Squad:avg_position now consistently returns Factorio's standard
      {x=, y=} table form (matching LuaEntity.position). Previous
      mixed-shape behavior forced consumers to handle both shapes via
      dual-form readers.
    - Path cache freshness window (was hardcoded 600 ticks) hoisted
      to lib/constants.lua as PATH_CACHE_TTL_TICKS.
    - Removed leftover [DBG] log spam from the raid-alert path.
  Compatibility:
    - Auto-detects modded enemies via subgroup == "enemies" AND via
      spawner result_units for raid composition.
    - Plays nicely with Armored Biters and other resistance-adding
      mods (clamped 30% cap on variant resistance).
    - Tested-working with Armored Biters, Pyanodons, Space Age +
      Space-Age-extras, Rampant.
    - Mods adding custom damage types (laser turrets, elemental
      overhauls, etc.) get full adaptive resistance support with no
      registration. Hyphenated damage type names are supported.
    - Storage shape unchanged from v7.0.1 except for the additive
      storage._welcomed boolean flag. No schema bump. v7.0.1 saves
      load directly into v8.0.0; the welcome message fires once on
      the first scheduler tick after upgrade.
    - Multiplayer-safe: deterministic across peers, no on_load storage
      mutation, runtime-global settings sync enforced by Factorio,
      difficulty effective values computed on every read (no stale
      cached state).
  Info:
    - README.md and ARCHITECTURE.md fully refreshed for v8.0.
      ARCHITECTURE pitfalls section documents the post-v7.0 hard-won
      patterns: surface.find_non_colliding_position before create_entity
      for enemy units; pcall around LuaEntityPrototype type-specific
      field access. Several known-and-deferred limitations are also
      explicitly documented (chunks PvP-blind raid targeting, variant
      level fallback for high-resistance parents, bounded
      pending_alerts accumulation, force_squad_flee health snapshot).
    - Test scenarios updated. T11 (raid unit selection) loosened to
      assert evolution-responsiveness (low ≠ high, both valid strings)
      instead of exact vanilla unit names — modded environments where
      biter-spawner.result_units now includes spitters or other tiered
      enemies pass cleanly.
    - New welcome message and difficulty-hard-warning locale strings.
Version: 7.0.1
Date: 2026-05-06
  Bugfixes:
    - Save/load no longer crashes with "Detected modifications to the
      'storage' table" CRC mismatch. The v7.0.0 (and earlier) on_load
      handler cleared storage.path_q.pending in place, which Factorio
      forbids — on_load must be strictly read-only or it breaks
      multiplayer determinism and trips the engine's CRC check. The
      handler now snapshots the IDs of in-flight path requests (which
      the engine abandons across save boundaries) and the first
      scheduler tick after load deletes only those specific IDs —
      preserving any new requests submitted between load and that
      tick. Test scenarios that submit a path request from on_init
      (T5/T6/T8) run to completion as expected.
Version: 7.0.0
Date: 2026-05-03
  Bugfixes:
    - Mega-raids now spawn modded biter prototypes by reading the chosen
      spawner's result_units list, instead of hardcoding vanilla biter
      names. Mods like Rampant, Krastorio2, and Armored Biters get
      automatic raid support with no registration required. Falls back
      to the previous vanilla-biter switch if a spawner has no usable
      result_units.
  Changes:
    - Storage now carries a schema version (storage._schema_version = 1)
      to support future save migrations. Existing saves are treated as
      version 1 on first load. No migrations exist yet.
    - The Nauvis-only design assumption is now codified via a compat.
      nauvis_index() helper instead of scattered surface-index/name
      compares. Behavior unchanged.
  Info:
    - README "Known limitations" section removed; the items it listed are
      no longer reproducible in v7.0.
    - Raid-interval comment in lib/raids/scheduler.lua now states the
      exact 0.34 multiplier instead of the loose "/3" approximation.
    - New data-stage validation pass for resistance variants. Loud
      failure at load time if variant generation desyncs from the
      naming contract, instead of silent breakage.
    - Comment added to lib/scheduler.lua's profiler-parsing line
      explaining why the serpent.line regex is the correct pattern in
      Factorio 2.0 (LuaProfiler exposes no numeric accessor).
    - DO-NOT-RENAME warning added near the existing
      script.register_metatable call in lib/state.lua to protect save
      compatibility.
    - diag_dump's hardcoded version string updated to 7.0.0 (was stuck
      at 6.2.0; pre-existing drift).
    - New test scenario T11 (11_raid_unit_selection) verifies the new
      raid unit selection at low and high evolution.
  Compatibility:
    - Nauvis-only by design. Space Age planets (Vulcanus, Gleba, Aquilo)
      are explicitly out of scope.
    - Modded enemy prototypes auto-detected via subgroup == "enemies"
      (unchanged) AND now via spawner result_units for raid composition.
    - Plays nicely with Armored Biters and other resistance-adding mods
      (introduced in v6.2.1; restated for v7.0 visibility).
Version: 6.2.1
Date: 2026-05-02
  Balancing:
    - Adaptive resistance variants now respect the 30% cap when the
      parent enemy already carries a base resistance from another mod
      (e.g. Armored Biters). For each damage type the merged percent
      is clamped at 30, and once the cap is reached no further variant
      levels are generated. Damage types where the parent is already
      at or above 30% produce no variants at all; other damage types
      are unaffected. Prevents super-tanky enemies when stacking with
      resistance-adding mods.
Version: 6.2.0
Date: 2026-05-01
  Info:
    - New diagnostic remote `diag_dump` returns a comprehensive state
      snapshot suitable for bug reports. Users invoke
      /c game.print(serpent.line(remote.call("smart-enemy-ai", "diag_dump")))
      and paste the result.
    - New ARCHITECTURE.md at the mod root documents subsystems,
      scheduler phases, storage shape, lifecycle, and common pitfalls.
      Aimed at future maintainers and contributors.
    - New T10 smoke integration test scenario exercises every subsystem
      simultaneously for 60 seconds and asserts no crashes or runaway
      state growth.
    - Inline header comments added to lib/spawn_variant.lua,
      lib/state.lua, lib/ai/retreat.lua, and lib/ai/pathing.lua to
      explain non-obvious invariants.
    - Path outcome counters now tracked under storage.path_q.outcome_counts
      so diag_dump can report distribution (accepted, rejected_detour,
      rejected_destroy_required, fallback_*).
Version: 6.0.1
Date: 2026-05-01
  Bugfixes:
    - Disabling the "Biter Build Adaptive Resistance" startup toggle now
      converts existing variant biters back to base prototypes during the
      configuration-change handler, instead of leaving them as orphaned
      entities that the engine would silently drop on save load.
    - Slice iterator cursors (regen, squads, resistance ledger) now
      validate their saved key against the target table before calling
      `next()`, defending against a Lua 5.2 edge case where a stored
      cursor could refer to a key deleted between slices.
    - Resistance ledger prune threshold lowered from 1.0 to 0.01 so
      single small-damage events (e.g. low-tier turret hits) survive
      long enough to influence variant selection at the next biter
      spawn. Storage growth is negligible — entries below 0.01
      contribute less than 0.000003 to the resist factor anyway, and
      natural decay (tau ~30 minutes) eventually evicts them.
  Info:
    - Removing the mod entirely from a save remains under Factorio's
      standard handling: orphaned variant biters are dropped by the
      engine, the save loads cleanly, the world continues. New biters
      from spawners are unaffected.
Version: 6.0.0
Date: 2026-05-01
  Changes:
    - Pathfinding now rejects routes more than twice as long as the
      straight-line distance to the target. Previously biters could
      slip through any maze of walls indefinitely without taking
      damage; they will now attack walls along the direct path
      instead. The threshold is adjustable via the new "Max
      pathfinding detour ratio" runtime setting.
    - Retreating squads now stay in flee mode for at least 10
      seconds once retreat is triggered. Previously the flee command
      could flicker on and off as members died, producing visibly
      hesitant movement. Configurable via the new "Retreat
      hysteresis" runtime setting.
    - Biters reaching their attack target now prioritize turrets,
      power infrastructure, and machinery over walls, with a final
      area-sweep to clean up remaining targets. Previously the
      generic attack-area command often ended up targeting the
      nearest wall regardless of what else was present.
  Major Features:
    - Adaptive resistance reworked from runtime heal-back to engine-
      applied prototype variants. When a biter spawns and your
      damage history shows accumulated damage of a type, the engine
      spawns a more resistant variant with that resistance baked in.
      No more visible "biter takes damage then heals back". The
      resistance fades through population turnover as new biters
      spawn under updated ledger state.
  Compatibility:
    - Mod loads cleanly with Space Age installed. AI and raid
      behaviors are Nauvis-only; defensive features (wall regen,
      projectile blocking) work surface-agnostically.
  Info:
    - README expanded with a Diagnostics section documenting all
      remote-interface helpers.
    - Test scenarios entirely replaced. Old flaky scenarios removed;
      seven new focused scenarios exercise the behavioral changes
      deterministically. Retreat hysteresis is verified by code
      review rather than scenario playback (the lock arithmetic is
      trivial and Factorio's scripted unit-group lifecycle does
      not cooperate with synthetic squads).
Version: 5.0.7
Date: 2026-05-01
  Balancing:
    - Default Adaptive Resistance Cap restored to 30%. The temporary
      reduction to 15% was needed only to mask the killing-blow bug
      fixed in 5.0.4; with that fix in place, 30% is the intended
      design value.
Version: 5.0.6
Date: 2026-05-01
  Bugfixes:
    - Reduced pathfinding bounding box so biters can navigate single-tile
      gaps in player walls. Previously the pathfinder rejected paths
      through narrow gaps, and biters fell back to attacking the closest
      wall instead of using the open route.
  Graphics:
    - Added mod portal thumbnail.
Version: 5.0.5
Date: 2026-05-01
  Info:
    - Updated mod author metadata.
Version: 5.0.4
Date: 2026-05-01
  Bugfixes:
    - Enemy unit groups created via the mod's remote interface now
      correctly begin moving after their pathfinding command is set.
      Previously such groups would receive a path but stand still until
      another command displaced them.
Version: 5.0.3
Date: 2026-05-01
  Info:
    - Added internal diagnostic logging for the raid-alert flow. No
      player-facing change.
Version: 5.0.2
Date: 2026-05-01
  Major Features:
    - Complete rewrite for Factorio 2.0.
    - Smarter enemy AI. Biter groups path around walls when an open
      route exists, retreat to the nearest spawner when their squad
      takes heavy losses, and split into multiple wings on large
      attacks.
    - Adaptive resistance. Enemies accumulate temporary resistance to
      recently-used damage types, with exponential decay and a
      configurable cap. Encourages mixed-damage strategies; resistance
      fades when you switch weapons.
    - Scaling raids. In addition to vanilla pollution attacks, scheduled
      mega-raids spawn periodically. A subtle alert appears on the map
      roughly thirty seconds before arrival. Raid size and frequency
      scale with evolution.
    - Wall and gate regeneration. Any wall or gate that has taken no
      damage for ten seconds will gradually regenerate to full HP over
      the following ten seconds. Any new damage during regeneration
      pauses and resets the timer.
    - Projectile blocking. Spitter and worm acid streams are converted
      into physical projectiles that collide with walls and gates
      instead of arcing over them. Player turret projectiles are
      unaffected.
    - UPS-budget shedding. The mod measures its own per-tick cost and
      automatically disables features in priority order when over
      budget. Features re-enable stepwise once load drops.
  Compatibility:
    - Auto-detects modded enemies that follow Factorio's standard
      "enemies" subgroup convention. Prototypes that do not can be
      added through the mod settings.
    - Mods that create custom enemy forces can register them via
      remote.call("smart-enemy-ai", "register_enemy_force", "name").
    - Multiplayer-safe. All randomness is seeded; behaviour is
      deterministic and replay-stable.
  Settings:
    - Fifteen tunable settings cover the UPS budget, regen window and
      duration, resistance cap and decay rate, raid interval and
      warning lead time, retreat threshold and assessment window,
      shed-ladder hysteresis, and additional enemy-prototype/force
      compatibility lists.