EXTEND / Entity Walkthrough
Entity Walkthrough
Trace a collectible through parser, runtime, editor, undo and tests.
Entity Walkthrough: From TOML to a Collectible
Start with Learning Path labs 1–5. This walkthrough follows the existing coin end-to-end, then gives the complete integration map for adding a second coin-like collectible named Token. It is an exercise, not an entity already included in the game.
1. Follow one existing coin
Open levels/labs/01_collision.toml. Its [[coins]] record contains x/y values.
src/shared/serializer_parse.cchecks the key, element types and maximum count.serializer_load_collectibles()copies those values intoLevelDef.coins.level_validate_runtime()checks placement bounds.src/levels/level_loader.cconverts each immutableCoinPlacementinto an active runtimeCoin.game_collide()tests the player’s hitbox, deactivates the coin and callsgame_award_score().coins_render()draws only active entries, subtracting camera x at draw time.- The editor saves the same
LevelDefthroughsrc/shared/serializer_save.c.
The parser does not live in level_loader.c: that module translates an
already validated model into runtime state. The shared serializer belongs to
both applications.
2. Define the Token contract
For this exercise, Token is a bounded x/y collectible worth 250 points, uses a
tinted existing coin texture, and does not restore health. Put the values in
named constants. Texture tint must be restored after drawing so shared coins
are not recolored. Keep sprite ownership in TextureResources; a Token borrows
that texture and must never free it.
/* src/collectibles/token.h */
#pragma once
#include "../shared/graphics.h"
#define MAX_TOKENS 32
#define TOKEN_SCORE 250
typedef struct { float x, y; int active; } Token;
void tokens_render(const Token *items, int count, Texture2D *texture, int camera_x);
Implement tokens_render() by following coins_render() in
src/collectibles/coin.c: skip inactive entries, build a 16×16 destination,
subtract camera_x and render the borrowed texture. The Makefile discovers a
new .c in src/collectibles/ automatically.
3. Add data and round-trip support
| File | Required change |
|---|---|
src/levels/level.h |
Add TokenPlacement { float x, y; }, bounded placement array and count to LevelDef |
src/game.h |
Include the Token header; add runtime array and count to GameState |
src/shared/serializer_parse.c |
Register tokens as an array of XY tables bounded by MAX_TOKENS |
src/shared/serializer_load_collectibles.c |
Add LOAD_XY_ARRAY("tokens", token_count, MAX_TOKENS, tokens) |
src/shared/serializer_save.c |
Emit every token as [[tokens]] with x/y using the existing float formatter |
src/levels/level_validate.c |
Validate the count before indexing and validate each placement’s world bounds |
tools/validate_levels.py |
Mirror the schema/count/placement contract in the Python validator |
Keep format_version = 1 at the document root. Unknown fields must still be
rejected; do not loosen schema validation to make the exercise pass.
4. Add runtime behavior
In level_loader.c, add a small load_tokens() that copies placement positions
and sets active = 1. Call it from both initial load and death/reset paths.
Wire tokens_render() into src/render/game_render.c beside coins.
In game_collide(), follow the coin collision loop. On overlap, clear active,
call game_award_score(gs, TOKEN_SCORE) and optionally play the existing pickup
sound. Use that score helper rather than adding directly: it implements bonus
lives and saturation. Keep the existing early returns after death/respawn so a
stale hitbox cannot collect an item at the previous location.
Add Token hitboxes to src/core/debug.c. A paused frame must draw without
mutating the Token. If you later supply a distinct sprite, wire its ownership
into game_resources.c, require it when used, and release it exactly once.
5. Complete editor integration
Follow the existing ENT_COIN cases; each row has a distinct responsibility:
| File | Integration |
|---|---|
editor.h, entity_meta.c |
Add ENT_TOKEN, display/category metadata, palette ordering, count and singleton handling |
tools.c |
Position access, maximum count, hit testing, placement defaults and deletion |
canvas.c, palette.c |
Actual preview, placement ghost, selection bounds and thumbnail |
properties.c |
x/y property controls using staged field commits |
undo.h |
Add Token placement storage to PlacementData |
editor_undo_apply.c |
Capture a Token snapshot; use the existing array operation pattern for undo/redo |
editor_clipboard.c |
Copy/paste the new placement and enforce the count limit |
editor_session.c |
Include active token placements in the document hash |
These editor paths all operate on LevelDef, not live runtime objects. Copy/paste
and undo must preserve selection indices after array insertion/removal. Do not
change fields directly from widgets without the established change-tracking
callbacks, or a visually successful edit may disappear from undo history.
6. Prove the whole path
Create an editor copy of the collision lab, place two Tokens, edit one, save, reopen, playtest and collect them. Undo/redo placement, deletion and a property edit; save and verify that returning to the save point clears the dirty marker.
Extend the existing rich serializer fixture with Tokens and extend its comparison to prove the round-trip. Add one editor operation case only if the generic coverage does not exercise Token. A gameplay case should prove one pickup awards 250 points exactly once. Reuse the count/bounds fixtures for invalid data.
make builder test CC=clang
make validate-levels
make level-catalog content-inventory
make docs-drift
make sanitize CC=clang
Update the collectible count group in tools/generate_level_catalog.py, the
source map and relevant manual references. You are finished when Token works
through file load, runtime, editor, undo, clipboard, validation and save/load—not
merely when the sprite appears on screen.