diff --git a/docs/user/bodies-and-shapes.md b/docs/user/bodies-and-shapes.md index 1be7fda..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. 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 cbeaf73..0c160c9 100644 --- a/include/maul2d/world.h +++ b/include/maul2d/world.h @@ -262,7 +262,10 @@ 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, 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 14ba8cd..fcfe098 100644 --- a/test/test_world.c +++ b/test/test_world.c @@ -1384,6 +1384,53 @@ static void TestDormancyMassAndExplosions(void) m2DestroyWorld(world); } +// 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(); + 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. @@ -5004,6 +5051,8 @@ static void TestJointEdgesThatWrapAreRefused(void) int main(void) { + TestBlastInsideAShape(); + TestNullWorldReadsZero(); TestClosestPointFromInside(); TestReplayScratchRefusedIsCapacity(); TestJointEdgesThatWrapAreRefused();