Add two tutorial games that build, run in CI, and cannot drift

examples/sidescroller and examples/jrpg are complete programs, built with the
library and exercised headless by ctest. The chapters quote them with
`c excerpt=examples/...` blocks rather than restating the code, so a chapter
cannot drift from a program that compiles -- the excerpt check fails the moment
the source moves. 34 excerpts in one chapter, 21 in the other.

The two are complementary. The sidescroller is the physics tutorial: gravity, a
jump, coins, hazards. The JRPG is the content-pipeline tutorial: a town map,
NPCs spawned from map objects, four-way per-facing animation, a text box, a
follower.

Both smoke tests drive real SDL_Events through akgl_controller_handle_event and
step the physics clock at a fixed 1/60s rather than sleeping, so a scripted run
is deterministic and finishes in under five seconds.

Writing them is what turned up most of the defects recorded in the next commit,
because a game exercises paths a unit test does not. Each workaround says in the
chapter which library gap forced it:

- collision is written in a custom movementlogicfunc, because
  akgl_physics_arcade_collide raises AKERR_API and akgl_physics_simulate never
  calls collide at all;
- the sidescroller cancels the step's own gravity when it blocks downward,
  because otherwise a quarter-pixel of penetration makes the *horizontal* sweep
  report blocked and the character walks backwards a tile at a time;
- both clear movement_controls_face on every map-spawned actor, because the
  default facefunc leaves a stopped actor with no facing bit, no sprite, and no
  draw;
- the JRPG's follower gets a renderfunc that nulls obj->parent for the duration
  of the draw, because a child's offset is counted twice.

Assets are CC0 from three Kenney packs, vendored with per-pack licence text,
per-file provenance, and the geometry contract in
docs/tutorials/assets/README.md. CC0 specifically rather than merely free: a
reader who copies a tutorial into their own game inherits no obligation.
scripts/fetch_tutorial_assets.sh refreshes them in the shape
mkcontrollermappings.sh was fixed into for 0.5.0 -- it checks curl's status,
refuses a pack page that does not say CC0, verifies the archive and the staged
dimensions, and leaves the tracked bytes untouched on any failure. Both failure
paths were tested, and a no-op refresh is byte-identical.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 20:59:00 -04:00
parent b938460127
commit 88aaa184e4
72 changed files with 8946 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# The top-down JRPG tutorial game, quoted by docs/20-tutorial-jrpg.md.
#
# It is a real target built by `all`, not a snippet: a tutorial whose program
# does not compile is the failure this whole documentation effort exists to
# stop, and the chapter's `c excerpt=` blocks fail the moment this source moves
# under them.
add_executable(jrpg
jrpg.c
world.c
textbox.c
)
target_link_libraries(jrpg
PRIVATE akstdlib::akstdlib akerror::akerror akgl
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer
jansson::jansson -lm)
target_include_directories(jrpg PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_options(jrpg PRIVATE ${AKGL_WARNING_FLAGS})
# The game finds its data through two absolute paths baked in here. A game that
# ships would install its assets and resolve them against SDL_GetBasePath(); a
# tutorial has to run from the build tree, from the source tree, and from
# whatever working directory CTest hands it, so the paths are compiled in.
get_filename_component(JRPG_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
target_compile_definitions(jrpg PRIVATE
JRPG_ASSET_DIR="${JRPG_REPO_ROOT}/docs/tutorials/assets/jrpg"
# tests/assets/akgl_test_mono.ttf, which ships beside its licence file.
JRPG_FONT_FILE="${JRPG_REPO_ROOT}/tests/assets/akgl_test_mono.ttf"
)
# The vendored SDL satellite libraries land in per-project subdirectories that
# are not on the loader's default search path, exactly as they do for the test
# suites. AKGL_VENDORED_RPATH is set by the top-level CMakeLists.txt and is only
# defined when something was actually vendored.
if(AKGL_VENDORED_DEPENDENCIES)
set_target_properties(jrpg PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
endif()
# The headless smoke run. `--demo` drives the arrow keys from a script and steps
# the physics clock by a fixed 1/60 s, so the run is deterministic and finishes
# in well under a second; `--frames` bounds it. Between them the run walks the
# player into a wall, opens the text box, and exercises the follower's render
# hook -- rather than just proving that main() returns.
#
# add_test() reaches the real command here: the top-level CMakeLists.txt shadows
# it to suppress the vendored projects' registrations and lifts the suppression
# again long before examples/ is added.
add_test(NAME example_jrpg COMMAND jrpg --frames 320 --demo)
set_tests_properties(example_jrpg PROPERTIES
TIMEOUT 120
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy"
)
# ENVIRONMENT above replaces the environment wholesale, so LD_LIBRARY_PATH has
# to go in the same property rather than a second one. Only needed when the
# dependencies were vendored; an installed build resolves them normally.
if(AKGL_VENDORED_DEPENDENCIES)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
set(JRPG_TEST_ENV_MOD "")
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
list(APPEND JRPG_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
endforeach()
set_tests_properties(example_jrpg
PROPERTIES ENVIRONMENT_MODIFICATION "${JRPG_TEST_ENV_MOD}")
else()
string(REPLACE ";" ":" JRPG_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
set_tests_properties(example_jrpg PROPERTIES
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy;LD_LIBRARY_PATH=${JRPG_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
)
endif()
endif()

369
examples/jrpg/jrpg.c Normal file
View File

@@ -0,0 +1,369 @@
/**
* @file jrpg.c
* @brief A small top-down JRPG: startup, the frame loop, and teardown.
*
* Chapter 20 of the manual quotes this program rather than restating it, so
* what is here is what the chapter teaches. Run it with no arguments for a
* window you can walk around in:
*
* ./examples/jrpg/jrpg
*
* Arrow keys walk, space talks to whoever is standing next to you and dismisses
* the box again. `--frames N` stops after N frames, which is what makes this
* runnable as a headless smoke test; `--demo` drives the arrow keys from a
* script and steps the physics clock by a fixed interval so the run is
* deterministic and takes no wall-clock time.
*/
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/controller.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/iterator.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akgl/tilemap.h>
#include "jrpg.h"
/** @brief Nanoseconds in one 60 Hz step. What `--demo` advances the clock by. */
#define JRPG_FIXED_STEP_NS (AKGL_TIME_ONESEC_NS / 60)
/** @brief Milliseconds one 60 Hz frame is allowed to take, for the interactive loop. */
#define JRPG_FRAME_BUDGET_MS 16
/*
* The scripted walk `--demo` drives. The player starts at (224, 256) and the
* elder stands at (272, 112), so this is: right for a second, up for two and a
* bit, talk, try to walk while frozen, dismiss, walk away. Every one of those
* is a path the interactive game has and a headless run would otherwise never
* touch.
*
* Frame Key Down What it is for
*/
static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = {
{ 10, SDLK_RIGHT, true }, /* the per-facing walk animation */
{ 70, SDLK_RIGHT, false },
{ 75, SDLK_UP, true }, /* up the map, past the buildings */
{ 205, SDLK_UP, false },
{ 215, SDLK_SPACE, true }, /* the elder is in range: open the box */
{ 216, SDLK_SPACE, false },
{ 225, SDLK_LEFT, true }, /* frozen: AKGL_ERR_LOGICINTERRUPT eats this */
{ 245, SDLK_LEFT, false },
{ 255, SDLK_SPACE, true }, /* dismiss */
{ 256, SDLK_SPACE, false },
{ 265, SDLK_DOWN, true }, /* and walk away */
{ 285, SDLK_DOWN, false }
};
#define JRPG_DEMO_STEPS (sizeof(JRPG_DEMO_SCRIPT) / sizeof(JRPG_DEMO_SCRIPT[0]))
static long frame_limit = 0;
static bool demo = false;
static bool running = true;
static int exitstatus = 0;
static bool lowfps_warned = false;
/**
* @brief Replacement `akgl_game.lowfpsfunc`: say it once, not sixty times a second.
*
* akgl_game.fps is a completed-second average, so it reads 0 for the first
* second of every process -- which is under the threshold, so the default hook
* logs a line on every frame until the first second is up. The hook exists to
* be replaced; a real game sheds work here rather than talking about it.
*/
static void lowfps_quiet(void)
{
if ( lowfps_warned == false ) {
lowfps_warned = true;
SDL_Log("Frame rate is under 30 and this game does nothing about it");
}
}
/**
* @brief Read `--frames N` and `--demo` off the command line.
*
* @param argc Argument count, from `main`.
* @param argv Argument vector, from `main`. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p argv is `NULL`.
* @throws AKERR_VALUE If `--frames` is last, or its argument is not a number.
*/
static akerr_ErrorContext *parse_args(int argc, char *argv[])
{
PREPARE_ERROR(errctx);
int i = 0;
int frames = 0;
FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "argv");
for ( i = 1; i < argc; i++ ) {
if ( strcmp(argv[i], "--demo") == 0 ) {
demo = true;
} else if ( strcmp(argv[i], "--frames") == 0 ) {
if ( (i + 1) >= argc ) {
FAIL_RETURN(errctx, AKERR_VALUE, "--frames needs a frame count");
}
i += 1;
PASS(errctx, aksl_atoi(argv[i], &frames));
frame_limit = frames;
} else {
FAIL_RETURN(errctx, AKERR_VALUE, "usage: jrpg [--frames N] [--demo]");
}
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Bring the library up, load the town, and bind the controls.
*
* The order below is the one game.h documents and the only one that works.
* Three things in it are not obvious and are what chapter 20 spends its time
* on: the properties have to be set between akgl_game_init and
* akgl_render_2d_init, because that is what reads them; the physics backend has
* to be initialized by hand, because akgl_game_init does not; and the assets
* have to be loaded sprites-then-characters-then-map.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
static akerr_ErrorContext *startup(void)
{
PREPARE_ERROR(errctx);
akgl_Control talk;
// Required before akgl_game_init, which refuses to run without all three:
// the window title, SDL's application metadata and the savegame
// compatibility check are built from them.
PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name), "libakgl JRPG tutorial", sizeof(akgl_game.name) - 1));
PASS(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1));
PASS(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.libakgl.examples.jrpg", sizeof(akgl_game.uri) - 1));
PASS(errctx, akgl_game_init());
akgl_game.lowfpsfunc = &lowfps_quiet;
PASS(errctx, akgl_set_property("game.screenwidth", JRPG_SCREEN_WIDTH));
PASS(errctx, akgl_set_property("game.screenheight", JRPG_SCREEN_HEIGHT));
PASS(errctx, akgl_render_2d_init(akgl_renderer));
// akgl_game_init points akgl_physics at akgl_default_physics and stops
// there. That storage is BSS, so all four of its method pointers are NULL,
// and akgl_game_update calls `akgl_physics->simulate(...)` without checking
// it: a NULL function pointer, and a segmentation fault on frame one rather
// than an error context.
//
// town.tmj happens to declare its own physics, and jrpg_world_load switches
// to the backend that builds -- so removing this line alone leaves *this*
// program working, and removing it along with that switch is a SIGSEGV on
// frame one. Measured, by removing both. This line is what makes the
// program correct for a map that declares no physics, which is most of
// them. Nothing calls the factory for you either way. A top-down game wants
// the arcade backend with no gravity, which is what the property defaults
// already give.
PASS(errctx, akgl_physics_init_arcade(akgl_physics));
PASS(errctx, akgl_text_loadfont(JRPG_FONT_NAME, JRPG_FONT_FILE, JRPG_FONT_SIZE));
PASS(errctx, jrpg_world_load());
PASS(errctx, jrpg_world_populate());
// The arrow keys and the D-pad, wired to the akgl_actor_cmhf_* pairs, in
// one call. Keyboard id 0 and gamepad id 0 mean "the first of each".
PASS(errctx, akgl_controller_default(0, "player", 0, 0));
// And one binding of our own on the same map. A binding is a struct, copied
// in, so a stack local is fine. handler_off has to be non-NULL --
// akgl_controller_handle_event does not check it before calling it -- and
// the button is set to something no gamepad reports, because the gamepad
// and keyboard arms of the match are evaluated together.
PASS(errctx, aksl_memset((void *)&talk, 0x00, sizeof(talk)));
talk.event_on = SDL_EVENT_KEY_DOWN;
talk.event_off = SDL_EVENT_KEY_UP;
talk.key = SDLK_SPACE;
talk.button = (uint8_t)SDL_GAMEPAD_BUTTON_INVALID;
talk.handler_on = &jrpg_cmhf_talk;
talk.handler_off = &jrpg_cmhf_ignore;
PASS(errctx, akgl_controller_pushmap(0, &talk));
SUCCEED_RETURN(errctx);
}
/**
* @brief Feed the scripted keystrokes due on this frame into the control maps.
*
* Synthesized `SDL_Event`s rather than direct calls to the handlers, so the
* demo goes through akgl_controller_handle_event, the control map lookup and
* the binding -- the same path a real key takes. A test that skipped that would
* not be testing the thing that breaks.
*
* @param frameno The frame about to be drawn.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
static akerr_ErrorContext *demo_step(long frameno)
{
PREPARE_ERROR(errctx);
SDL_Event event;
size_t i = 0;
for ( i = 0; i < JRPG_DEMO_STEPS; i++ ) {
if ( JRPG_DEMO_SCRIPT[i].frame != frameno ) {
continue;
}
PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
if ( JRPG_DEMO_SCRIPT[i].down ) {
event.type = SDL_EVENT_KEY_DOWN;
} else {
event.type = SDL_EVENT_KEY_UP;
}
event.key.which = 0;
event.key.key = JRPG_DEMO_SCRIPT[i].key;
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief One frame: events, camera, the library's own update, the box, present.
*
* akgl_game_update is update-every-actor, step the physics, draw the world. It
* does *not* clear or present the target, so the frame is bracketed by the
* backend's own frame_start and frame_end, and anything drawn between the
* update and frame_end lands on top of the world.
*
* @param frameno The frame number, for the demo script.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
static akerr_ErrorContext *frame(long frameno)
{
PREPARE_ERROR(errctx);
SDL_Event event;
akgl_Iterator opflags = {
.flags = AKGL_ITERATOR_OP_UPDATE,
.layerid = 0
};
while ( SDL_PollEvent(&event) ) {
if ( event.type == SDL_EVENT_QUIT ) {
running = false;
}
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
}
if ( demo ) {
PASS(errctx, demo_step(frameno));
// Drive the clock rather than sleeping on it. akgl_physics_simulate
// measures dt from gravity_time, which is a public field, so setting it
// one fixed step into the past makes every frame worth exactly 1/60 of
// a second of simulated time no matter how fast the loop actually runs.
// tests/physics_sim.c does the same, for the same reason: a simulated
// second should not cost a real one.
akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS;
}
PASS(errctx, jrpg_camera_follow());
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
// Every failure path out of akgl_game_update returns with the game state
// mutex still held. SDL's mutexes are recursive and this loop is single
// threaded, so the next frame would not deadlock -- but it would be running
// on top of a frame that did not finish. Treat a failed frame as terminal.
PASS(errctx, akgl_game_update(&opflags));
PASS(errctx, jrpg_textbox_draw());
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
SUCCEED_RETURN(errctx);
}
/**
* @brief Give back what has to be given back, in the order that works.
*
* There is no `akgl_game_shutdown`; teardown is the caller's. Two of the three
* lines here are load-bearing and the third is a deliberate omission:
*
* - akgl_text_unloadallfonts must run **before** `SDL_Quit`. Fonts are kept in
* an SDL property registry, and `SDL_Quit` destroys the registry -- taking
* the last reference to every font still in it with no way left to close
* them.
* - akgl_tilemap_release is **not** called. Its layer loop destroys
* `tilesets[i].texture` rather than `layers[i].texture`, so it double-frees
* every tileset texture and never frees an image layer's, and it NULLs
* nothing, so a second call is a use-after-free. TODO.md, "Known and still
* open" item 2. `SDL_Quit` reclaims the textures correctly; calling the
* function that is supposed to would be worse than not.
* - The pools are static storage. There is nothing to free and the process is
* about to exit.
*/
static void teardown(void)
{
IGNORE(akgl_text_unloadallfonts());
if ( akgl_window != NULL ) {
SDL_DestroyWindow(akgl_window);
akgl_window = NULL;
}
SDL_Quit();
}
int main(int argc, char *argv[])
{
PREPARE_ERROR(errctx);
akgl_Actor *player = NULL;
long frameno = 0;
uint64_t started = 0;
uint64_t spent = 0;
ATTEMPT {
CATCH(errctx, parse_args(argc, argv));
CATCH(errctx, startup());
// Taken here rather than after FINISH: the registry is an SDL property
// set and `SDL_Quit` in teardown() destroys it, so a lookup afterwards
// finds nothing. The actor itself is in a static pool and outlives both.
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
// The loop is the last thing in this ATTEMPT block on purpose. CATCH
// reports failure by `break`ing, and a `break` inside a loop binds to
// the loop rather than to the block -- so a failing frame leaves the
// loop and falls straight into CLEANUP, which is what is wanted. Put
// anything after this loop and it would run after a failure instead.
// src/tilemap.c carries the same note over the same shape.
while ( running ) {
started = SDL_GetTicks();
CATCH(errctx, frame(frameno));
frameno += 1;
if ( (frame_limit > 0) && (frameno >= frame_limit) ) {
running = false;
}
if ( demo == false ) {
spent = SDL_GetTicks() - started;
if ( spent < JRPG_FRAME_BUDGET_MS ) {
SDL_Delay((uint32_t)(JRPG_FRAME_BUDGET_MS - spent));
}
}
}
} CLEANUP {
teardown();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "the JRPG example could not finish");
// Set a flag and act on it after FINISH. A bare `return` from inside a
// HANDLE block leaves before RELEASE_ERROR and leaks the context's pool
// slot; AGENTS.md spells that out, and an example is a bad place to
// teach it wrong.
exitstatus = 1;
} FINISH_NORETURN(errctx);
// Enough for the smoke run to be read rather than merely passed. A run that
// exits 0 having drawn nothing looks exactly like one that worked.
if ( player != NULL ) {
printf("jrpg: %ld frames, player at (%.0f, %.0f)\n", frameno, player->x, player->y);
} else {
printf("jrpg: %ld frames, no player\n", frameno);
}
return exitstatus;
}

188
examples/jrpg/jrpg.h Normal file
View File

@@ -0,0 +1,188 @@
/**
* @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/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 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_

125
examples/jrpg/textbox.c Normal file
View File

@@ -0,0 +1,125 @@
/**
* @file textbox.c
* @brief The dialogue panel: two rectangles and a string, drawn over the world.
*
* Text in libakgl is immediate mode. Every akgl_text_rendertextat call
* rasterizes the string through SDL_ttf, uploads it as a texture, blits it and
* destroys the texture again -- there is no cached glyph atlas and no text
* object to hold on to. That is the wrong shape for a page of static prose
* redrawn sixty times a second and exactly the right shape for this: one short
* line, on screen only while somebody is talking.
*/
#include <stdbool.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/registry.h>
#include <akgl/text.h>
#include "jrpg.h"
/*
* Colour R G B A
*/
static const SDL_Color TEXTBOX_FILL = { 24, 20, 37, 235 };
static const SDL_Color TEXTBOX_EDGE = { 240, 236, 214, 255 };
static const SDL_Color TEXTBOX_INK = { 240, 236, 214, 255 };
/** @brief Pixels between the panel and the edges of the screen. */
#define TEXTBOX_MARGIN 8.0f
/** @brief Panel height in pixels. Two lines of 12pt monospace, plus padding. */
#define TEXTBOX_HEIGHT 56.0f
/** @brief Pixels between the panel's edge and the text inside it. */
#define TEXTBOX_PADDING 8.0f
static char textbox_text[JRPG_TEXTBOX_MAX_TEXT];
static bool textbox_visible = false;
bool jrpg_textbox_showing(void)
{
return textbox_visible;
}
void jrpg_textbox_close(void)
{
textbox_visible = false;
}
akerr_ErrorContext *jrpg_textbox_open(char *text)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text");
// aksl_strncpy, not strncpy: this is a fixed-width field, and strncpy at
// exactly the field width leaves it unterminated. Bounded at one less than
// the field so an over-long line truncates rather than being refused.
PASS(errctx,
aksl_strncpy(
textbox_text,
sizeof(textbox_text),
text,
sizeof(textbox_text) - 1
));
textbox_visible = true;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_textbox_draw(void)
{
PREPARE_ERROR(errctx);
TTF_Font *font = NULL;
SDL_FRect panel;
if ( textbox_visible == false ) {
SUCCEED_RETURN(errctx);
}
// akgl_text_rendertextat refuses the empty string -- SDL_ttf reports "Text
// has zero width" and the library passes that on as AKERR_NULLPOINTER,
// while akgl_text_measure accepts it. The two disagree, so anything drawing
// a line that might be empty checks for it. TODO.md, "Known and still
// open".
if ( textbox_text[0] == '\0' ) {
SUCCEED_RETURN(errctx);
}
// Fonts live in a registry under a name, not in an akgl type. There is no
// handle to keep, and no reference counting either: whoever calls
// akgl_text_unloadfont invalidates every pointer anybody else fetched.
// Fetching it per frame is the cheap way to stay honest about that.
font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, JRPG_FONT_NAME, NULL);
FAIL_ZERO_RETURN(
errctx,
font,
AKGL_ERR_REGISTRY,
"No font registered as \"%s\"",
JRPG_FONT_NAME
);
// Screen coordinates, from the camera's size rather than from a second copy
// of the window dimensions. akgl_render_2d_init put them there.
panel.x = TEXTBOX_MARGIN;
panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN);
panel.h = TEXTBOX_HEIGHT;
panel.y = akgl_camera->h - TEXTBOX_MARGIN - panel.h;
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &panel, TEXTBOX_FILL));
PASS(errctx, akgl_draw_rect(akgl_renderer, &panel, TEXTBOX_EDGE));
PASS(errctx,
akgl_text_rendertextat(
font,
textbox_text,
TEXTBOX_INK,
(int)(panel.w - (2.0f * TEXTBOX_PADDING)),
(int)(panel.x + TEXTBOX_PADDING),
(int)(panel.y + TEXTBOX_PADDING)
));
SUCCEED_RETURN(errctx);
}

458
examples/jrpg/world.c Normal file
View File

@@ -0,0 +1,458 @@
/**
* @file world.c
* @brief The content pipeline: sprites, characters, the town map, and what the
* map's actors do once they exist.
*
* Almost nothing here is game logic. It is the four steps between a directory
* of JSON and a world with people standing in it, in the one order that works,
* plus the two hooks -- a `movementlogicfunc` and a `renderfunc` -- that a game
* has to supply because the library does not.
*/
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/sprite.h>
#include <akgl/tilemap.h>
#include "jrpg.h"
/*
* The three character templates and the twenty-four sprites that dress them.
* The file names are a product of these three tables rather than a list of
* twenty-four strings, because that is what the naming convention *is*: one
* sprite per character, per motion, per facing. A missing combination shows up
* as a load failure naming the file, not as art that silently never appears.
*/
/* Character template Sprite name prefix */
static char *CAST[] = {
"player", /* jrpg_player jrpg_player_* */
"elder", /* jrpg_elder jrpg_elder_* */
"shopkeeper" /* jrpg_shopkeeper jrpg_shopkeeper_* */
};
static char *MOTIONS[] = { "idle", "walk" };
static char *FACINGS[] = { "up", "down", "left", "right" };
#define CAST_COUNT (sizeof(CAST) / sizeof(CAST[0]))
#define MOTION_COUNT (sizeof(MOTIONS) / sizeof(MOTIONS[0]))
#define FACING_COUNT (sizeof(FACINGS) / sizeof(FACINGS[0]))
/** @brief One person in the town who has something to say. */
typedef struct {
char *actor; /**< Registry key, which is the `name` of the map object that spawned them. */
char *line; /**< What they say. */
} jrpg_Townsfolk;
/*
* Actor (the map object's name) What they say
*/
static jrpg_Townsfolk TOWNSFOLK[] = {
{ "elder", "ELDER: The old road north is closed. Nothing to be done about it,\nand nothing beyond it worth the walk." },
{ "shopkeeper", "SHOPKEEPER: Nothing in stock but tiles and good intentions.\nCome back when I have inventory." }
};
#define TOWNSFOLK_COUNT (sizeof(TOWNSFOLK) / sizeof(TOWNSFOLK[0]))
/** @brief How close the player has to stand, in pixels between sprite centres. */
#define TALK_RANGE 64.0f
/*
* The player's footprint, in pixels from the top-left of a 32x32 frame. A
* character stands on the bottom third of their sprite, so that is the part
* that has to fit through a doorway -- testing the whole frame would make every
* corridor two tiles wide.
*
* 0 32
* +------------------------------+ 0
* | |
* | (art) |
* | |
* | +----------------+ | 20 FEET_TOP
* | | footprint | |
* +------+----------------+------+ 30 FEET_TOP + FEET_H
* 6 26
* FEET_X FEET_X + FEET_W
*/
#define FEET_X 6.0f
#define FEET_W 20.0f
#define FEET_TOP 20.0f
#define FEET_H 10.0f
/** @brief Where the party member walks, in pixels relative to the player. */
#define FOLLOWER_OFFSET_X (-14.0f)
#define FOLLOWER_OFFSET_Y (10.0f)
/**
* @brief Load one sprite definition by its three naming components.
*
* @param cast Character stem, e.g. `"player"`.
* @param motion `"idle"` or `"walk"`.
* @param facing `"up"`, `"down"`, `"left"` or `"right"`.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
static akerr_ErrorContext *sprite_load(char *cast, char *motion, char *facing)
{
PREPARE_ERROR(errctx);
char path[PATH_MAX];
int written = 0;
FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast");
FAIL_ZERO_RETURN(errctx, motion, AKERR_NULLPOINTER, "motion");
FAIL_ZERO_RETURN(errctx, facing, AKERR_NULLPOINTER, "facing");
PASS(errctx,
aksl_snprintf(
&written,
path,
sizeof(path),
"%s/sprite_jrpg_%s_%s_%s.json",
JRPG_ASSET_DIR,
cast,
motion,
facing
));
PASS(errctx, akgl_sprite_load_json(path));
SUCCEED_RETURN(errctx);
}
/**
* @brief Load one character definition by its stem.
* @param cast Character stem, e.g. `"player"`.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
static akerr_ErrorContext *character_load(char *cast)
{
PREPARE_ERROR(errctx);
char path[PATH_MAX];
int written = 0;
FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast");
PASS(errctx,
aksl_snprintf(
&written,
path,
sizeof(path),
"%s/character_jrpg_%s.json",
JRPG_ASSET_DIR,
cast
));
PASS(errctx, akgl_character_load_json(path));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_world_load(void)
{
PREPARE_ERROR(errctx);
char path[PATH_MAX];
int written = 0;
size_t c = 0;
size_t m = 0;
size_t f = 0;
// Sprites first. A character JSON names its sprites by registry key and
// akgl_character_load_json refuses one it cannot find, so loading the two
// the other way round fails on every mapping in the file.
for ( c = 0; c < CAST_COUNT; c++ ) {
for ( m = 0; m < MOTION_COUNT; m++ ) {
for ( f = 0; f < FACING_COUNT; f++ ) {
PASS(errctx, sprite_load(CAST[c], MOTIONS[m], FACINGS[f]));
}
}
}
for ( c = 0; c < CAST_COUNT; c++ ) {
PASS(errctx, character_load(CAST[c]));
}
// And the map last: loading it creates an actor for every `actor` object in
// its object layer, and each of those resolves a character by name.
PASS(errctx,
aksl_snprintf(&written, path, sizeof(path), "%s/town.tmj", JRPG_ASSET_DIR));
PASS(errctx, akgl_tilemap_load(path, akgl_gamemap));
// The map declares its own physics -- `physics.model` and zero gravity on
// both axes -- and akgl_tilemap_load builds a backend for it and sets
// use_own_physics. It does not *switch* to it: akgl_game_update simulates
// through the global akgl_physics, whatever that points at. Honouring the
// map is the caller's job, and this is it.
if ( akgl_gamemap->use_own_physics ) {
akgl_physics = &akgl_gamemap->physics;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_world_populate(void)
{
PREPARE_ERROR(errctx);
akgl_Actor *player = NULL;
akgl_Actor *follower = NULL;
int i = 0;
// Every actor the map spawned, including the player.
//
// akgl_actor_initialize sets movement_controls_face, and the default
// facefunc it installs clears every facing bit and then sets the one
// matching a *movement* bit. An NPC has no movement bits, so on the first
// frame its state falls from ALIVE|FACE_DOWN (17) to ALIVE (16), which is
// a state combination no character JSON maps a sprite to -- and an actor
// with no sprite for its state is skipped rather than reported. Every NPC
// in the town disappears on frame one, silently, and so does the player as
// soon as they stop walking.
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( akgl_heap_actors[i].refcount == 0 ) {
continue;
}
akgl_heap_actors[i].movement_controls_face = false;
}
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(
errctx,
player,
AKGL_ERR_REGISTRY,
"town.tmj spawned no actor named \"player\""
);
player->movementlogicfunc = &jrpg_actor_logic_walk;
// The party member. Nothing in the map creates it, and nothing has to: an
// actor is a pool object with a name, and the character it instantiates is
// already registered. This one borrows the elder's template, which is the
// whole point of splitting instance from template.
PASS(errctx, akgl_heap_next_actor(&follower));
PASS(errctx, akgl_actor_initialize(follower, JRPG_FOLLOWER_NAME));
PASS(errctx, akgl_actor_set_character(follower, "jrpg_elder"));
follower->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
follower->movement_controls_face = false;
follower->visible = true;
follower->layer = player->layer;
follower->renderfunc = &jrpg_follower_render;
PASS(errctx, player->addchild(player, follower));
// A child's velocity fields are read as a fixed offset from the parent
// rather than as a velocity: akgl_physics_simulate snaps a child to
// `parent->x + vx` and never simulates it. Set after addchild, because
// addchild is what makes them mean an offset.
follower->vx = FOLLOWER_OFFSET_X;
follower->vy = FOLLOWER_OFFSET_Y;
SUCCEED_RETURN(errctx);
}
/**
* @brief Is the map cell containing this pixel one the player cannot enter?
*
* Two rules, and both of them are the game's rather than the library's. The
* outer ring of the map is solid, because nothing else stops an actor walking
* off the edge of the world. Everything else is the decoration layer: a cell
* with a tile in it is a building, a tree or a lamp post, and a cell with 0 in
* it is open ground.
*
* @param px Position in map pixels along x.
* @param py Position in map pixels along y.
* @return True when the cell blocks movement.
*/
static bool cell_solid(float32_t px, float32_t py)
{
int tx = 0;
int ty = 0;
tx = (int)(px / (float32_t)akgl_gamemap->tilewidth);
ty = (int)(py / (float32_t)akgl_gamemap->tileheight);
if ( (tx <= 0) || (ty <= 0) ||
(tx >= (akgl_gamemap->width - 1)) || (ty >= (akgl_gamemap->height - 1)) ) {
return true;
}
return (akgl_gamemap->layers[JRPG_LAYER_SOLID].data[(ty * akgl_gamemap->width) + tx] != 0);
}
/**
* @brief Would an actor whose frame is at (x, y) have its feet in a wall?
*
* Tests the four corners of the footprint. Four corners is enough only because
* the footprint is smaller than a tile in both directions; a bigger one would
* step over a single-cell wall between two of its corners.
*
* @param x Candidate frame position along x, in map pixels.
* @param y Candidate frame position along y, in map pixels.
* @return True when any corner of the footprint lands in a solid cell.
*/
static bool feet_blocked(float32_t x, float32_t y)
{
float32_t left = x + FEET_X;
float32_t right = x + FEET_X + FEET_W;
float32_t top = y + FEET_TOP;
float32_t bottom = y + FEET_TOP + FEET_H;
return (cell_solid(left, top) ||
cell_solid(right, top) ||
cell_solid(left, bottom) ||
cell_solid(right, bottom));
}
akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "obj->basechar");
// A conversation freezes the world. AKGL_ERR_LOGICINTERRUPT raised from a
// movementlogicfunc means "skip the rest of this tick for this actor", and
// akgl_physics_simulate swallows it in a HANDLE block -- so gravity, drag,
// the velocity recompute and the move are all skipped and the frame carries
// on. Thrust has already been integrated by the time this runs, so it is
// zeroed here; leaving it would let it accumulate while frozen and lurch on
// the first step after the box closes. The movement bits go too, so the
// walk animation stops rather than marching on the spot -- which does mean
// a direction held across the close has to be pressed again.
if ( jrpg_textbox_showing() ) {
obj->tx = 0.0f;
obj->ty = 0.0f;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_ALL);
FAIL_RETURN(
errctx,
AKGL_ERR_LOGICINTERRUPT,
"%s does not move while a conversation is open",
(char *)obj->name
);
}
// Everything the default hook does -- re-copy the character's speed limits,
// turn the movement bits into signed acceleration -- is still wanted. This
// hook adds to it rather than replacing it.
PASS(errctx, akgl_actor_logic_movement(obj, dt));
// Where this step would put us. akgl_physics_simulate has already
// integrated thrust for this step and capped it against the character's
// speed ellipse, and it computes velocity as `e + t` -- and with the town's
// zero gravity and zero drag, `e` stays zero, so `v` is `t`. That is what
// makes the prediction exact rather than approximate. A map with gravity
// would have to account for `ey` here as well.
//
// Axis at a time, so walking into a wall diagonally slides along it rather
// than stopping dead.
if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) {
obj->tx = 0.0f;
obj->ax = 0.0f;
}
if ( feet_blocked(obj->x, obj->y + (obj->ty * dt)) ) {
obj->ty = 0.0f;
obj->ay = 0.0f;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_follower_render(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_Actor *parent = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
// The guard. akgl_physics_simulate has already written this actor's x and y
// as `parent->x + vx` and `parent->y + vy`, which is an absolute world
// position; akgl_actor_render adds the parent's position to it a second
// time. Detaching the parent for the duration of the draw takes the branch
// that does not. CLEANUP puts it back on every path, including the failing
// one -- an actor left with a NULL parent would be simulated as a free
// agent on the very next step.
parent = obj->parent;
obj->parent = NULL;
ATTEMPT {
CATCH(errctx, akgl_actor_render(obj));
} CLEANUP {
obj->parent = parent;
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_camera_follow(void)
{
PREPARE_ERROR(errctx);
akgl_Actor *player = NULL;
float32_t mapw = 0.0f;
float32_t maph = 0.0f;
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, player, AKGL_ERR_REGISTRY, "No actor named \"player\"");
mapw = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth);
maph = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight);
akgl_camera->x = (player->x + 16.0f) - (akgl_camera->w / 2.0f);
akgl_camera->y = (player->y + 16.0f) - (akgl_camera->h / 2.0f);
if ( akgl_camera->x < 0.0f ) {
akgl_camera->x = 0.0f;
}
if ( akgl_camera->y < 0.0f ) {
akgl_camera->y = 0.0f;
}
if ( akgl_camera->x > (mapw - akgl_camera->w) ) {
akgl_camera->x = mapw - akgl_camera->w;
}
if ( akgl_camera->y > (maph - akgl_camera->h) ) {
akgl_camera->y = maph - akgl_camera->h;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_cmhf_talk(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
akgl_Actor *npc = NULL;
size_t i = 0;
float32_t dx = 0.0f;
float32_t dy = 0.0f;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
if ( jrpg_textbox_showing() ) {
jrpg_textbox_close();
SUCCEED_RETURN(errctx);
}
for ( i = 0; i < TOWNSFOLK_COUNT; i++ ) {
npc = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, TOWNSFOLK[i].actor, NULL);
if ( npc == NULL ) {
continue;
}
dx = npc->x - obj->x;
dy = npc->y - obj->y;
if ( sqrtf((dx * dx) + (dy * dy)) > TALK_RANGE ) {
continue;
}
PASS(errctx, jrpg_textbox_open(TOWNSFOLK[i].line));
SUCCEED_RETURN(errctx);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *jrpg_cmhf_ignore(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
SUCCEED_RETURN(errctx);
}