/** * @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 #include #include #include #include /* * 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 Index of the tile layer this game treats as solid. * * An index, not a name, because akgl_TilemapLayer has no `name` member: the * loader reads a layer's `id`, `type`, `opacity`, `visible`, offset and data, * and drops the name Tiled wrote. A map cannot say "the layer called * collision", so the game and the map agree on a number. See chapter 20. */ #define JRPG_LAYER_SOLID 1 /** @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 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 walls. * * libakgl implements no collision at all -- akgl_physics_arcade_collide raises * AKERR_API, akgl_physics_simulate never calls `collide`, and * akgl_physics_arcade_move consults no tilemap -- so a game that wants a wall * writes one here. Raises AKGL_ERR_LOGICINTERRUPT while the text box is open, * which is the documented way to tell the simulator to skip an actor's tick. * * @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_