3030//! [`super::connections`] are derived from these names, which is what makes a
3131//! transcription slip fail rather than shift the model.
3232//! - [`CLIFF_ORIENTATION_COLLISION_BOX`] is the table the engine loads into
33- //! `proto + 0x5c0 + id * 0x48`, and `tryToAddCliff` hands it to `wouldCollide`
34- //! with `Direction = 0` - the identity arm, which copies the rectangle
35- //! verbatim and discards `rotbb`'s `1/8` orientation tag. So the collision
36- //! shape is the RAW stored rectangle, not a rotated one.
33+ //! `proto + 0x5c0 + id * 0x48` (`0x590` on 2.0.77). `tryToAddCliff` - the map
34+ //! PREVIEW generator - hands it to `wouldCollide` with `Direction = 0` and
35+ //! discards `rotbb`'s `1/8` tag. The path that PLACES cliffs,
36+ //! `EntityMapGenerationTask::applyCliffs`, does not: the box goes to
37+ //! `Surface::wouldCollide` WITH its orientation word, and the collision shape
38+ //! is the rotated rectangle. That test lives in [`super::collision`] and is
39+ //! graded against the game's own calls; see #407.
3740//!
3841//! ## The boxes are literals here and computed in the TypeScript
3942//!
4851//! full precision. Shipping the quantised values means the render path does no
4952//! floating-point rounding for the boxes at all, while the derivation stays
5053//! checkable against the Lua it came from.
54+ //!
55+ //! **The quantisation is TRUNCATION toward zero, not rounding.** Measured
56+ //! against the boxes the engine tested on every recorded call (#407): `trunc`
57+ //! reproduces all 16 corner boxes, `Math.round` misses 15 by one unit on one
58+ //! to three edges, and floor, ceil and round-half-even miss all 16. The table
59+ //! shipped rounded from 2026-08-02 to #407.
5160
5261/// Default `cliff_elevation_0` map-gen setting.
5362pub const CLIFF_ELEVATION_0_DEFAULT : f64 = 10.0 ;
@@ -308,26 +317,26 @@ pub type CliffCollisionBox = [f64; 4];
308317/// multiple of `1/256` - see the module docs for why that is the format rather
309318/// than a coincidence.
310319pub const CLIFF_ORIENTATION_COLLISION_BOX : [ CliffCollisionBox ; 20 ] = [
311- [ -2.0 , -1.5 , 2.0 , 1.5 ] , // 0 west-to-east
312- [ -1.0 , -2.0 , 1.0 , 2.0 ] , // 1 north-to-south
313- [ -2.0 , -0.5 , 2.0 , 0.5 ] , // 2 east-to-west
314- [ -1.0 , -2.0 , 1.0 , 2.0 ] , // 3 south-to-north
315- [ -2.3125 , -2.87109375 , -0.1875 , 1.37109375 ] , // 4 west-to-north
316- [ -0.87109375 , -1.8125 , 3.37109375 , 0.3125 ] , // 5 north-to-east
317- [ 0.04296875 , -0.51953125 , 1.45703125 , 3.01953125 ] , // 6 east-to-south
318- [ -2.51953125 , 0.54296875 , 1.01953125 , 1.95703125 ] , // 7 south-to-west
319- [ -3.37109375 , -0.3125 , 0.87109375 , 1.8125 ] , // 8 west-to-south
320- [ -1.45703125 , -3.01953125 , -0.04296875 , 0.51953125 ] , // 9 north-to-west
321- [ -1.01953125 , -1.95703125 , 2.51953125 , -0.54296875 ] , // 10 east-to-north
322- [ 0.1875 , -1.37109375 , 2.3125 , 2.87109375 ] , // 11 south-to-east
323- [ -2.20703125 , -1.4140625 , -0.79296875 , 1.4140625 ] , // 12 west-to-none
324- [ 0.0859375 , -0.70703125 , 2.9140625 , 0.70703125 ] , // 13 none-to-east
325- [ 0.89453125 , -0.6640625 , 1.60546875 , 2.1640625 ] , // 14 east-to-none
326- [ -2.66796875 , 0.40234375 , 0.17578125 , 1.109375 ] , // 15 none-to-west
327- [ -0.9140625 , -1.70703125 , 1.9140625 , -0.29296875 ] , // 16 north-to-none
328- [ 0.14453125 , -0.76953125 , 0.85546875 , 2.76953125 ] , // 17 none-to-south
329- [ -2.26953125 , 0.64453125 , 1.26953125 , 1.35546875 ] , // 18 south-to-none
330- [ -1.20703125 , -2.4140625 , 0.20703125 , 0.4140625 ] , // 19 none-to-north
320+ [ -2.0 , -1.5 , 2.0 , 1.5 ] , // 0 west-to-east
321+ [ -1.0 , -2.0 , 1.0 , 2.0 ] , // 1 north-to-south
322+ [ -2.0 , -0.5 , 2.0 , 0.5 ] , // 2 east-to-west
323+ [ -1.0 , -2.0 , 1.0 , 2.0 ] , // 3 south-to-north
324+ [ -2.30859375 , -2.87109375 , -0.1875 , 1.37109375 ] , // 4 west-to-north
325+ [ -0.87109375 , -1.80859375 , 3.37109375 , 0.30859375 ] , // 5 north-to-east
326+ [ 0.0390625 , -0.515625 , 1.45703125 , 3.015625 ] , // 6 east-to-south
327+ [ -2.515625 , 0.5390625 , 1.015625 , 1.95703125 ] , // 7 south-to-west
328+ [ -3.37109375 , -0.30859375 , 0.87109375 , 1.80859375 ] , // 8 west-to-south
329+ [ -1.45703125 , -3.015625 , -0.0390625 , 0.515625 ] , // 9 north-to-west
330+ [ -1.015625 , -1.95703125 , 2.515625 , -0.5390625 ] , // 10 east-to-north
331+ [ 0.1875 , -1.37109375 , 2.30859375 , 2.87109375 ] , // 11 south-to-east
332+ [ -2.20703125 , -1.4140625 , -0.7890625 , 1.4140625 ] , // 12 west-to-none
333+ [ 0.08203125 , -0.70703125 , 2.9140625 , 0.70703125 ] , // 13 none-to-east
334+ [ 0.89453125 , -0.6640625 , 1.6015625 , 2.1640625 ] , // 14 east-to-none
335+ [ -2.6640625 , 0.3984375 , 0.17578125 , 1.10546875 ] , // 15 none-to-west
336+ [ -0.9140625 , -1.70703125 , 1.9140625 , -0.2890625 ] , // 16 north-to-none
337+ [ 0.14453125 , -0.765625 , 0.8515625 , 2.765625 ] , // 17 none-to-south
338+ [ -2.265625 , 0.64453125 , 1.265625 , 1.3515625 ] , // 18 south-to-none
339+ [ -1.20703125 , -2.4140625 , 0.20703125 , 0.4140625 ] , // 19 none-to-north
331340] ;
332341
333342/// The four straight orientations, written as plain boxes in the Lua rather
@@ -379,35 +388,18 @@ pub const CLIFF_ORIENTATION_ROTBB: [Option<[f64; 4]>; 20] = [
379388/// `tests::the_square_root_constant_is_the_one_the_typescript_writes` pins it.
380389const SQRT2 : f64 = std:: f64:: consts:: SQRT_2 ;
381390
382- /// `Math.round`, which is NOT `f64::round`.
383- ///
384- /// JavaScript rounds a half UP (toward `+inf`), so `Math.round(-0.5)` is `-0`;
385- /// Rust rounds a half AWAY FROM ZERO, so `(-0.5f64).round()` is `-1`.
386- ///
387- /// Every edge below is far from a half in practice - `rotbb`'s `sqrt(2)` sees
388- /// to that - but "in practice" is not a reason to write the other function, and
389- /// `tests::the_rounding_is_javascripts_and_not_rusts` plants the case that
390- /// separates them.
391- #[ inline]
392- fn js_round ( v : f64 ) -> f64 {
393- ( v + 0.5 ) . floor ( )
394- }
395-
396391/// `rotbb(x, y, size, intersect)` as the ENGINE reads it back
397392/// (`entity-util.lua:9`), returning the RAW rectangle.
398393///
399394/// `rotbb` builds a rectangle centred at `(x + size/2, y + size/2)` with
400395/// half-extents `((1 - intersect/size) * d, (intersect/size) * d)` where
401- /// `d = size/2 * sqrt(2)`, and tags it with an orientation of `1/8`. **The tag
402- /// is discarded for collision** - three steps of disassembly establish it, and
403- /// the module docs name them - so this returns the rectangle unrotated .
396+ /// `d = size/2 * sqrt(2)`, and tags it with an orientation of `1/8`. This
397+ /// returns the rectangle unrotated; the tag is [`super::collision::ROTBB_TAG`],
398+ /// and the placement path applies it (#407) .
404399///
405- /// Two wrong shapes shipped in the TypeScript before this one, and the more
406- /// accurate-looking of them was the wrong one: a 45-degree separating-axis test
407- /// scored better on every metric because it also absorbed an unrelated
408- /// orientation defect. See `test/cliffCollisionBox.spec.ts`.
409- ///
410- /// Edges are quantised to `1/256` because `MapPosition` is 8-bit fixed point.
400+ /// Edges are quantised to `1/256` by truncation toward zero, because
401+ /// `MapPosition` is 8-bit fixed point and that is what the loader does - see
402+ /// the module docs.
411403#[ must_use]
412404pub fn rotbb_box ( x : f64 , y : f64 , size : f64 , intersect : f64 ) -> CliffCollisionBox {
413405 let dist = ( size / 2.0 ) * SQRT2 ;
@@ -416,7 +408,7 @@ pub fn rotbb_box(x: f64, y: f64, size: f64, intersect: f64) -> CliffCollisionBox
416408 let y_dist = y_ratio * dist;
417409 let cx = x + size / 2.0 ;
418410 let cy = y + size / 2.0 ;
419- let q = |v : f64 | js_round ( v * 256.0 ) / 256.0 ;
411+ let q = |v : f64 | ( v * 256.0 ) . trunc ( ) / 256.0 ;
420412 [
421413 q ( cx - x_dist) ,
422414 q ( cy - y_dist) ,
@@ -434,8 +426,12 @@ pub struct CliffTileBox {
434426 pub bottom : i64 ,
435427}
436428
437- /// The tile rectangle `EntityMapGenerationTask::wouldCollide` scans for a cliff
438- /// of cell `code` centred at `(center_x, center_y)`.
429+ /// The tile rectangle `EntityMapGenerationTask::wouldCollide` - the map PREVIEW
430+ /// path's test - scans for a cliff of cell `code` centred at
431+ /// `(center_x, center_y)`. The placement path scans `getAABB`'s square instead
432+ /// and then tests each tile against the oriented box; see
433+ /// [`super::collision`]. Kept as the "before" model the game's recorded calls
434+ /// are graded against.
439435///
440436/// Both ends are **inclusive** and both come from a **floor**, because the
441437/// engine works in `MapPosition`'s 8-bit fixed point and takes
@@ -525,8 +521,6 @@ mod tests {
525521 }
526522 }
527523
528- /// `Math.round` and `f64::round` disagree on a negative half, and this port
529- /// needs JavaScript's. Planted, because no real box edge lands on one.
530524 /// The TypeScript writes `1.4142135623730951`; Rust's constant must be the
531525 /// same bits, or `rotbb_box` evaluates different arithmetic.
532526 ///
@@ -539,13 +533,19 @@ mod tests {
539533 assert_eq ! ( SQRT2 . to_bits( ) , 1.414_213_562_373_095_1_f64 . to_bits( ) ) ;
540534 }
541535
536+ /// Truncation, not rounding: planted on the one edge where the two differ
537+ /// by a whole unit, and on a negative edge, where truncation and floor
538+ /// differ. `south-to-east`'s right edge is `2.30859375` truncated and
539+ /// `2.3125` rounded; the engine holds the former on every recorded call.
542540 #[ test]
543- fn the_rounding_is_javascripts_and_not_rusts ( ) {
544- assert_eq ! ( js_round( -0.5 ) , 0.0 ) ;
545- assert_eq ! ( ( -0.5f64 ) . round( ) , -1.0 ) ;
546- assert_eq ! ( js_round( 0.5 ) , 1.0 ) ;
547- assert_eq ! ( js_round( 1.5 ) , 2.0 ) ;
548- assert_eq ! ( js_round( -1.5 ) , -1.0 ) ;
541+ fn the_quantisation_truncates_toward_zero ( ) {
542+ let b = rotbb_box ( -1.0 , -1.5 , 4.5 , 3.0 ) ;
543+ assert_eq ! ( b[ 2 ] , 2.308_593_75 ) ;
544+ assert_eq ! ( b[ 0 ] , 0.1875 ) ;
545+ assert_eq ! (
546+ b[ 1 ] , -1.371_093_75 ,
547+ "a negative edge truncates toward zero, not down"
548+ ) ;
549549 }
550550
551551 /// The floor is inclusive at both ends, so a straight orientation's 4-tile
0 commit comments