Skip to content

Commit 64b5b79

Browse files
wormeymanclaude
andauthored
fix(cliffs): the collision box is the RAW rectangle - #88 was wrong (#84) (#90)
* docs(cliffs): shipping orientation is 2.4%, not 2.0% (#84) The banner kept the pre-#88 figure. Since the collision box was corrected to rotbb's rotated rectangle the rejection costs ZERO true positives, so the shipping matched set is identical to the unfiltered one - 1531 either way, 37 wrong orientations, 2.42%. Measured on both paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt * fix(cliffs): the collision box is the RAW rectangle - #88 was wrong (#84) Disassembly, prompted by flagging #88's fix as the thing I was least confident about. It was right to flag: #88 scored best on every metric and was wrong about the mechanism. The engine does NOT collide against rotbb's rectangle rotated 45 degrees. It uses the RAW stored rectangle and discards the orientation tag entirely: 1. EntityMapGenerationTask::tryToAddCliff (0x101625038) loads the orientation's box from proto + 0x5c0 + id*0x48 (20 bytes: four int32 edges at +4, the orientation word at +0x14) and calls wouldCollide with Direction = 0 - literally `mov x4, #0x0`. 2. wouldCollide (0x101625468) forwards box and direction to BoundingBox::BoundingBox(BoundingBox const&, Direction) (0x101c04380), then floors with (box + position) >> 8 over an inclusive tile rect. 3. That constructor zeroes the destination, writes sentinel 0x80010000 into the destination's orientation word, and dispatches through a jump table whose entry 0 is 0 (read at 0x102d01400) - the identity arm, which copies left_top/right_bottom verbatim. The source orientation is never read; the rotate arm is reachable only for a non-zero Direction. Corroborated by the API mirror: BoundingBox is documented as {MapPosition, MapPosition} or {..., RealOrientation} with orientation OPTIONAL, and OrientedCliffPrototype::collision_bounding_box is a plain BoundingBox. | box | false rej | recall | precision | evidence | | ------------------------------ | --------- | ------ | --------- | ---------- | | AABB (until #88) | 13 | 0.9675 | 0.9743 | assumption | | 45-degree oriented rect (#88) | 0 | 0.9758 | 0.9727 | fit only | | raw stored rect (this) | 6 | 0.9720 | 0.9713 | disasm | The middle row is the trap. It shrank the box past the engine's, and the excess shrinkage absorbed a DIFFERENT defect: 4 of the 6 cliffs the correct box still rejects are cells where our orientation disagrees with the game's, so we load the wrong box entirely. Those 4 belong to the standing orientation residual and should stay visible. A model that scores perfectly by hiding a second bug is worse than one that leaves it exposed. Edges are quantised to 1/256 - MapPosition is 8-bit fixed point, so x_dist's sqrt(2) cannot reach the engine at full precision. test/cliffOrientation.spec.ts asserted the OPPOSITE of this until now, on the stated reasoning that the AABB "is what the engine ends up scanning". That block is rewritten: the shipped box is the raw rectangle, it DEPENDS on intersect (the old file asserted independence), and its area is at most half the AABB's - though it is not contained in it, since a small intersect pushes hx past size/2. An attempt to assert containment on every axis failed for exactly that reason. cliffBoxCoversTile and the separating-axis narrow phase are deleted; the broad phase alone is now the whole rule, which is what the engine does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8243b65 commit 64b5b79

8 files changed

Lines changed: 241 additions & 291 deletions

docs/noise/cliffs-NOTES.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
> | | recall | precision | wrong orientation |
66
> | --- | --- | --- | --- |
77
> | **Nauvis** | 1.0000 | 1.0000 | **0 / 334** |
8-
> | **Vulcanus**, as shipped | **0.9758** | **0.9727** | 2.0% |
8+
> | **Vulcanus**, as shipped | **0.9720** | **0.9713** | ~2.4% |
99
> | **Vulcanus**, no lava rejection | 0.9758 | 0.8719 | 37 / 1531 = 2.4% |
1010
>
11+
> The shipped row went 0.9675 -> 0.9758 -> **0.9720** in one day as the collision
12+
> box was corrected twice. The middle value came from a 45-degree oriented-box
13+
> model (#88) that scored best and was **wrong**; disassembly showed the engine
14+
> discards the box's orientation tag entirely. Do not "restore" the better
15+
> number - see `## The collision box, settled by disassembly` in
16+
> `vulcanus-cliffs-NOTES.md`.
17+
>
1118
> **Read the shipping row.** The renderer applies `tryToAddCliff`'s lava-collision
1219
> rejection and the second row does not; leaving it off is what produced the
1320
> "precision 0.872 / 187-cell excess" figure #84 opened with. The rejection drops

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
> ## STATUS, 2026-08-01: issue #18 is CLOSED; remainder tracked in #84
44
>
5-
> **As shipped**: recall **0.9758**, precision **0.9727**, ratio 1.003. Region
6-
> `[0,0]` reproduces the game's cliff set entirely (recall **1.0000**). Before
5+
> **As shipped**: recall **0.9720**, precision **0.9713**, ratio 1.001. Before
76
> #18's fix: recall 0.806 / 0.938 / 0.853 and 12.5% wrong orientations.
87
>
98
> Two rules do that work and both were found late: `tryToAddCliff`'s
10-
> lava-collision rejection (185 false positives across the three oracle regions,
11-
> precision 0.8719 -> 0.9743) and the fact that its box is a **rotated**
12-
> rectangle rather than its bounding box (recovers 13 real cliffs, recall
13-
> 0.9675 -> 0.9758). See the last two sections.
9+
> lava-collision rejection (185 false positives across the three oracle regions)
10+
> and the shape of its box, which is the **raw stored rectangle** - the engine
11+
> discards the `1/8` orientation tag. See the last three sections, and note the
12+
> LAST one corrects the one before it.
1413
>
1514
> **Root cause: `multisample`'s offsets are in the calling noise program's GRID
1615
> UNITS, not tiles**, so `vulcanus_basalt_lakes_multisample`'s `min` is a 4-tile
@@ -22,7 +21,8 @@
2221
> **Every accuracy table below this banner is the PRE-FIX state**, kept because the
2322
> reasoning is the useful part. Do not quote one as current. The exceptions are
2423
> the last two sections, which are post-fix and are where this banner's numbers
25-
> come from. Note the SECOND of them corrects the first - read both.
24+
> come from. Each of the last three corrects the one before it - read all three,
25+
> in order, or you will act on a superseded number.
2626
2727
Factorio 2.1.12 (build 87038, mac-arm64). Ported 2026-07-26. Companion to
2828
`cliffs-NOTES.md`, which holds the reverse-engineering of the placement rule
@@ -1135,3 +1135,72 @@ loosening that trades precision for recall.
11351135
enrichment), and in every case the game's is a smaller `-to-none` variant of
11361136
ours. A wrong orientation means the wrong box, so the two defects compound.
11371137
Those 6 belong to the standing orientation residual, which is unchanged.
1138+
1139+
## The collision box, settled by disassembly (2026-08-02)
1140+
1141+
The section above is **wrong about the mechanism** and its numbers are
1142+
superseded. It concluded the engine collides against `rotbb`'s rectangle rotated
1143+
45 degrees. It does not. The engine uses the **raw stored rectangle**, and
1144+
discards the orientation tag.
1145+
1146+
### What the binary does
1147+
1148+
Three steps, all in the 2.1.12 arm64 slice:
1149+
1150+
1. `EntityMapGenerationTask::tryToAddCliff` (`0x101625038`) switches on the
1151+
orientation, loads that entry's box from `proto + 0x5c0 + id*0x48` (20 bytes:
1152+
four `int32` edges at `+4`, the orientation word at `+0x14`), and calls
1153+
`wouldCollide` with **`Direction = 0`** - literally `mov x4, #0x0`.
1154+
2. `EntityMapGenerationTask::wouldCollide` (`0x101625468`) forwards box and
1155+
direction to `BoundingBox::BoundingBox(BoundingBox const&, Direction)`
1156+
(`0x101c04380`), then floors the result with `(box + position) >> 8` and scans
1157+
the inclusive tile rectangle against a 96x96 mask grid.
1158+
3. That constructor zeroes the destination, writes the sentinel `0x80010000` into
1159+
the destination's orientation word, and dispatches on the direction through a
1160+
jump table whose **entry 0 is 0** (read at `0x102d01400`) - the identity arm,
1161+
which copies `left_top`/`right_bottom` verbatim and returns. The source box's
1162+
own orientation is never read. The rotation arm below it, which calls
1163+
`Vector2<double, Vector>::rotate(Direction)`, is reachable only for a non-zero
1164+
`Direction`.
1165+
1166+
Corroborated by the API mirror rather than by disassembly alone: `BoundingBox` is
1167+
documented as `{MapPosition, MapPosition}` **or** `{MapPosition, MapPosition,
1168+
RealOrientation}` with `orientation` optional, and
1169+
`OrientedCliffPrototype::collision_bounding_box` is a plain `BoundingBox`.
1170+
Nothing in the docs says collision honours the orientation, and the binary says
1171+
it does not.
1172+
1173+
### Three shapes, and the best-scoring one is wrong
1174+
1175+
| box | false rejections | recall | precision | evidence |
1176+
| --- | --- | --- | --- | --- |
1177+
| AABB `[x, x+size] x [y, y+size]` | 13 | 0.9675 | 0.9743 | none - an assumption |
1178+
| 45-degree oriented rect (#88) | **0** | **0.9758** | 0.9727 | empirical fit only |
1179+
| **raw stored rect (current)** | 6 | 0.9720 | 0.9713 | **disassembly + API docs** |
1180+
1181+
**#88 scored best on every metric and was wrong.** It shrank the box past what
1182+
the engine uses, and the excess shrinkage also absorbed a *different* defect: 4
1183+
of the 6 cliffs the correct box still rejects are cells where our orientation
1184+
disagrees with the game's, so we load the wrong box entirely. Those 4 belong to
1185+
the standing orientation residual and should stay visible.
1186+
1187+
Note the raw rectangle is not simply "smaller". `hx + hy` is fixed at
1188+
`size/2*sqrt2`, so its area is at most half the AABB's - but with a small
1189+
`intersect` it sticks out PAST the AABB in x while collapsing in y. A first
1190+
attempt to assert containment on every axis failed for that reason.
1191+
1192+
Edges are quantised to 1/256 because `MapPosition` is 8-bit fixed point, so
1193+
`x_dist`'s `sqrt(2)` cannot reach the engine at full precision.
1194+
1195+
### The lesson, which is the same one twice in two days
1196+
1197+
**A correction that scores better than the truth is still wrong, and it is
1198+
dangerous precisely because it scores better.** #86 over-reported a collapsing
1199+
gap because the AABB box was over-rejecting; #88 then hit 13/13 by over-shrinking
1200+
and hid four orientation bugs. Both times the flattering number came from a
1201+
too-strong correction. When a fix lands on a metric perfectly, treat that as a
1202+
prompt to find the independent evidence, not as the evidence.
1203+
1204+
The route that worked here was: stop tuning shapes against the metric, and go
1205+
read what the engine does. The binary is unstripped and the whole chain took
1206+
three `lldb` calls.

src/noise/cliffs/cliffCatalog.ts

Lines changed: 52 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -263,38 +263,65 @@ export const CLIFF_CODE_TO_ORIENTATION: Readonly<Record<number, number>> = {
263263
export type CliffCollisionBox = readonly [number, number, number, number];
264264

265265
/**
266-
* `rotbb(x, y, size, intersect)`'s axis-aligned bounding box
266+
* `rotbb(x, y, size, intersect)` as the ENGINE reads it back
267267
* (`base/prototypes/entity/entity-util.lua:9`).
268268
*
269269
* `rotbb` builds a rectangle centred at `(x + size/2, y + size/2)` with
270270
* half-extents `((1 - intersect/size) * d, (intersect/size) * d)` where
271-
* `d = size/2 * sqrt(2)`, and tags it with an orientation of **1/8** - a 45
272-
* degree rotation. Rotating those half-extents by 45 gives an AABB half-extent
273-
* of `(hx + hy) * cos(45) = (d) * cos(45) = size/2` on BOTH axes, whatever
274-
* `intersect` was. So the AABB is exactly the square `[x, x+size] x [y, y+size]`
275-
* and `intersect` only decides how the diagonal is split inside it.
271+
* `d = size/2 * sqrt(2)`, and tags it with an orientation of **1/8**.
276272
*
277-
* **The AABB is the BROAD phase only - `intersect` is load-bearing after all.**
278-
* A note here used to say `intersect` could be dropped because it does not move
279-
* the AABB. That is true of the AABB and false of the collision: `rotbb` tags
280-
* the box with orientation `1/8` and the engine collides against the ROTATED
281-
* rectangle, whose corners the AABB overruns. `intersect` decides how far the
282-
* diagonal is split, hence which corners are empty. See
283-
* {@link CLIFF_ORIENTATION_ROTBB} and {@link cliffBoxCoversTile}; measured in
284-
* `test/cliffOrientedBox.spec.ts`.
273+
* **The 1/8 tag is DISCARDED for collision, so this returns the raw rectangle.**
274+
* Established by disassembly 2026-08-02, three steps deep:
275+
*
276+
* 1. `EntityMapGenerationTask::tryToAddCliff` (`0x101625038`) loads the
277+
* orientation's box from `proto + 0x5c0 + id*0x48`, copies 20 bytes (four
278+
* `int32` edges at `+4` plus the orientation word at `+0x14`), and calls
279+
* `wouldCollide` with **`Direction = 0`** (`mov x4, #0x0`).
280+
* 2. `EntityMapGenerationTask::wouldCollide` (`0x101625468`) forwards that box
281+
* and direction to `BoundingBox::BoundingBox(BoundingBox const&, Direction)`
282+
* (`0x101c04380`).
283+
* 3. That constructor zeroes the destination, writes the sentinel `0x80010000`
284+
* into the destination's orientation word, and dispatches on the direction
285+
* through a jump table whose **entry 0 is 0** - the identity arm, which
286+
* copies `left_top`/`right_bottom` verbatim and returns. The source box's own
287+
* orientation is never read; the rotation arm below it is reached only for a
288+
* non-zero `Direction`.
289+
*
290+
* So the collision rectangle is the stored rectangle, axis-aligned, and the
291+
* tile scan floors it with `(box + position) >> 8` over an inclusive rect.
292+
*
293+
* **Two shapes were shipped here before this and both were wrong.** The AABB
294+
* `[x, x+size] x [y, y+size]` (until #88) is too big at the corners; a 45-degree
295+
* separating-axis test (#88) is too SMALL, and scored better than the truth
296+
* because it also absorbed the unrelated orientation residual. See
297+
* `test/cliffCollisionBox.spec.ts`.
298+
*
299+
* Edges are quantised to 1/256 because `MapPosition` is 8-bit fixed point, so
300+
* `x_dist`'s `sqrt(2)` cannot survive into the engine at full precision.
285301
*/
286-
function rotbbBox(x: number, y: number, size: number): CliffCollisionBox {
287-
return [x, y, x + size, y + size];
302+
function rotbbBox(x: number, y: number, size: number, intersect: number): CliffCollisionBox {
303+
const dist = (size / 2) * SQRT2;
304+
const yRatio = intersect / size;
305+
const xDist = (1 - yRatio) * dist;
306+
const yDist = yRatio * dist;
307+
const cx = x + size / 2;
308+
const cy = y + size / 2;
309+
const q = (v: number): number => Math.round(v * 256) / 256;
310+
return [q(cx - xDist), q(cy - yDist), q(cx + xDist), q(cy + yDist)];
288311
}
289312

313+
/** The four straight orientations, written as plain boxes in the Lua. */
314+
const CLIFF_STRAIGHT_COLLISION_BOX: readonly CliffCollisionBox[] = [
315+
[-2.0, -1.5, 2.0, 1.5], // 0 west-to-east
316+
[-1.0, -2.0, 1.0, 2.0], // 1 north-to-south
317+
[-2.0, -0.5, 2.0, 0.5], // 2 east-to-west
318+
[-1.0, -2.0, 1.0, 2.0], // 3 south-to-north
319+
];
320+
290321
/**
291322
* `rotbb(x, y, size, intersect)`'s four arguments per orientation id, verbatim
292323
* from `create_cliff_data_specification` (`entity-util.lua:85`), or `null` for
293-
* the four straight orientations, whose boxes are written out as plain
294-
* axis-aligned rectangles with no orientation tag.
295-
*
296-
* This exists because {@link CLIFF_ORIENTATION_COLLISION_BOX} is only the
297-
* bounding box. The engine's collision uses the rotated rectangle itself.
324+
* the four straight orientations. Verified identical to the Lua, in order.
298325
*/
299326
export const CLIFF_ORIENTATION_ROTBB: readonly (
300327
| readonly [number, number, number, number]
@@ -324,81 +351,6 @@ export const CLIFF_ORIENTATION_ROTBB: readonly (
324351

325352
const SQRT2 = 1.4142135623730951;
326353

327-
/**
328-
* Does the tile `[tx, tx+1] x [ty, ty+1]` overlap the collision shape of a
329-
* cliff of orientation `id` centred at `(centerX, centerY)`?
330-
*
331-
* For the four straight orientations the shape IS the axis-aligned box, so any
332-
* tile the broad phase enumerated overlaps it and this returns `true`. For the
333-
* sixteen `rotbb` orientations the shape is that rectangle rotated 45 degrees
334-
* clockwise (Factorio orientation `1/8`, and `+y` is south), which the AABB
335-
* overruns at all four corners - a separating-axis test over the two world axes
336-
* and the rectangle's own two decides it.
337-
*
338-
* **Why this is not gold-plating.** Using the AABB drops real cliffs: across the
339-
* three Vulcanus oracle regions the game placed 13 cliffs whose AABB contains
340-
* lava and whose rotated box does not, and it kept every one. Narrowing to the
341-
* oriented rectangle clears **13 of 13** while retaining 182 of the 185
342-
* rejections that were removing genuine false positives - so it is not a
343-
* loosening that trades precision for recall, it is the correct shape.
344-
*/
345-
export function cliffBoxCoversTile(
346-
id: number,
347-
centerX: number,
348-
centerY: number,
349-
tx: number,
350-
ty: number,
351-
): boolean {
352-
const spec = CLIFF_ORIENTATION_ROTBB[id];
353-
if (spec === undefined || spec === null) return true;
354-
const [bx, by, size, intersect] = spec;
355-
const dist = (size / 2) * SQRT2;
356-
const yRatio = intersect / size;
357-
const xDist = (1 - yRatio) * dist;
358-
const yDist = yRatio * dist;
359-
const cx = centerX + bx + size / 2;
360-
const cy = centerY + by + size / 2;
361-
// cos 45 = sin 45; clockwise in screen coords (x east, y south).
362-
const k = Math.SQRT1_2;
363-
const corners: readonly (readonly [number, number])[] = [
364-
[-xDist, -yDist],
365-
[xDist, -yDist],
366-
[xDist, yDist],
367-
[-xDist, yDist],
368-
].map(([u, v]) => [cx + (u - v) * k, cy + (u + v) * k] as const);
369-
const square: readonly (readonly [number, number])[] = [
370-
[tx, ty],
371-
[tx + 1, ty],
372-
[tx + 1, ty + 1],
373-
[tx, ty + 1],
374-
];
375-
const axes: readonly (readonly [number, number])[] = [
376-
[1, 0],
377-
[0, 1],
378-
[k, k],
379-
[-k, k],
380-
];
381-
for (const [ax, ay] of axes) {
382-
let aMin = Infinity;
383-
let aMax = -Infinity;
384-
let bMin = Infinity;
385-
let bMax = -Infinity;
386-
for (const [px, py] of corners) {
387-
const d = px * ax + py * ay;
388-
if (d < aMin) aMin = d;
389-
if (d > aMax) aMax = d;
390-
}
391-
for (const [px, py] of square) {
392-
const d = px * ax + py * ay;
393-
if (d < bMin) bMin = d;
394-
if (d > bMax) bMax = d;
395-
}
396-
// Touching is not overlapping: a tile the rectangle only grazes is free.
397-
if (aMax <= bMin || bMax <= aMin) return false;
398-
}
399-
return true;
400-
}
401-
402354
/**
403355
* `CliffOrientation` id -> the orientation's `collision_bounding_box`, at
404356
* `scale = 1.0` (both `cliff` and `cliff-vulcanus`), relative to the cliff's
@@ -427,28 +379,10 @@ export function cliffBoxCoversTile(
427379
* 0.779) to 888 predicted (ratio **1.003**, precision **0.930**), rejecting 173
428380
* false positives and only 4 true ones. See issue #18.
429381
*/
430-
export const CLIFF_ORIENTATION_COLLISION_BOX: readonly CliffCollisionBox[] = [
431-
[-2.0, -1.5, 2.0, 1.5], // 0 west-to-east
432-
[-1.0, -2.0, 1.0, 2.0], // 1 north-to-south
433-
[-2.0, -0.5, 2.0, 0.5], // 2 east-to-west
434-
[-1.0, -2.0, 1.0, 2.0], // 3 south-to-north
435-
rotbbBox(-3.5, -3, 4.5), // 4 west-to-north
436-
rotbbBox(-1, -3, 4.5), // 5 north-to-east
437-
rotbbBox(-1, -0.5, 3.5), // 6 east-to-south
438-
rotbbBox(-2.5, -0.5, 3.5), // 7 south-to-west
439-
rotbbBox(-3.5, -1.5, 4.5), // 8 west-to-south
440-
rotbbBox(-2.5, -3, 3.5), // 9 north-to-west
441-
rotbbBox(-1, -3, 3.5), // 10 east-to-north
442-
rotbbBox(-1, -1.5, 4.5), // 11 south-to-east
443-
rotbbBox(-3, -1.5, 3), // 12 west-to-none
444-
rotbbBox(0, -1.5, 3), // 13 none-to-east
445-
rotbbBox(0, -0.5, 2.5), // 14 east-to-none
446-
rotbbBox(-2.5, -0.5, 2.51), // 15 none-to-west
447-
rotbbBox(-1, -2.5, 3), // 16 north-to-none
448-
rotbbBox(-1, -0.5, 3), // 17 none-to-south
449-
rotbbBox(-2, -0.5, 3), // 18 south-to-none
450-
rotbbBox(-2, -2.5, 3), // 19 none-to-north
451-
];
382+
export const CLIFF_ORIENTATION_COLLISION_BOX: readonly CliffCollisionBox[] =
383+
CLIFF_ORIENTATION_ROTBB.map((spec, id) =>
384+
spec === null ? CLIFF_STRAIGHT_COLLISION_BOX[id] : rotbbBox(...spec),
385+
);
452386

453387
/**
454388
* The `CliffOrientation` id a cell code places, or `undefined` when the code

src/noise/cliffs/cliffPlacement.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import {
1313
CLIFF_CELL_CENTER_X,
1414
CLIFF_CELL_CENTER_Y,
1515
CLIFF_GRID_SIZE,
16-
cliffBoxCoversTile,
1716
cliffCollisionTileBox,
18-
cliffOrientationForCode,
1917
getModifiedElevationInterval,
2018
isCliffPlaced,
2119
} from "./cliffCatalog";
@@ -321,24 +319,18 @@ export function makeCliffPlacementFromFields(
321319
* the orientation's collision box and drop the cell if any tile in it collides.
322320
* With no `tileCollides` supplied this is a constant `false` and costs nothing.
323321
*
324-
* **Two phases, because sixteen of the twenty boxes are rotated.**
325-
* `cliffCollisionTileBox` is the BROAD phase - the axis-aligned tile rectangle
326-
* `wouldCollide` derives with `(box + position) >> 8`. For the four straight
327-
* orientations that is the whole shape. For the sixteen `rotbb` ones the real
328-
* shape is that rectangle turned 45 degrees, so `cliffBoxCoversTile` runs a
329-
* narrow phase and discards the AABB's four empty corners. Skipping it drops
330-
* 13 real Vulcanus cliffs whose corners happen to overhang lava.
322+
* The box is `cliffCollisionTileBox` and nothing narrows it: `wouldCollide`
323+
* floors the stored rectangle with `(box + position) >> 8` and scans the
324+
* inclusive tile rect, with the box's own `1/8` orientation tag discarded.
325+
* See `rotbbBox` in `cliffCatalog.ts` for the disassembly that establishes it.
331326
*/
332327
const rejected = (code: number, x: number, y: number): boolean => {
333328
if (tileCollides === undefined) return false;
334329
const box = cliffCollisionTileBox(code, x, y);
335330
// `undefined` only for a code that places nothing, which cannot reach here.
336331
if (box === undefined) return false;
337-
const id = cliffOrientationForCode(code);
338-
if (id === undefined) return false;
339332
for (let tx = box.left; tx <= box.right; tx++)
340-
for (let ty = box.top; ty <= box.bottom; ty++)
341-
if (tileCollides(tx, ty) && cliffBoxCoversTile(id, x, y, tx, ty)) return true;
333+
for (let ty = box.top; ty <= box.bottom; ty++) if (tileCollides(tx, ty)) return true;
342334
return false;
343335
};
344336

0 commit comments

Comments
 (0)