Files
libakgl/examples/jrpg/textbox.c

126 lines
3.9 KiB
C
Raw Normal View History

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>
2026-08-01 20:59:00 -04:00
/**
* @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);
}