Files
libakgl/examples/jrpg/jrpg.h
Andrew Kesterson cbf7c1b6c2 Delete the JRPG's collision too, and wall the map with proxies
cell_solid and feet_blocked go, and so does the prediction in the player's
movementlogicfunc. That prediction was only ever exact because the town has zero
gravity and zero drag -- v is t, so `x + tx * dt` is where the step lands. A map
with gravity would have had to fold ey in as well, which is the game
re-implementing the integrator. Resolution runs after the move now, so there is
nothing to predict.

The map's decoration layer carries `collidable`, which retires JRPG_LAYER_SOLID:
akgl_TilemapLayer has no name member, so the game and the map had to agree on an
index out of band and inserting a layer in Tiled broke it.

The edge of the world is not on any layer, so it is four static proxies covering
the outer ring plus a tile of overhang -- one pool slot per side instead of a
hundred solid tiles, and the first use of the static-proxy path in either
example. They carry LAYER_STATIC explicitly: shape_box defaults a shape to
LAYER_ACTOR, and a wall left on that layer is a wall everything walks through.

Only the player gets a shape. NPCs stand still and are spoken to; the follower
is a child snapped to its parent every step.

Verified against the old implementation with the demo script rewritten to hold
one direction: holding left into a building stops the player at x=122 with both,
and holding up into the map's edge stops them at y=-4 with both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 07:46:19 -04:00

189 lines
7.9 KiB
C

/**
* @file jrpg.h
* @brief Shared declarations for the JRPG tutorial game.
*
* Every function with external linkage in this program is declared here, the
* way AGENTS.md requires of the library itself. It is a three-translation-unit
* program and it would compile without the header; declaring them anyway is
* what keeps a signature from drifting between the definition and the call.
*
* Everything this program exports carries the `jrpg_` prefix. `static` helpers
* drop it, because the prefix exists only to avoid collisions with libakgl,
* SDL and libc -- the same rule, for the same reason.
*/
#ifndef _JRPG_JRPG_H_
#define _JRPG_JRPG_H_
#include <stdbool.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/collision.h>
#include <akgl/types.h>
/*
* Where the game's data lives. Both are absolute paths baked in by
* examples/jrpg/CMakeLists.txt, because the tutorial has to run from the build
* tree, from the source tree, and from wherever CTest happens to put its
* working directory. The fallbacks are what a reader compiling this by hand
* from the repository root would want.
*/
#ifndef JRPG_ASSET_DIR
#define JRPG_ASSET_DIR "docs/tutorials/assets/jrpg"
#endif
#ifndef JRPG_FONT_FILE
#define JRPG_FONT_FILE "tests/assets/akgl_test_mono.ttf"
#endif
/*
* The screen size is written as text because that is the only form
* akgl_set_property takes, and akgl_render_2d_init copies it onto `akgl_camera`
* on the way past. Everything below reads the camera rather than a second copy
* of the same two numbers.
*/
#define JRPG_SCREEN_WIDTH "320"
#define JRPG_SCREEN_HEIGHT "240"
/** @brief Registry name of the one font this game loads. */
#define JRPG_FONT_NAME "jrpg"
/** @brief Point size the font is rasterized at. A size is baked into the handle. */
#define JRPG_FONT_SIZE 12
/** @brief Longest line an NPC can say, terminator included. */
#define JRPG_TEXTBOX_MAX_TEXT 256
/** @brief Registry name of the party member this game attaches to the player. */
#define JRPG_FOLLOWER_NAME "companion"
/**
* @brief One entry in the scripted demo the headless smoke run drives.
*
* The keystrokes go in through akgl_controller_handle_event like any other
* event, so the smoke run exercises the same binding, state and physics path a
* player does rather than a separate one written to be testable.
*/
typedef struct {
long frame; /**< Frame number this step fires on. */
SDL_Keycode key; /**< Key to synthesize. */
bool down; /**< True for a press, false for a release. */
} jrpg_ScriptStep;
/**
* @brief Load every sprite, then every character, then the town map.
*
* In that order, and the order is not a preference: akgl_character_load_json
* resolves each sprite name through #AKGL_REGISTRY_SPRITE, and
* akgl_tilemap_load resolves each `character` property through
* #AKGL_REGISTRY_CHARACTER while it spawns the map's actor objects.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_load(void);
/**
* @brief The collision world, owned by world.c.
*
* The physics backend resolves through it after every sub-move. Nothing else in
* the game touches it: the town is walls and people, and the walls are the
* map's own decoration layer plus four proxies around the edge.
*/
extern akgl_CollisionWorld jrpg_collision;
/**
* @brief Fix up the actors the map spawned, and attach the party member.
*
* Clears `movement_controls_face` on everything -- without which the default
* `facefunc` erases the facing bit the map set and every NPC stops being drawn
* on frame one -- installs the blocking movement logic on the player, and
* builds the follower as a child actor.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_REGISTRY If the map spawned no actor named "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_populate(void);
/**
* @brief Centre the camera on the player, clamped to the edges of the map.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_REGISTRY If there is no actor named "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_camera_follow(void);
/**
* @brief The player's `movementlogicfunc`: the library default, plus the freeze.
*
* Walls are the library's now -- see chapter 15 -- so all this adds to
* akgl_actor_logic_movement is raising AKGL_ERR_LOGICINTERRUPT while the text
* box is open, which is the documented way to tell the simulator to skip an
* actor's tick. That also skips `collide`, since the CATCH around
* movementlogicfunc jumps past the whole rest of the step.
*
* @param obj The actor to compute acceleration for. Required, along with its
* `basechar`.
* @param dt Seconds this step covers.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
* @throws AKGL_ERR_LOGICINTERRUPT While a conversation is open.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt);
/**
* @brief The follower's `renderfunc`: akgl_actor_render with the parent detached.
*
* The guard for a defect. akgl_physics_simulate writes a child's `x` as
* `parent->x + vx` -- an absolute world position -- and akgl_actor_render then
* draws it at `parent->x + obj->x`, adding the parent's position a second time.
* Nulling `parent` for the duration of the draw takes the branch that does not
* add it. See chapter 20.
*
* @param obj The child actor to draw. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_follower_render(akgl_Actor *obj);
/**
* @brief Control handler: talk to whoever is standing nearby, or close the box.
* @param obj The actor the control map drives -- the player. Required.
* @param event The event that triggered this. Required, but not read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_talk(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Control handler that does nothing, for the release half of a binding.
*
* akgl_controller_handle_event does not check a matched binding's handler
* pointer before calling it, so a binding with a `NULL` `handler_off` is a
* crash rather than an error. Every binding gets both halves.
*
* @param obj The actor the control map drives. Required.
* @param event The event that triggered this. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_ignore(akgl_Actor *obj, SDL_Event *event);
/**
* @brief Put a line of dialogue on screen and freeze the world.
* @param text The line to show. Required. Truncated at #JRPG_TEXTBOX_MAX_TEXT.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p text is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_open(char *text);
/** @brief Dismiss the text box. */
void jrpg_textbox_close(void);
/** @brief True while a line of dialogue is on screen. */
bool jrpg_textbox_showing(void);
/**
* @brief Draw the panel and its text, if there is anything to draw.
*
* Called after akgl_game_update, so it lands on top of the world rather than
* under it, and in screen coordinates rather than world ones --
* akgl_text_rendertextat does not go through the camera, which is what a HUD
* wants.
*
* @return `NULL` on success -- including when the box is closed -- otherwise an
* error context owned by the caller.
* @throws AKGL_ERR_REGISTRY If #JRPG_FONT_NAME is not a loaded font.
*/
akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_draw(void);
#endif // _JRPG_JRPG_H_