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
The tutorial asset contract
What examples/sidescroller/ and examples/jrpg/ are drawing, and the exact
numbers they have to use. Licensing is in PROVENANCE.md; refreshing the art
from upstream is scripts/fetch_tutorial_assets.sh.
Every claim below was checked against src/, not against header prose, and the
whole set was loaded through the real library -- akgl_sprite_load_json,
akgl_character_load_json, akgl_tilemap_load -- before being committed.
Geometry
| Thing | Value | Why it is that value |
|---|---|---|
| Tile | 16x16 | Both tilesets ship on a 16x16 grid |
| Tileset spacing, margin | 0, 0 | akgl_tilemap_compute_tileset_offsets gets nothing else right; see below |
| Character frame | 32x32 | The fixed contract for these tutorials |
| Art inside a frame | 16x16 | Composited at (8, 16): centred, sitting on the cell's bottom edge |
| Frames per sheet row | all | Every sheet is a single row, so coords_for_frame never has to wrap |
| Frames per animation | max 16 | AKGL_SPRITE_MAX_FRAMES; a 17th is AKERR_OUTOFBOUNDS at load |
| Frame id | 0..255 | uint8_t; a larger id is AKERR_OUTOFBOUNDS at load |
The one alignment rule. akgl_actor_render draws into a rectangle of
sprite->width by sprite->height with its top-left at the actor's x, y.
Because every frame's art is bottom-centred in its cell, an actor at (x, y)
has its feet at y + 32 and its 16 px body spanning x + 8 to x + 23. Ground
contact, pickup tests and hitboxes should use that inner rectangle, not the
32x32 cell.
Sidescroller sheets
sidescroller/player.png -- 192x32, six frames.
| Frame | Facing | Pose | Used by |
|---|---|---|---|
| 0 | right | contact | ss_player_idle_right, ss_player_run_right |
| 1 | right | passing, airborne | ss_player_run_right, ss_player_jump_right |
| 2 | right | opposite contact | ss_player_run_right |
| 3 | left | contact (mirror of 0) | ss_player_idle_left, ss_player_run_left |
| 4 | left | passing, airborne (mirror of 1) | ss_player_run_left, ss_player_jump_left |
| 5 | left | opposite contact (mirror of 2) | ss_player_run_left |
The left-facing frames exist as their own art because akgl_actor_render passes
SDL_FLIP_NONE to draw_texture unconditionally (src/actor.c). There is no
way to ask the library for a mirrored blit.
sidescroller/coin.png -- 32x32, one frame. The pack ships no rotation frames.
sidescroller/hazard.png -- 128x32, four frames.
| Frame | What | Used by |
|---|---|---|
| 0 | red blob, ground, pose A | ss_hazard_blob |
| 1 | red blob, ground, pose B | ss_hazard_blob |
| 2 | moth, flying, wings up | ss_hazard_moth |
| 3 | moth, flying, wings down | ss_hazard_moth |
JRPG sheets
jrpg/player.png, jrpg/npc_shopkeeper.png and jrpg/npc_elder.png are
384x32, twelve frames, and share one layout.
| Frames | Facing | Poses | Idle sprite uses | Walk sprite uses |
|---|---|---|---|---|
| 0-2 | down | stand, step A, step B | 0 | 1, 0, 2, 0 |
| 3-5 | left | stand, step A, step B | 3 | 4, 3, 5, 3 |
| 6-8 | right | stand, step A, step B | 6 | 7, 6, 8, 6 |
| 9-11 | up | stand, step A, step B | 9 | 10, 9, 11, 9 |
The stand frame between the two steps is what makes it read as a walk rather than a shuffle; it is the same three-frame cycle the source art was drawn for.
States
A character's sprite map is keyed by SDL_itoa(state) and looked up with
SDL_GetPointerProperty (akgl_character_sprite_get), so the match is on the
whole integer, not on a mask test. A state the character has no exact entry
for makes the actor invisible for that frame -- actor_visible treats
AKERR_KEY as "nothing to draw", which is an answer, not an error.
These are the values the library's own input handlers produce.
akgl_actor_cmhf_<dir>_on clears every FACE_* and MOVING_* bit before
setting its own pair, and _off clears only its MOVING_* bit, so the facing
survives the key release and no two MOVING_* bits are ever set at once.
| State | Bits | Sidescroller sprite | JRPG sprite |
|---|---|---|---|
| 16 | ALIVE | (coin, hazards) | -- |
| 17 | ALIVE, FACE_DOWN | ..._idle_right |
..._idle_down |
| 18 | ALIVE, FACE_LEFT | ..._idle_left |
..._idle_left |
| 20 | ALIVE, FACE_RIGHT | ..._idle_right |
..._idle_right |
| 24 | ALIVE, FACE_UP | -- | ..._idle_up |
| 146 | ALIVE, FACE_LEFT, MOVING_LEFT | ..._run_left |
..._walk_left |
| 276 | ALIVE, FACE_RIGHT, MOVING_RIGHT | ..._run_right |
..._walk_right |
| 530 | ALIVE, FACE_LEFT, MOVING_UP | ..._jump_left |
-- |
| 532 | ALIVE, FACE_RIGHT, MOVING_UP | ..._jump_right |
-- |
| 536 | ALIVE, FACE_UP, MOVING_UP | ..._jump_right |
..._walk_up |
| 658 | ALIVE, FACE_LEFT, MOVING_LEFT, UP | ..._jump_left |
-- |
| 788 | ALIVE, FACE_RIGHT, MOVING_RIGHT, UP | ..._jump_right |
-- |
| 1041 | ALIVE, FACE_DOWN, MOVING_DOWN | -- | ..._walk_down |
The four jump states are there because a sidescroller that routes its jump
through akgl_actor_cmhf_up_on lands on 536 -- that handler clears FACE_RIGHT
and sets FACE_UP, which in a side view is not a facing at all. A game that
sets ey directly instead never reaches those states, and nothing breaks
either way.
State names in character JSON are the prefixed spellings from
src/actor_state_string_names.c: AKGL_ACTOR_STATE_ALIVE, not
ACTOR_STATE_ALIVE. util/assets/littleguy.json uses the old unprefixed names
and velocity_x, and does not load against the current library. Do not copy it.
Maps
Both maps are Tiled 1.8 TMJ with the tileset embedded.
akgl_tilemap_load reads columns, firstgid, tilecount, image,
imagewidth, imageheight, margin, spacing, tilewidth, tileheight and
name straight out of each element of the map's tilesets array
(akgl_tilemap_load_tilesets_each). The string source appears nowhere in
src/tilemap.c and nothing in the library ever opens a .tsj, so an external
tileset reference fails with AKERR_KEY on the missing columns. README.md
in the repository root says the opposite; it is wrong.
| Map | Cells | Layers | Objects |
|---|---|---|---|
sidescroller/level1.tmj |
40x15 | background (tile), terrain (tile), actors | 7 |
jrpg/town.tmj |
30x20 | ground (tile), decoration (tile), actors | 3 |
Global tile ids are (row * columns) + column + 1; 0 means an empty cell and
is skipped. sidescroller/tiles.png has ten columns and 60 tiles;
jrpg/tiles.png has twenty-seven columns and 486 tiles, the last four columns
of which are the character art the sheets above were cut from. Those cells are
never referenced by town.tmj.
Other things the loader insists on, all of them checked:
- Every object needs a
typestring, including ones that are not actors. A missingtypefails the whole load, not just that object. - An actor object needs a non-empty
name, acharacterstring property and astateint property. The string-array form ofstateworks in character JSON and is not accepted here. - The
characternamed must already be inAKGL_REGISTRY_CHARACTER, which means every sprite and character JSON has to be loaded before the map. - A map's
propertiesare optional; if present,physics.modelmust name a backend that exists (nullorarcade) or the load fails. Gravity and drag keys arefloat. - Tileset image paths resolve through
akgl_path_relativeagainst the map's own directory. Image-layer paths do not -- those go through a plain"%s/%s"join, so an absolute path there does not work. Keep every path relative.
Physics
The sidescroller map asks for the arcade backend with physics.gravity.y
900.0 and physics.drag.y 1.5. There is no terminal velocity in the backend;
ey approaches gravity_y / drag_y, so those two numbers set it to 600 px/s.
The JRPG map asks for arcade with both gravity components at 0.0.
ss_player has speed_y and acceleration_y of 0.0 on purpose: a zero top
speed on an axis means akgl_physics_simulate zeroes that axis's thrust
outright, so holding a vertical direction cannot make the player fly. A jump
belongs in ey.