Files
libakgl/docs/tutorials/assets/jrpg/town.tmj

1364 lines
12 KiB
Plaintext
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
{
"compressionlevel": -1,
"height": 20,
"infinite": false,
"layers": [
{
"data": [
1,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
3,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
90,
91,
91,
91,
91,
91,
91,
91,
91,
92,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
171,
172,
172,
172,
173,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
198,
199,
199,
199,
200,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
198,
199,
199,
199,
200,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
198,
199,
199,
199,
200,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
225,
226,
226,
226,
227,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
117,
118,
118,
118,
118,
118,
118,
118,
118,
119,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
144,
145,
145,
145,
145,
145,
145,
145,
145,
146,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
28,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
29,
30,
55,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
56,
57
],
"height": 20,
"id": 1,
"name": "ground",
"opacity": 1,
"type": "tilelayer",
"visible": true,
"width": 30,
"x": 0,
"y": 0
},
{
"data": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
233,
0,
0,
0,
0,
234,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
260,
0,
0,
0,
0,
261,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
73,
73,
73,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
100,
284,
100,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
234,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
261,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
233,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
260,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
73,
73,
73,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
100,
284,
100,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
234,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
261,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
233,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
233,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
260,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
260,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"height": 20,
"id": 2,
"name": "decoration",
"opacity": 1,
"type": "tilelayer",
"visible": true,
"width": 30,
"x": 0,
Delete the JRPG's collision too, and wall the map with proxies 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
2026-08-02 07:46:19 -04:00
"y": 0,
"properties": [
{
"name": "collidable",
"type": "bool",
"value": true
}
]
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
},
{
"draworder": "topdown",
"id": 3,
"name": "actors",
"objects": [
{
"height": 32,
"id": 1,
"name": "player",
"properties": [
{
"name": "character",
"type": "string",
"value": "jrpg_player"
},
{
"name": "state",
"type": "int",
"value": 17
}
],
"rotation": 0,
"type": "actor",
"visible": true,
"width": 32,
"x": 224,
"y": 256
},
{
"height": 32,
"id": 2,
"name": "shopkeeper",
"properties": [
{
"name": "character",
"type": "string",
"value": "jrpg_shopkeeper"
},
{
"name": "state",
"type": "int",
"value": 17
}
],
"rotation": 0,
"type": "actor",
"visible": true,
"width": 32,
"x": 80,
"y": 128
},
{
"height": 32,
"id": 3,
"name": "elder",
"properties": [
{
"name": "character",
"type": "string",
"value": "jrpg_elder"
},
{
"name": "state",
"type": "int",
"value": 17
}
],
"rotation": 0,
"type": "actor",
"visible": true,
"width": 32,
"x": 272,
"y": 112
}
],
"opacity": 1,
"type": "objectgroup",
"visible": true,
"x": 0,
"y": 0
}
],
"nextlayerid": 4,
"nextobjectid": 4,
"orientation": "orthogonal",
"properties": [
{
"name": "physics.model",
"type": "string",
"value": "arcade"
},
{
"name": "physics.gravity.x",
"type": "float",
"value": 0.0
},
{
"name": "physics.gravity.y",
"type": "float",
"value": 0.0
}
],
"renderorder": "right-down",
"tiledversion": "1.8.2",
"tileheight": 16,
"tilesets": [
{
"columns": 27,
"firstgid": 1,
"image": "tiles.png",
"imageheight": 288,
"imagewidth": 432,
"margin": 0,
"name": "rpg_urban",
"spacing": 0,
"tilecount": 486,
"tileheight": 16,
"tilewidth": 16
}
],
"tilewidth": 16,
"type": "map",
"version": "1.8",
"width": 30
}