Say what was hit, which way is out, and how far

The narrowphase has been producing a contact since it went in, and the interpreter
was throwing it away. `RCOLLISION(n, f)` reports it: what was hit (a sprite or a
`SOLID` rectangle), which one, the contact normal, the penetration depth, the
contact point, and which axis to reverse.

**The normal points out of the other thing and toward this one**, so a program
moves along it by the depth and is exactly clear. That sign is the one assertion
in the new test that could not be caught any other way -- both parties of a
sprite-against-sprite hit get their own record, each pointing the way *that*
sprite has to move, and sharing one would tell both to go the same direction,
which is how two things end up stuck inside each other.

**Field 7 is the one that deletes the most BASIC.** It is the minimum translation
axis, computed from the normal in C, and it is there because doing it in BASIC
means comparing two floats -- which is exactly where this dialect's left-operand
rule catches people. `BALLBRICKS`/`TESTCELL` in the artwork breakout spend six
lines computing an overlap rectangle and comparing its width to its height to get
this number.

The record is **sticky and deepest-wins**: replaced whenever that sprite is in a
contact and otherwise left alone, so `BUMP` stays the event and this stays the
detail of it. Making it clear itself when nothing touches would break the pairing,
because `BUMP` accumulates across steps and a once-a-frame poll would find the
detail already gone. Reading `BUMP` clears both, so they cannot disagree.

Deliberately narrower than `akgl_Contact`: no actor pointers, because BASIC has no
actor; no tile fields, because there is no tilemap; no z, because every test is
planar; and **no `dt` and no `sensor`**, which libakgl documents as filled in by
the resolver. This interpreter never resolves anything, so those two come back
zero and mean nothing, and an always-zero field in a reference table is a lie.

Documented with the two caveats that matter: fields 2, 3 and 4 are floats and want
a `%` variable, and the contact *point* is exact only for two boxes -- libakgl's
solver returns a point on the portal it converged to, while the normal and depth
are exact for every pair.

Chapter 8's collision section stops claiming only type 1 exists, which has been
false since the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
2026-08-02 10:53:26 -04:00
parent 5709dc160c
commit a9600c3fcc
12 changed files with 401 additions and 4 deletions

View File

@@ -1069,6 +1069,99 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief The contact says which way to push out and how far.
*
* The reason for going through libakgl's narrowphase at all. Four comparisons
* can say *that* two things overlap; only a narrowphase can say by how much and
* in which direction, and that is what a game needs to reverse a ball or stop a
* player at a wall.
*
* **The normal's sign is the assertion worth having.** It points out of the other
* shape and toward this one, so a program moves along it by the depth and is
* clear. Getting it backwards is the classic mistake and nothing else here would
* notice.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_contact_geometry(void)
{
PREPARE_ERROR(errctx);
akbasic_Contact contact;
uint16_t mask = 0;
bool found = false;
/*
* A 24x21 sprite at (100, 100), and a wall whose left edge is at 118. They
* overlap by six pixels along x and by far more along y, so the minimum
* translation axis is x and the sprite has to move left to clear it.
*/
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 SOLID 3, 118, 0, 300, 300\n"
"80 MOVSPR 1, 100, 100\n", &mask));
TEST_REQUIRE_INT(mask, 0x01);
memset(&contact, 0, sizeof(contact));
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
TEST_REQUIRE(found, "sprite 1 overlapped a wall, so it should have a contact");
TEST_REQUIRE_INT(contact.what, 2);
TEST_REQUIRE_INT(contact.other, 3);
TEST_REQUIRE_INT(contact.axis, 1);
/* Six pixels of overlap: 100 + 24 = 124, less the wall's 118. */
TEST_REQUIRE(contact.depth > 5.5 && contact.depth < 6.5,
"the overlap is six pixels, got %f", contact.depth);
/* Out of the wall and toward the sprite is leftward, so negative x. */
TEST_REQUIRE(contact.nx < -0.5,
"the normal should point away from the wall, got (%f, %f)",
contact.nx, contact.ny);
stop_runtime();
/* The same overlap from above: the axis becomes y and the normal flips. */
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 SPRSAV P#, 1\n"
"60 SPRITE 1, 1\n"
"70 SOLID 3, 0, 118, 300, 300\n"
"80 MOVSPR 1, 100, 100\n", &mask));
TEST_REQUIRE_INT(mask, 0x01);
memset(&contact, 0, sizeof(contact));
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
TEST_REQUIRE(found, "sprite 1 should have a contact");
TEST_REQUIRE_INT(contact.axis, 2);
TEST_REQUIRE(contact.ny < -0.5,
"the normal should point up out of the wall, got (%f, %f)",
contact.nx, contact.ny);
stop_runtime();
/*
* Sprite against sprite, and **both** get a contact pointing the way *they*
* have to move. Sharing one normal would tell both to go the same direction,
* which is how two things end up stuck inside each other.
*/
PASS(errctx, two_sprites("90 MOVSPR 1, 100, 100\n"
"100 MOVSPR 2, 118, 100\n", &mask));
TEST_REQUIRE_INT(mask, 0x03);
memset(&contact, 0, sizeof(contact));
PASS(errctx, SPRITES.contact(&SPRITES, 1, &contact, &found));
TEST_REQUIRE(found, "sprite 1 should have a contact");
TEST_REQUIRE_INT(contact.what, 1);
TEST_REQUIRE_INT(contact.other, 2);
TEST_REQUIRE(contact.nx < -0.5, "sprite 1 is on the left, so it moves left");
memset(&contact, 0, sizeof(contact));
PASS(errctx, SPRITES.contact(&SPRITES, 2, &contact, &found));
TEST_REQUIRE(found, "sprite 2 should have a contact too");
TEST_REQUIRE_INT(contact.other, 1);
TEST_REQUIRE(contact.nx > 0.5, "sprite 2 is on the right, so it moves right");
stop_runtime();
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -1139,6 +1232,7 @@ int main(void)
CATCH(errctx, test_collision_cross_shape());
CATCH(errctx, test_sprite_shapes());
CATCH(errctx, test_static_geometry());
CATCH(errctx, test_contact_geometry());
CATCH(errctx, test_drawing_layer_persists());
} CLEANUP {
if ( font != NULL ) {