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:
125
examples/jrpg/textbox.c
Normal file
125
examples/jrpg/textbox.c
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user