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>
166 lines
10 KiB
C
166 lines
10 KiB
C
/**
|
|
* @file character.h
|
|
* @brief The reusable half of an actor: movement constants and a state-to-sprite map.
|
|
*
|
|
* A character is the template; an akgl_Actor is the instance. Everything that is
|
|
* the same for every goblin on the map -- top speed, acceleration, frame dwell
|
|
* time, and which sprite to draw for which combination of actor-state bits --
|
|
* lives here and is shared by pointer, so a hundred actors cost one character.
|
|
*
|
|
* Characters are pool objects (akgl_heap_next_character) and are published in
|
|
* the #AKGL_REGISTRY_CHARACTER property registry under their name, which is how
|
|
* akgl_actor_set_character finds them. akgl_registry_init must have run first.
|
|
*/
|
|
|
|
#ifndef _AKGL_CHARACTER_H_
|
|
#define _AKGL_CHARACTER_H_
|
|
|
|
#include <SDL3/SDL_properties.h>
|
|
#include <akgl/types.h>
|
|
#include <akgl/collision.h>
|
|
#include <akgl/sprite.h>
|
|
|
|
#define AKGL_CHARACTER_MAX_NAME_LENGTH 128
|
|
|
|
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
|
|
typedef struct akgl_Character {
|
|
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
|
|
char name[AKGL_CHARACTER_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
|
|
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
|
|
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONEMS_NS. */
|
|
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
|
|
float32_t ay; /**< Acceleration along y. */
|
|
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
|
|
float32_t sx; /**< Maximum speed along x, world units per second. */
|
|
float32_t sy; /**< Maximum speed along y. */
|
|
float32_t sz; /**< Maximum speed along z. Not read from JSON; stays 0 unless set by hand. */
|
|
akgl_CollisionShape shape; /**< The collision volume every actor of this character gets, unless the actor overrides it. A zeroed shape (#AKGL_COLLISION_SHAPE_NONE) means actors of this character do not collide, which is what a character loaded before shapes existed still gets. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); /**< Bound to akgl_character_sprite_add by akgl_character_initialize. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); /**< Bound to akgl_character_sprite_get by akgl_character_initialize. */
|
|
} akgl_Character;
|
|
|
|
|
|
/**
|
|
* @brief Zero a pooled character, name it, and publish it in the character registry.
|
|
*
|
|
* Wipes the struct, copies @p name into it, creates the empty state-to-sprite
|
|
* property set, binds the `sprite_add`/`sprite_get` function pointers, inserts
|
|
* the character into #AKGL_REGISTRY_CHARACTER under @p name, and takes the first
|
|
* reference. Everything numeric (speeds, accelerations, `speedtime`) is left at
|
|
* zero for the caller -- or akgl_character_load_json -- to fill in.
|
|
*
|
|
* @param obj Pooled character to initialize, normally straight from
|
|
* akgl_heap_next_character. Required. Any previous contents are
|
|
* discarded without releasing the sprites they referenced.
|
|
* @param name Registry key, NUL-terminated. Required. Truncated at
|
|
* #AKGL_CHARACTER_MAX_NAME_LENGTH. A name already in the
|
|
* registry is silently replaced, and the character it displaced
|
|
* becomes unreachable rather than being released.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p basechar or @p name is `NULL`, or if SDL
|
|
* cannot create the state-to-sprite property set (out of memory).
|
|
* @throws AKERR_KEY If the character cannot be written into
|
|
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
|
|
* has not run and the registry id is still 0.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *obj, char *name);
|
|
/**
|
|
* @brief Bind a sprite to one exact combination of actor-state bits.
|
|
*
|
|
* The map key is the decimal spelling of @p state, so lookups match the *whole*
|
|
* value: a sprite added for `FACE_LEFT|MOVING_LEFT` is not found by a lookup for
|
|
* `FACE_LEFT` alone. Callers are expected to register every combination they
|
|
* intend to draw. Adding a sprite takes a reference on it.
|
|
*
|
|
* @param basechar Character to add the mapping to. Required. Must already have
|
|
* been through akgl_character_initialize.
|
|
* @param ref Sprite to draw for @p state. Required. Its `refcount` is
|
|
* incremented, so the character keeps it alive.
|
|
* @param state The exact actor-state bitmask (`AKGL_ACTOR_STATE_*`) this
|
|
* sprite is for. 0 is accepted and is a usable key.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p basechar or @p ref is `NULL`.
|
|
*
|
|
* @note Re-adding a different sprite for a @p state that is already mapped
|
|
* replaces the entry without releasing the sprite it displaced, so the
|
|
* displaced sprite's reference is never given back.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
|
/**
|
|
* @brief Look up the sprite bound to one exact combination of actor-state bits.
|
|
*
|
|
* The counterpart to akgl_character_sprite_add, and matched the same way: an
|
|
* exact match on the whole @p state value, with no fallback to a subset of the
|
|
* bits and no default sprite. Unlike most lookups in this codebase, *not finding
|
|
* one is an error* rather than a successful "nothing here" -- an actor with no
|
|
* sprite for its current state cannot be drawn.
|
|
*
|
|
* @param basechar Character to search. Required.
|
|
* @param state The exact actor-state bitmask to look up.
|
|
* @param dest Receives the mapped sprite. Required -- the return value is
|
|
* spoken for by the error context. Set to `NULL` when there is
|
|
* no mapping, alongside the AKERR_KEY error.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p basechar or @p dest is `NULL`.
|
|
* @throws AKERR_KEY If no sprite is bound to exactly that @p state. The message
|
|
* carries the state both as a decimal and as a bit pattern.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
|
|
|
|
// This is an SDL iterator so we can't return our error state from it.
|
|
/**
|
|
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one state-to-sprite entry.
|
|
*
|
|
* Currently implements exactly one operation: with #AKGL_ITERATOR_OP_RELEASE
|
|
* set, it hands the mapped sprite back to its heap layer. Every other flag is
|
|
* ignored here. Used to tear a character's sprite map down entry by entry.
|
|
*
|
|
* @param userdata The akgl_Iterator carrying the operation flags, passed through
|
|
* by `SDL_EnumerateProperties`. Required despite the `void *` --
|
|
* a `NULL` here is an error, not "no operations".
|
|
* @param props The character's `state_sprites` set, supplied by SDL.
|
|
* @param name The property key: one state bitmask in decimal. Required.
|
|
*
|
|
* @warning This is an SDL callback, so it returns `void` and has nowhere to
|
|
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
|
|
* stack trace and then calls libakerror's unhandled-error handler --
|
|
* whose default implementation **exits the process** with the error
|
|
* status. A missing sprite or a `NULL` @p userdata therefore terminates
|
|
* the game rather than skipping an entry. Install your own
|
|
* `akerr_handler_unhandled_error` if that is not what you want.
|
|
*/
|
|
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
|
|
|
/**
|
|
* @brief Build a character from a JSON definition file and register it.
|
|
*
|
|
* Claims a character from the pool, then reads: `name` (the registry key),
|
|
* `sprite_mappings` (an array of `{ "sprite": <name>, "state": [<state names>] }`
|
|
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
|
|
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
|
|
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
|
|
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
|
|
*
|
|
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
|
|
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
|
|
* the pooled character is released again.
|
|
*
|
|
* @param filename Path to the JSON document. Required. Used verbatim -- it is
|
|
* not resolved against `SDL_GetBasePath()`.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p filename is `NULL`; if the file cannot be
|
|
* opened or does not parse (the message carries jansson's line number
|
|
* and text); or if a mapping names a sprite that is not in the sprite
|
|
* registry.
|
|
* @throws AKERR_KEY If a `state` array contains a name that is not a known
|
|
* actor state, if a required top-level key is absent, or if the
|
|
* character cannot be added to the registry.
|
|
* @throws AKERR_TYPE If a key is present but holds the wrong JSON type -- for
|
|
* example `speedtime` as a string. The message names the key.
|
|
* @throws AKERR_OUTOFBOUNDS If a `state` array is indexed past its end.
|
|
* @throws AKGL_ERR_HEAP If the character pool or the string pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
|
|
|
#endif // _AKGL_CHARACTER_H_
|