Add collision shapes, on the character and overridable per actor
A shape is a convex volume positioned relative to an actor's origin. It lives on
akgl_Character, so every goblin sharing a character shares one shape definition
the way they already share speeds and the state-to-sprite map, and an actor may
override it with akgl_Actor::shape_override.
The flag is not redundant with an empty shape. "This actor deliberately has no
collider" and "this actor has not been given one yet" are different states, and
without somewhere to record the difference akgl_actor_set_character cannot tell
whether it is allowed to overwrite what it finds.
include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete
struct pointers rather than including their headers, because both of those need
akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite
compiles it as the first include of a translation unit, so that property is
enforced rather than merely intended -- which is why the tilemap types got struct
tags two commits ago.
The masks are asymmetric and the defaults are the load-bearing part: layermask
ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with
map geometry and with no other actor. "Everything with a hitbox shoves everything
else" would be a surprising default for a town full of scenery and a painful one
to discover after the fact; opting in to actor-versus-actor is one bit, opting
out of it would have been a hunt through every NPC a game spawns.
Every shape gets a z depth, because the narrowphase behind this is three
dimensional and answers with the *minimum* separating translation. A thin
extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a
player can see while remaining inside the floor -- a collision system reporting
success while doing nothing. The ratio of 2 is the smallest for which the z
overlap of any two shapes the setters build provably exceeds any planar
penetration they can reach.
The test for that asserts the inequality across a spread of sizes rather than
asserting the constant is 2, so a change to the constant fails here rather than
in a game.
Two things about that test are worth recording, because the first version had
both:
- It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by
`break`ing, and the assertion was inside a nested `for` -- so the break left
the loop rather than the ATTEMPT block and the failure evaporated. This is the
hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified
by setting the ratio to 0.5 and watching the suite still pass, then fixing it
and watching it report the 512x512 case by name.
- tests/collision_arena.c had the same bug in a loop added in the previous
commit, and it is fixed here too.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:48:05 -04:00
/**
* @ file collision . h
* @ brief Collision shapes : what an actor occupies , and what it will collide with .
*
* A shape is a convex volume positioned relative to an actor ' s origin . It lives
* on the akgl_Character , so every goblin sharing a character shares one shape
* definition , exactly as they share speeds and the state - to - sprite map - - and an
* individual actor may override it when it needs to .
*
* @ section collision_leaf This header includes nothing of ours but types . h
*
* ` actor . h ` and ` character . h ` both need akgl_CollisionShape by value , so this
* header cannot include either of them without a cycle . It names actors and
* tilemaps as incomplete struct pointers instead , which is why they need struct
* tags and why ` akgl_Tilemap ` grew one . The ` headers ` suite compiles this file
* as the first include of a translation unit and would fail immediately if that
* ever stopped being true .
*
* @ section collision_extrusion Why a 2 D shape has a depth
*
* The narrowphase behind this is three - dimensional , which is what lets the same
* shapes carry into a 3 D game later . It answers with the * * minimum * * translation
* that separates two volumes - - and if a 2 D shape were extruded into a thin
* slab , the cheapest way to separate two of them would be along z . The
* narrowphase would report a contact , the resolver would push the actor into the
* screen , and on screen nothing would happen at all while the actor sank through
* the floor .
*
* So the setters give every shape a depth of # AKGL_COLLISION_DEPTH_RATIO times
* its largest planar half - extent unless told otherwise . That is not a large
* number chosen for comfort ; it is the smallest ratio for which the z overlap of
* any two shapes built this way is provably larger than any planar penetration
* they can reach , so z can never be the minimum axis . Pass a depth by hand only
* if you know what that costs .
*/
# ifndef _AKGL_COLLISION_H_
# define _AKGL_COLLISION_H_
# include <stdbool.h>
# include <stdint.h>
# include <SDL3/SDL_rect.h>
# include <akerror.h>
# include <akgl/types.h>
struct akgl_Actor ;
struct akgl_Tilemap ;
/** @brief No shape. The actor takes no part in collision. */
# define AKGL_COLLISION_SHAPE_NONE 0
/** @brief An axis-aligned box. The common case, and the one with a closed-form answer. */
# define AKGL_COLLISION_SHAPE_BOX 1
/** @brief A circle in the xy plane, extruded along z. `hx` is the radius and `hy` is ignored. */
# define AKGL_COLLISION_SHAPE_CIRCLE 2
/** @brief A capsule whose long axis is x: a box with semicircular caps left and right. */
# define AKGL_COLLISION_SHAPE_CAPSULE_X 3
/** @brief A capsule whose long axis is y: a box with semicircular caps top and bottom. */
# define AKGL_COLLISION_SHAPE_CAPSULE_Y 4
/** @brief No flags. */
# define AKGL_COLLISION_FLAG_NONE 0x00000000u
/** @brief Never moved by the resolver and never re-inserted into the broad phase. */
# define AKGL_COLLISION_FLAG_STATIC 0x00000001u
/** @brief Reports a contact and never pushes. Pickups, trigger volumes, damage zones. */
# define AKGL_COLLISION_FLAG_SENSOR 0x00000002u
/** @brief Present but ignored this step. Cheaper than dropping the shape and putting it back. */
# define AKGL_COLLISION_FLAG_DISABLED 0x00000004u
/**
* @ brief Reserved for a swept narrowphase . * * Not implemented ; setting it does nothing . * *
*
* Sub - stepping bounds how far an actor moves between collision tests , which
* stops anything moving at ordinary speeds from passing through a wall . A
* projectile is not moving at ordinary speeds . See ` TODO . md ` .
*/
# define AKGL_COLLISION_FLAG_BULLET 0x00000008u
/** @brief On no layer. A shape with this `layermask` collides with nothing. */
# define AKGL_COLLISION_LAYER_NONE 0x00000000u
/** @brief Map geometry: solid tiles and static proxies. The default `collidemask`. */
# define AKGL_COLLISION_LAYER_STATIC (1u << 0)
/** @brief Ordinary actors. The default `layermask`. */
# define AKGL_COLLISION_LAYER_ACTOR (1u << 1)
/** @brief Suggested layer for the player. Nothing in the library treats it specially. */
# define AKGL_COLLISION_LAYER_PLAYER (1u << 2)
/** @brief Suggested layer for hostiles. */
# define AKGL_COLLISION_LAYER_ENEMY (1u << 3)
/** @brief Suggested layer for things picked up rather than bumped into. */
# define AKGL_COLLISION_LAYER_PICKUP (1u << 4)
/** @brief Every layer, including the ones a game defines for itself in bits 5 and above. */
# define AKGL_COLLISION_LAYER_ALL 0xFFFFFFFFu
/**
* @ brief Depth given to a 2 D shape , as a multiple of its largest planar half - extent .
*
* 2 and not 1. At 1 , two identical squares fully overlapped have the same
* overlap on z as in the plane , and which axis the narrowphase calls " minimum "
* comes down to floating - point luck . At 2 the z overlap of any pair is at least
* twice the largest planar half - extent either can reach , and the planar
* penetration is at most that - - so z loses every time , by construction rather
* than by margin .
*/
# define AKGL_COLLISION_DEPTH_RATIO 2.0f
/**
* @ brief A convex volume , positioned relative to an actor ' s origin .
*
* Stored as a centre and half - extents rather than as an ` SDL_FRect ` , because
* that is the form both the overlap test and the narrowphase want and this way
* the conversion happens once , in a setter , instead of on every one of the tens
* of thousands of tests a frame . The setters take the rectangle form a game
* already has .
*/
typedef struct akgl_CollisionShape {
uint8_t kind ; /**< One of the `AKGL_COLLISION_SHAPE_*` values. #AKGL_COLLISION_SHAPE_NONE means this actor does not collide, and is what a zeroed shape is. */
uint32_t flags ; /**< Bitwise OR of the `AKGL_COLLISION_FLAG_*` values. */
uint32_t layermask ; /**< Which layers this shape is *on*. What other shapes test against. */
uint32_t collidemask ; /**< Which layers this shape *responds to*. Asymmetric on purpose: a player can block against an NPC while the NPC ignores the player. */
float32_t ox ; /**< Centre offset from the actor's `x`, in map pixels. */
float32_t oy ; /**< Centre offset from the actor's `y`. Positive is down, matching screen space. */
float32_t oz ; /**< Centre offset from the actor's `z`. */
float32_t hx ; /**< Half-extent along x. The radius, for a circle or a capsule. */
float32_t hy ; /**< Half-extent along y. Ignored for a circle. */
float32_t hz ; /**< Half-extent along z. Never 0 after a setter has run; see the extrusion note on this file. */
} akgl_CollisionShape ;
Pool collision proxies, and tie one to the life of its actor
A proxy is what the broad phase will index: an actor's shape, where it is, and
the bounds that follow from those. It is a sixth heap layer, two per actor --
one for the actor itself and headroom for static geometry a game registers that
is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will
read those out of the tilemap's own array, because one proxy per tile is tens of
megabytes to index data that is already a grid.
The pool follows the existing convention rather than improving on it.
akgl_heap_next_collision_proxy finds a free slot and does not claim it;
akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry,
and it is kept on purpose: an acquire abandoned before initialization leaks
nothing, because the slot still reads as free, and a sixth pool with its own
rule would be worse than one defect with five instances. Fixing item 8 means
fixing all five together with every call site audited, and that is its own
change.
The proxy holds a *copy* of the shape rather than a pointer. An actor's own
logic may rewrite its shape mid-step -- a character crouching, a projectile
arming -- and a broad phase whose bounds came from a shape that has since
changed is a bug that only appears when two things happen in the same frame.
akgl_heap_release_actor now releases the actor's proxy. Without it the proxy
holds a borrowed `owner` into a slot that has just been zeroed, so the broad
phase keeps a registration whose owner reads as a free actor, and the next
contact against it is a collision with nothing. Verified by removing the release
and watching test_proxy_dies_with_its_actor go red.
The tests pin the convention as well as the behaviour: that two acquires with no
initialize between them return the same slot is asserted, not merely tolerated,
so that changing the convention has to change a test that says why it exists.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:56:21 -04:00
/**
* @ brief One shape ' s registration in the broad phase .
*
* A proxy is what the partitioner indexes . It carries a * * copy * * of the shape
* rather than a pointer to it , which is deliberate : an actor ' s own logic may
* legitimately rewrite its shape mid - step - - a character crouching , a projectile
* arming - - and a broad phase whose bounds were computed from a shape that has
* since changed is a source of bugs that only appear when two things happen in
* the same frame . The copy is refreshed once per step , from one place .
*
* Proxies are pool objects . The library creates and destroys them ; a game does
* not , and a game that releases an actor gets its proxy released with it .
*/
typedef struct akgl_CollisionProxy {
uint8_t refcount ; /**< Pool bookkeeping; 0 means the slot is free. First member, as in every pooled type here. */
struct akgl_Actor * owner ; /**< The actor this proxy tracks, or `NULL` for static geometry with no actor behind it. Borrowed; no reference is taken. */
akgl_CollisionShape shape ; /**< A copy of the owner's shape as of the last refresh. */
SDL_FRect bounds ; /**< Planar world bounds in map pixels, computed from #shape at (#x, #y). */
float32_t x ; /**< World position #bounds was computed from. */
float32_t y ; /**< World position #bounds was computed from. */
float32_t z ; /**< World position #bounds was computed from. Not represented in #bounds. */
uint32_t stamp ; /**< Sweep serial this proxy was last visited on, so a proxy spanning several cells is reported once. Written by the partitioner. */
} akgl_CollisionProxy ;
Answer whether two shapes overlap, and how to undo it
akgl_collision_test takes two proxies and fills in a contact: a unit normal, a
depth, and a point. Three paths, cheapest first.
The proxies' bounds reject most pairs in four comparisons and no arithmetic,
which is free because the bounds were computed when the proxy was synced.
A box against a box is answered in closed form. That is not only cheaper than
the general solver, it is *exact*, and the difference shows up where it matters
most: an actor resting on a floor wants a normal of precisely (0, -1, 0), and an
iterative solver converges to something like (0.0001, -0.99999, 0), which
accumulates into a slow sideways creep. A tile game is almost entirely boxes, so
this is the path that runs.
Everything else goes to ccdMPRPenetration. MPR rather than GJK+EPA because MPR
allocates nothing at all, converges in fewer iterations, and its one weakness --
a coarser contact *point* -- is on a field the blocking resolver never reads.
EPA stays compiled and available behind the arena for a caller who one day wants
an accurate manifold. The header says the point is approximate so nobody builds
a damage falloff on it.
The normal points out of the second shape and toward the first, so a caller
moving `a` along it by `depth` separates the pair. libccd hands back the
opposite convention; converting once here saves every resolver from remembering
the sign, and getting it backwards would compile, would pass any "do these
collide" test, and would drag actors into walls. There is a test that asserts
the direction, and inverting the conversion turns it red.
AKGL_COLLISION_TEST_PLANAR flattens the normal into the xy plane. The extrusion
the setters apply already makes z the most expensive axis, so this is a net for
a caller who set a depth by hand -- and the failure it catches is silent: the
narrowphase reports a contact, the resolver pushes the actor into the screen,
and the actor does not move on screen while remaining inside the floor. It also
rescues the degenerate case, two shapes at exactly the same centre, where there
is no planar direction at all and a zero-length normal would be a wall that
stops nothing. Level authors stack things constantly.
Three things the tests found rather than assumed:
- The guard has two copies, one per path, and the box one is where it earns its
place. Removing both is caught; removing only the iterative one is not,
because the iterative solver is seeded from the line between the two centres
and so converges to a planar answer on its own for a planar offset. That is
measured and written down in the test rather than papered over with a fixture
contrived to force it.
- A concentric pair has no preferred sign on the degenerate axis, so the test
asserts the magnitude of the normal rather than its direction. The first
version asserted -1 and failed against an equally correct +1.
- The box fast path and the iterative solver are two implementations of one
answer, so they are driven over the same arrangements and required to agree on
the decision and the axis. A shortcut nothing cross-checks is a shortcut
waiting to diverge.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:18:09 -04:00
/**
* @ brief Force every contact normal into the xy plane .
*
* A safety net rather than a mode . The extrusion the setters apply already makes
* z the most expensive axis to separate on , so a normal along z should be
* unreachable - - but a caller who set a depth by hand has opted out of that
* guarantee , and the failure it produces is silent : the resolver pushes the
* actor into the screen , which moves it nowhere a player can see while leaving
* it inside whatever it hit .
*
* Pass it for a 2 D game , which is every game today . When there is a real third
* axis to resolve on , leave it off .
*/
# define AKGL_COLLISION_TEST_PLANAR 0x00000001u
/**
* @ brief One overlap , described well enough to undo it .
*
* The narrowphase fills in the geometry . Which actor , which tile , and whether
* either side was a sensor are filled in by the layer above , which is the only
* one that knows .
*/
typedef struct akgl_Contact {
struct akgl_Actor * self ; /**< The actor being told about this contact. Filled in by the resolver, not the narrowphase. */
struct akgl_Actor * other ; /**< The other actor, or `NULL` when the hit was map geometry with no actor behind it. */
int32_t tilex ; /**< Tile column that was hit, or -1 when the other side was not a tile. */
int32_t tiley ; /**< Tile row, or -1. */
int32_t tilelayer ; /**< Index into the tilemap's layers, or -1. */
int32_t tilegid ; /**< Global tile id that was hit, or 0. This is what tells a spike from a floor without a second lookup. */
float32_t nx ; /**< Contact normal, unit length. **Points out of the other shape and toward this one**, so moving along it by #depth separates them. */
float32_t ny ; /**< Normal along y. Negative means the surface is *below*, since y grows downward. */
float32_t nz ; /**< Normal along z. Always 0 when #AKGL_COLLISION_TEST_PLANAR was used. */
float32_t depth ; /**< How far along the normal to move to stop overlapping, in map pixels. Never negative. */
float32_t px ; /**< A point on the overlap, in map pixels. Approximate for a non-box pair; see below. */
float32_t py ; /**< Contact point along y. */
float32_t pz ; /**< Contact point along z. */
float32_t dt ; /**< Length of the sub-step this contact was found in, in seconds. Filled in by the resolver. */
bool sensor ; /**< Either side carries #AKGL_COLLISION_FLAG_SENSOR: report, do not push. Filled in by the resolver. */
bool statichit ; /**< The other side does not move -- a tile, or a proxy flagged #AKGL_COLLISION_FLAG_STATIC. */
} akgl_Contact ;
/**
* @ brief Test two positioned shapes , and describe the overlap if there is one .
*
* Three paths , cheapest first . The proxies ' bounds reject most pairs outright .
* A box against a box is answered in closed form - - exact depth , exactly
* axis - aligned normal , no iteration - - which matters because a tile game is
* almost entirely boxes , and because a resting actor wants a normal that is
* precisely ` ( 0 , - 1 , 0 ) ` rather than one converged to within a tolerance , or it
* creeps . Everything else goes to the iterative narrowphase .
*
* @ note * * The contact point is approximate for anything but a box pair . * * The
* iterative solver returns a point on the portal it converged to , which
* for a deep off - centre overlap can sit noticeably away from the deepest
* point . The depth and the normal are not approximate , and the blocking
* resolver uses only those . Do not build a damage falloff on the point .
*
* @ param a First proxy . Required .
* @ param b Second proxy . Required .
* @ param flags Bitwise OR of the ` AKGL_COLLISION_TEST_ * ` values .
* @ param dest Receives the contact geometry when @ p hit comes back ` true ` .
* Required . The normal points out of @ p b and toward @ p a .
* @ param hit Receives whether the two overlap . Required .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If any pointer argument is ` NULL ` .
* @ throws AKGL_ERR_COLLISION If the solver could not characterise an
* intersection it found - - a degenerate shape , or the arena running out .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_test ( akgl_CollisionProxy * a , akgl_CollisionProxy * b , uint32_t flags , akgl_Contact * dest , bool * hit ) ;
Pool collision proxies, and tie one to the life of its actor
A proxy is what the broad phase will index: an actor's shape, where it is, and
the bounds that follow from those. It is a sixth heap layer, two per actor --
one for the actor itself and headroom for static geometry a game registers that
is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will
read those out of the tilemap's own array, because one proxy per tile is tens of
megabytes to index data that is already a grid.
The pool follows the existing convention rather than improving on it.
akgl_heap_next_collision_proxy finds a free slot and does not claim it;
akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry,
and it is kept on purpose: an acquire abandoned before initialization leaks
nothing, because the slot still reads as free, and a sixth pool with its own
rule would be worse than one defect with five instances. Fixing item 8 means
fixing all five together with every call site audited, and that is its own
change.
The proxy holds a *copy* of the shape rather than a pointer. An actor's own
logic may rewrite its shape mid-step -- a character crouching, a projectile
arming -- and a broad phase whose bounds came from a shape that has since
changed is a bug that only appears when two things happen in the same frame.
akgl_heap_release_actor now releases the actor's proxy. Without it the proxy
holds a borrowed `owner` into a slot that has just been zeroed, so the broad
phase keeps a registration whose owner reads as a free actor, and the next
contact against it is a collision with nothing. Verified by removing the release
and watching test_proxy_dies_with_its_actor go red.
The tests pin the convention as well as the behaviour: that two acquires with no
initialize between them return the same slot is asserted, not merely tolerated,
so that changing the convention has to change a test that says why it exists.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:56:21 -04:00
/*
* The following is part of the internal API . Proxies are created and destroyed
* by the library , not by a game .
*/
/**
* @ brief Claim a pooled proxy for an actor ' s shape and take the reference on it .
*
* Follows the convention every pool here uses : akgl_heap_next_collision_proxy
* finds a free slot and does * * not * * claim it , and this takes the reference .
* Write the two adjacent , because until the reference is taken the slot is still
* free and the next acquire hands out the same pointer . That is ` TODO . md ` " Known
* and still open " item 8, it applies to four of the five existing pools, and
* this one does not depart from it - - a sixth convention would be worse than the
* defect .
*
* @ param obj The proxy to initialize . Required .
* @ param owner The actor it tracks , or ` NULL ` for static geometry .
* @ param shape The shape to copy into it . Required .
* @ param x World position of the owner .
* @ param y World position of the owner .
* @ param z World position of the owner .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p obj or @ p shape is ` NULL ` .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_proxy_initialize ( akgl_CollisionProxy * obj , struct akgl_Actor * owner , akgl_CollisionShape * shape , float32_t x , float32_t y , float32_t z ) ;
/**
* @ brief Recompute a proxy ' s bounds from its owner ' s current shape and position .
*
* Called once per step per live proxy , before the broad phase is asked anything .
* This is the one place the shape copy is refreshed .
*
* @ param obj The proxy . Required .
* @ param shape The current shape to copy . Required .
* @ param x Current world position of the owner .
* @ param y Current world position of the owner .
* @ param z Current world position of the owner .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p obj or @ p shape is ` NULL ` .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_proxy_sync ( akgl_CollisionProxy * obj , akgl_CollisionShape * shape , float32_t x , float32_t y , float32_t z ) ;
Add collision shapes, on the character and overridable per actor
A shape is a convex volume positioned relative to an actor's origin. It lives on
akgl_Character, so every goblin sharing a character shares one shape definition
the way they already share speeds and the state-to-sprite map, and an actor may
override it with akgl_Actor::shape_override.
The flag is not redundant with an empty shape. "This actor deliberately has no
collider" and "this actor has not been given one yet" are different states, and
without somewhere to record the difference akgl_actor_set_character cannot tell
whether it is allowed to overwrite what it finds.
include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete
struct pointers rather than including their headers, because both of those need
akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite
compiles it as the first include of a translation unit, so that property is
enforced rather than merely intended -- which is why the tilemap types got struct
tags two commits ago.
The masks are asymmetric and the defaults are the load-bearing part: layermask
ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with
map geometry and with no other actor. "Everything with a hitbox shoves everything
else" would be a surprising default for a town full of scenery and a painful one
to discover after the fact; opting in to actor-versus-actor is one bit, opting
out of it would have been a hunt through every NPC a game spawns.
Every shape gets a z depth, because the narrowphase behind this is three
dimensional and answers with the *minimum* separating translation. A thin
extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a
player can see while remaining inside the floor -- a collision system reporting
success while doing nothing. The ratio of 2 is the smallest for which the z
overlap of any two shapes the setters build provably exceeds any planar
penetration they can reach.
The test for that asserts the inequality across a spread of sizes rather than
asserting the constant is 2, so a change to the constant fails here rather than
in a game.
Two things about that test are worth recording, because the first version had
both:
- It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by
`break`ing, and the assertion was inside a nested `for` -- so the break left
the loop rather than the ATTEMPT block and the failure evaporated. This is the
hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified
by setting the ratio to 0.5 and watching the suite still pass, then fixing it
and watching it report the 512x512 case by name.
- tests/collision_arena.c had the same bug in a loop added in the previous
commit, and it is fixed here too.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:48:05 -04:00
/**
* @ brief Build an axis - aligned box from the frame - relative rectangle a game has .
*
* @ p body is an offset and a size measured from the actor ' s position - - the same
* form a game already writes a hitbox in , inset into its sprite frame because
* sprite art does not reach the edges of its cell . It is converted here to the
* centre - and - half - extent form everything downstream uses .
*
* The shape is zeroed first , so every field not named by the arguments ends at a
* known value : no flags , and the default masks of # AKGL_COLLISION_LAYER_ACTOR on
* and # AKGL_COLLISION_LAYER_STATIC responded to . That default is chosen so an
* actor given a shape and nothing else collides with the map and with nothing
* else - - a town full of NPCs does not start shoving itself around because
* somebody gave the townsfolk hitboxes .
*
* @ param dest Receives the shape . Required .
* @ param body Offset and size in map pixels , relative to the actor ' s position .
* Required . Both ` w ` and ` h ` must be positive .
* @ param depth Half - extent along z . Pass 0 for a 2 D game and the extrusion is
* chosen for you ; see the note on this file for why that matters .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p dest or @ p body is ` NULL ` .
* @ throws AKERR_VALUE If ` body - > w ` or ` body - > h ` is not positive , or @ p depth is
* negative .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_shape_box ( akgl_CollisionShape * dest , SDL_FRect * body , float32_t depth ) ;
/**
* @ brief Build a circle , extruded along z into a cylinder .
*
* A cylinder and not a sphere : a sphere ' s caps would round away from the plane
* and let a shape slip past a corner in a way a 2 D game never expects .
*
* @ param dest Receives the shape . Required .
* @ param ox Centre offset from the actor ' s ` x ` , in map pixels .
* @ param oy Centre offset from the actor ' s ` y ` .
* @ param radius Radius in map pixels . Must be positive .
* @ param depth Half - extent along z , or 0 to have it chosen .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p dest is ` NULL ` .
* @ throws AKERR_VALUE If @ p radius is not positive , or @ p depth is negative .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_shape_circle ( akgl_CollisionShape * dest , float32_t ox , float32_t oy , float32_t radius , float32_t depth ) ;
/**
* @ brief Build a capsule : a box with semicircular caps on one axis .
*
* Useful for a character that should slide off a corner rather than catch on it ,
* which a box does and a capsule does not .
*
* @ param dest Receives the shape . Required .
* @ param body Offset and size in map pixels , as for akgl_collision_shape_box .
* Required .
* @ param axis # AKGL_COLLISION_SHAPE_CAPSULE_X or # AKGL_COLLISION_SHAPE_CAPSULE_Y .
* @ param depth Half - extent along z , or 0 to have it chosen .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p dest or @ p body is ` NULL ` .
* @ throws AKERR_VALUE If either side is not positive , @ p depth is negative , @ p
* axis is neither capsule kind , or the capped axis is not the longer one
* - - a capsule whose caps are wider than it is long is a circle written
* confusingly , and is refused rather than silently reinterpreted .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_shape_capsule ( akgl_CollisionShape * dest , SDL_FRect * body , uint8_t axis , float32_t depth ) ;
/**
* @ brief The planar bounds a shape occupies with its owner at a given position .
*
* This is what the broad phase indexes on and what a cheap overlap test uses
* before anything more expensive runs . ` z ` is not represented : the extrusion
* exists to keep the narrowphase honest , not to be searched .
*
* @ param shape The shape . Required .
* @ param x The owner ' s ` x ` .
* @ param y The owner ' s ` y ` .
* @ param dest Receives the bounds in map pixels . Required .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If @ p shape or @ p dest is ` NULL ` .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_shape_bounds ( akgl_CollisionShape * shape , float32_t x , float32_t y , SDL_FRect * dest ) ;
/**
* @ brief Whether two shapes are allowed to interact , in the given direction .
*
* Asymmetric , and that is the point : @ p self responds to @ p other when @ p
* other ' s ` layermask ` intersects @ p self ' s ` collidemask ` . The reverse is a
* separate question with its own answer , which is how a player blocks against a
* pushable crate while the crate ignores everything .
*
* A shape of kind # AKGL_COLLISION_SHAPE_NONE , or one carrying
* # AKGL_COLLISION_FLAG_DISABLED , interacts with nothing in either direction .
*
* @ param self The shape doing the responding . Required .
* @ param other The shape being responded to . Required .
* @ param dest Receives whether the pair is worth testing . Required .
* @ return ` NULL ` on success , otherwise an error context owned by the caller .
* @ throws AKERR_NULLPOINTER If any argument is ` NULL ` .
*/
akerr_ErrorContext AKERR_NOIGNORE * akgl_collision_shape_interacts ( akgl_CollisionShape * self , akgl_CollisionShape * other , bool * dest ) ;
# endif