# 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__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 `type` string**, including ones that are not actors. A missing `type` fails the whole load, not just that object. - An actor object needs a non-empty `name`, a `character` **string** property and a `state` **int** property. The string-array form of `state` works in character JSON and is not accepted here. - The `character` named must already be in `AKGL_REGISTRY_CHARACTER`, which means every sprite and character JSON has to be loaded before the map. - A map's `properties` are optional; if present, `physics.model` must name a backend that exists (`null` or `arcade`) or the load fails. Gravity and drag keys are `float`. - Tileset image paths resolve through `akgl_path_relative` against 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`.