td

SpatialGeometryRng
Login

SpatialGeometryRng

Home · Architecture · Combat · Extensions · Known gaps · Source index

Spatial queries, geometry, and RNG

Snapshot/read and command/write model

Live. Every enemy, projectile, and death pass builds an owned EnemySnapshot vector and rebuilds the world's grid. A snapshot row contains ID, position, HP, and radius. Query results return copyable EnemyRef values containing ID, position, and HP.

live World.enemies
  -> EnemySnapshot::collect
  -> Grid::rebuild (cells hold snapshot indices)
  -> SpatialQuery reads immutable snapshot
  -> behavior queues commands by EntityId
  -> apply commands against live World

This separates broad read access from deferred writes and avoids borrowing unrelated live enemies inside a behavior. A new snapshot and grid rebuild occur for each run_system, not once per frame.

Risk. The snapshot is pass-local, not transactionally live. A target can be killed by an earlier queued command while later commands or projectiles still refer to its snapshot ID. Application looks up the live target, but it does not reject an enemy already at zero HP.

Sources: src/spatial.rs, src/systems.rs, src/world.rs.

Uniform grid

Live. The grid is created with logical width and height 800 and cell size 10, producing 80 × 80 cells. Every cell stores u32 indices into the current snapshot rather than entity IDs.

Operation Behavior
coords(pos) Integer cast of pos / cell_size
rebuild Clears cell vectors, clamps each enemy cell to grid bounds, inserts snapshot index
candidates_in_box Clamps query bounds and yields indices from overlapping cells
Exact filtering Performed by each query after broad-phase candidates

Cell vector capacities survive clear, but the snapshot vector itself is newly collected each pass.

Risk. Negative float-to-integer casts truncate toward zero before clamping. Out-of-range enemies are bucketed into edge cells, so the grid represents positions outside the logical arena as if they were on its boundary for broad-phase purposes; exact distance checks still use the original position.

Source: src/spatial.rs.

Query semantics

Nearest enemy

Live as a utility, not used by the tower. nearest_enemy(from, exclude) widens a square search by one cell at a time. It returns only after the best candidate lies within the current guaranteed circular radius or the grid-wide reach is exhausted. It excludes exactly one entity ID.

The tower does not use this method; it scans world.enemies directly.

Enemies in radius

Unwired through pools. enemies_in_radius(center, r) gathers cells overlapping the circle's bounding box and retains enemies whose centers satisfy distance² ≤ r². It ignores enemy body radius.

Projectile collision

Live. first_hit(pos, projectile_radius) searches a box padded by one cell, keeps body-overlap candidates satisfying:

distance²(enemy.pos, projectile.pos) <= (projectile.radius + enemy.radius)²

and returns the nearest overlapping enemy ID.

The one-cell padding assumes enemy radius is no larger than cell size. Current blobs satisfy this exactly: radius 10, cell size 10.

Sources: src/spatial.rs, src/systems.rs.

Geometry helpers

Helper State Formula/behavior
normal_off_enemy Live Normalize projectile.pos - enemy.pos
steer Unwired Normalize current/wanted vectors, linear blend clamped to [0,1], renormalize at original speed
reflect_dir Unwired v - 2(v·n)n
Arena wall normal Live Adds inward axis normals for crossed boundaries, then normalizes for corners

Risk. normal_off_enemy normalizes a zero vector if projectile and enemy centers coincide. steer has the same issue for zero current velocity, zero desired direction, or a blended cancellation.

Wall dispatch runs when no enemy was hit. With no current behavior returning reflection or pierce for walls, the projectile is marked consumed. Consumed wall hits do not call on_expire and do not use ExpireReason::Consumed.

Sources: src/geometry.rs, src/world.rs, src/behavior/projectile.rs.

Coordinate-system mismatch

Risk. Simulation bounds and rendered arena offsets are inconsistent.

rendered window:       900 x 900
drawn arena:           x=50..850, y=50..850
logical Arena bounds:  x=0..800,  y=0..800
grid bounds:           x=0..800,  y=0..800
tower/seek center:     (450, 450)
scheduled spawn:       (0, 400)
drawn left marker:     (50, 450)

Consequences:

Sources: src/world.rs, src/systems.rs, src/tower.rs, src/enemies/movement.rs.

Deterministic RNG

Live. World seeds its RNG with 1. The constructor XORs the seed with a fixed constant. next_u32 uses xorshift operations; f32 uses 24 output bits to produce [0,1), and range_f32(min,max) linearly maps that value.

Each run_system calls world.rng.fork():

parent state += golden-ratio constant
mixed parent bits seed child stream
behavior pass consumes child only
next pass forks parent again

The child's final state is not merged into the parent. Therefore, how many random values one behavior consumes does not change the next pass's parent-derived stream, but adding/removing a pass or event-delivery wave does.

Current random consumers:

Consumer Stream
Blob split angle jitter and distance Child RNG of death sweep
Weapon crate offers Parent world RNG directly
range_f32 Unwired

Risk. run_system forks even when a pass has no random work, and every event delivery wave forks again. Changes to pass/event counts perturb later crate offers and split streams even if the new code never rolls a random value.

Risk. Deterministic RNG does not make all behavior order deterministic: tower loadout starts from HashMap iteration. Current unequal behavior priorities provide a stable poison-before-impact ordering, but equal-priority additions would need an explicit tie-break.

Sources: src/rng.rs, src/systems.rs, src/world.rs, src/enemies/behaviors.rs.

Verification gaps

There are no unit or integration tests for nearest-neighbor completeness, collision padding, out-of-bounds bucketing, zero-vector normalization, RNG sequences/fork isolation, split placement, or coordinate conversion. See IncompleteSystems.

Home · Architecture · Combat · Extensions · Known gaps · Source index