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
189 lines
7.9 KiB
C
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_
|