From 95f1f730463b6a5a9abd78d41b12a36467c252cc Mon Sep 17 00:00:00 2001 From: Sirac Ozmen Date: Thu, 5 Nov 2026 12:22:45 +0300 Subject: [PATCH 1/3] test(world): cover a blast inside a shape and null-world readers --- test/test_world.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/test_world.c b/test/test_world.c index a77d12b..231a184 100644 --- a/test/test_world.c +++ b/test/test_world.c @@ -1384,6 +1384,53 @@ static void TestDormancyMassAndExplosions(void) m2DestroyWorld(world); } +// A blast whose center lies inside a shape pushes the body away from its +// center of mass, through the blast point; one at the center of mass has +// no direction to push in and leaves the body as it was. +static void TestBlastInsideAShape(void) +{ + m2WorldDef def = m2DefaultWorldDef(); + def.bodyCapacity = 2; + def.shapeCapacity = 2; + def.gravity = (m2Vec2){0.0f, 0.0f}; + m2World* world = NewWorld(&def); + m2BodyDef bd = m2DefaultBodyDef(); + bd.type = m2_dynamicBody; + m2BodyId box = NewBody(world, &bd); + m2ShapeDef sd = m2DefaultShapeDef(); + m2Polygon unit = m2MakeBox(0.5f, 0.5f); + NewPolygonShape(world, box, &sd, &unit); + m2ExplosionDef boom = m2DefaultExplosionDef(); + boom.position = (m2Pos2){0.25, 0.0}; + m2World_Explode(world, &boom); + m2Vec2 v = m2Body_GetLinearVelocity(world, box); + CHECK(v.x < -0.5f && v.y == 0.0f && m2Body_GetAngularVelocity(world, box) == 0.0f, + "a blast inside the box, right of its center, pushes it straight left"); + m2Body_SetLinearVelocity(world, box, (m2Vec2){0.0f, 0.0f}); + uint64_t hash = m2World_Hash(world); + boom.position = (m2Pos2){0.0, 0.0}; + m2World_Explode(world, &boom); + v = m2Body_GetLinearVelocity(world, box); + CHECK(v.x == 0.0f && v.y == 0.0f && m2Body_GetAngularVelocity(world, box) == 0.0f && + m2World_Hash(world) == hash, + "a blast at its center of mass leaves it still"); + m2DestroyWorld(world); +} + +// The world readers that take a null world read zero. +static void TestNullWorldReadsZero(void) +{ + m2Profile profile = m2World_GetProfile(NULL); + m2Counters counters = m2World_GetCounters(NULL); + CHECK(profile.step == 0.0f && profile.broadphase == 0.0f && profile.narrowphase == 0.0f && + profile.solve == 0.0f && profile.sleep == 0.0f, + "a null world's profile is zeroed"); + CHECK(m2World_GetKineticEnergy(NULL) == 0.0, "a null world has no kinetic energy"); + CHECK(counters.bodyCount == 0 && counters.shapeCount == 0 && counters.jointCount == 0 && + counters.misuseCount == 0, + "a null world's counters are zeroed"); +} + // Small-basket readers and mutators (parity sprint 4b): frame // helpers, the per-body joint walk, runtime geometry, and chain // materials. @@ -4976,6 +5023,8 @@ static void TestJointEdgesThatWrapAreRefused(void) int main(void) { + TestBlastInsideAShape(); + TestNullWorldReadsZero(); TestReplayScratchRefusedIsCapacity(); TestJointEdgesThatWrapAreRefused(); TestHashSeesTheBroadphase(); From 54e418f0192060283ab46f27d6f424bd1fdaa1ef Mon Sep 17 00:00:00 2001 From: Sirac Ozmen Date: Thu, 5 Nov 2026 13:13:09 +0300 Subject: [PATCH 2/3] docs(world): say how a blast inside a shape pushes it --- docs/user/bodies-and-shapes.md | 2 +- include/maul2d/world.h | 4 +++- test/test_world.c | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/user/bodies-and-shapes.md b/docs/user/bodies-and-shapes.md index 1be7fda..ad03064 100644 --- a/docs/user/bodies-and-shapes.md +++ b/docs/user/bodies-and-shapes.md @@ -71,4 +71,4 @@ m2Result broke = m2Body_Shatter(world, wall, pieces, pieceCount, fragments, 8, & ## Explosions -`m2World_Explode` is one blast: every dynamic shape within reach gets an outward impulse at its closest point, at full strength inside the radius and fading to zero across the falloff band, filtered by category. It is one journal operation. +`m2World_Explode` is one blast: every dynamic shape within reach gets an outward impulse at its closest point, at full strength inside the radius and fading to zero across the falloff band, filtered by category. A shape with the blast inside it is pushed out through its body's center of mass; a blast at the center of mass itself pushes nothing. It is one journal operation. diff --git a/include/maul2d/world.h b/include/maul2d/world.h index cbeaf73..9c1a079 100644 --- a/include/maul2d/world.h +++ b/include/maul2d/world.h @@ -262,7 +262,9 @@ extern "C" /// One blast. Every dynamic, non-sensor shape whose category passes /// maskBits and whose closest point lies within radius + falloff gets /// an outward impulse at that point: full strength inside radius, - /// fading to zero across the falloff band. + /// fading to zero across the falloff band. A shape with the blast inside + /// it is pushed out through its body's center of mass; a blast at the + /// center of mass itself pushes nothing. typedef struct m2ExplosionDef { m2Pos2 position; diff --git a/test/test_world.c b/test/test_world.c index 231a184..acedaae 100644 --- a/test/test_world.c +++ b/test/test_world.c @@ -1384,9 +1384,8 @@ static void TestDormancyMassAndExplosions(void) m2DestroyWorld(world); } -// A blast whose center lies inside a shape pushes the body away from its -// center of mass, through the blast point; one at the center of mass has -// no direction to push in and leaves the body as it was. +// A shape with the blast inside it is pushed out through its body's +// center of mass; a blast at the center of mass itself pushes nothing. static void TestBlastInsideAShape(void) { m2WorldDef def = m2DefaultWorldDef(); From 002ff17d8a85b74c2e8ca3fea698a2a27bd25ca3 Mon Sep 17 00:00:00 2001 From: Sirac Ozmen Date: Thu, 5 Nov 2026 13:58:34 +0300 Subject: [PATCH 3/3] docs(world): bound the inside-blast rule to what the code does --- docs/user/bodies-and-shapes.md | 2 +- include/maul2d/world.h | 5 +++-- test/test_world.c | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/user/bodies-and-shapes.md b/docs/user/bodies-and-shapes.md index ad03064..8b06b65 100644 --- a/docs/user/bodies-and-shapes.md +++ b/docs/user/bodies-and-shapes.md @@ -71,4 +71,4 @@ m2Result broke = m2Body_Shatter(world, wall, pieces, pieceCount, fragments, 8, & ## Explosions -`m2World_Explode` is one blast: every dynamic shape within reach gets an outward impulse at its closest point, at full strength inside the radius and fading to zero across the falloff band, filtered by category. A shape with the blast inside it is pushed out through its body's center of mass; a blast at the center of mass itself pushes nothing. It is one journal operation. +`m2World_Explode` is one blast: every dynamic shape within reach gets an outward impulse at its closest point, at full strength inside the radius and fading to zero across the falloff band, filtered by category. A shape with the blast inside it, its rounding included, is pushed in the direction from the blast to its body's center of mass; a blast inside it at the center of mass itself pushes nothing. It is one journal operation. diff --git a/include/maul2d/world.h b/include/maul2d/world.h index 9c1a079..0c160c9 100644 --- a/include/maul2d/world.h +++ b/include/maul2d/world.h @@ -263,8 +263,9 @@ extern "C" /// maskBits and whose closest point lies within radius + falloff gets /// an outward impulse at that point: full strength inside radius, /// fading to zero across the falloff band. A shape with the blast inside - /// it is pushed out through its body's center of mass; a blast at the - /// center of mass itself pushes nothing. + /// it, its rounding included, is pushed in the direction from the blast + /// to its body's center of mass; a blast inside it at the center of mass + /// itself pushes nothing. typedef struct m2ExplosionDef { m2Pos2 position; diff --git a/test/test_world.c b/test/test_world.c index acedaae..7c15266 100644 --- a/test/test_world.c +++ b/test/test_world.c @@ -1384,8 +1384,9 @@ static void TestDormancyMassAndExplosions(void) m2DestroyWorld(world); } -// A shape with the blast inside it is pushed out through its body's -// center of mass; a blast at the center of mass itself pushes nothing. +// A shape with the blast inside it, its rounding included, is pushed in +// the direction from the blast to its body's center of mass; a blast +// inside it at the center of mass itself pushes nothing. static void TestBlastInsideAShape(void) { m2WorldDef def = m2DefaultWorldDef();