Builder Manual Play
Builder Manual / Developer Guide

GUIDE / Developer Guide

Developer Guide

Coding conventions, safe extension patterns, entity workflow, and contribution rules.

Developer Guide


This guide covers the patterns and conventions used in Super Mango and explains how to extend the game safely and consistently.


Coding Conventions

Language and Standard

  • C11 (-std=c11)
  • Compiler: clang (default), gcc compatible

Naming

CategoryConventionExample
Filessnake_caseplayer.c, coin.h
Functionsmodule_verbplayer_init, coins_render
Struct typesPascalCase via typedefPlayer, GameState, Coin
Enum valuesUPPER_SNAKE_CASEANIM_IDLE, ANIM_WALK
Constants (#define)UPPER_SNAKE_CASEFLOOR_Y, TILE_SIZE
Local variablessnake_casedt, frame_ms, elapsed
Assetssnake_case under assets/sprites/<category>/player/player.png, collectibles/coin.png, entities/spider.png
Soundscomponent_descriptor.wav under assets/sounds/<category>/player/player_jump.wav, collectibles/coin.wav, entities/bird.wav

Memory and Safety Rules

  • Every pointer must be set to NULL immediately after freeing. (SDL_Destroy* and free() on NULL are no-ops, preventing double-free crashes.)
  • Error paths call SDL_GetError() / IMG_GetError() / Mix_GetError() and write to stderr.
  • Resources are always freed in reverse init order.
  • Use float for positions and velocities; cast to int only at render time (SDL_Rect fields are int).

Coordinate System

All game-object positions and sizes live in logical space (400x300). Never use WINDOW_W / WINDOW_H for game math — SDL scales the logical canvas to the OS window automatically.

See Constants Reference for all defined constants.


Adding a New Entity

Most active entities follow this lifecycle pattern:

entity_init    -> set initial state (textures often live shared in GameState)
entity_update  -> move, apply physics, detect events
entity_render  -> draw to renderer
entity_cleanup -> SDL_DestroyTexture, set to NULL

Collectibles and simple decorations may use lighter helpers. For example, coins store only placement state in Coin and render through coins_render() using a shared texture from GameState.

Active entities may also expose:

entity_handle_input   -> if player-controlled
entity_animate        -> static helper, called from entity_update

Step-by-Step

1. Create the header — coin-like collectible example

#pragma once
#include <SDL.h>

#define MAX_COINS       64
#define COIN_DISPLAY_W  16
#define COIN_DISPLAY_H  16
#define COIN_SCORE     100

typedef struct {
    float x;      /* logical position (top-left) */
    float y;
    int   active; /* 1 = visible, 0 = collected */
} Coin;

void coins_render(const Coin *coins, int count,
                  SDL_Renderer *renderer, SDL_Texture *tex, int cam_x);

2. Create the implementation — src/collectibles/coin.c

#include "collectibles/coin.h"

void coins_render(const Coin *coins, int count,
                  SDL_Renderer *renderer, SDL_Texture *tex, int cam_x) {
    if (!tex) return;

    for (int i = 0; i < count; i++) {
        if (!coins[i].active) continue;

        SDL_Rect dst = {
            (int)(coins[i].x - cam_x),
            (int)coins[i].y,
            COIN_DISPLAY_W,
            COIN_DISPLAY_H
        };
        SDL_RenderCopy(renderer, tex, NULL, &dst);
    }
}

The Makefile picks up coin.c automatically from the src/collectibles/ subdirectory — no Makefile changes needed.

3. Add texture to TextureResources in game.h

Textures are loaded in game_init() and stored under gs->textures. The entity array and count live directly in GameState:

#include "collectibles/coin.h"

typedef struct {
    // ... existing fields ...
    TextureResources textures; /* contains SDL_Texture *coin */
    Coin coins[MAX_COINS];    /* fixed-size array -- simple and cache-friendly */
    int  coin_count;          /* how many are currently active */
} GameState;

4. Wire up in the runtime core

// src/core/game_resources.c -- load shared texture:
gs->textures.coin = IMG_LoadTexture(gs->renderer, "assets/sprites/collectibles/coin.png");
if (!gs->textures.coin) {
    fprintf(stderr, "Failed to load coin.png: %s\n", IMG_GetError());
    return -1;
}

// level_loader.c -- populate array from TOML placements:
gs->coins[i] = (Coin){ .x = def->coins[i].x, .y = def->coins[i].y, .active = 1 };
gs->coin_count = def->coin_count;

// focused runtime helper render section, in the correct layer order:
coins_render(gs->coins, gs->coin_count, gs->renderer, gs->textures.coin, (int)gs->camera.x);

// src/core/game_lifecycle.c cleanup path, before SDL_DestroyRenderer:
DESTROY_TEX(gs->textures.coin);

Use the focused runtime module that owns the behavior: resource loading belongs in src/core/game_resources.c, lifecycle orchestration in src/core/game_lifecycle.c, per-frame update orchestration in src/core/game_update.c and its specialized helpers, and collision/pickup behavior in src/collision/.

5. Add to a TOML level file

Entity spawn positions are defined in TOML level files in the levels/ directory. Add your entity’s array table entry there:

# In levels/your_level.toml:
[[coins]]
x = 120.0
y = 180.0

[[coins]]
x = 200.0
y = 140.0

Then extend level_loader.c to parse the new array table and populate the GameState array (or call your _init function when the entity owns richer runtime state). See level_design for the full TOML schema and Level Design — TOML Reference for placement examples for every entity type.

You can also use the visual level editor (make run-editor) to place entities interactively without writing TOML by hand.

6. Add debug hitbox — src/core/debug.c

Every entity must have hitbox visualization in core/debug.c:

// In debug_render:
for (int i = 0; i < gs->coin_count; i++) {
    if (!gs->coins[i].active) continue;
    SDL_Rect hb = { (int)gs->coins[i].x, (int)gs->coins[i].y,
                    COIN_DISPLAY_W, COIN_DISPLAY_H };
    SDL_SetRenderDrawColor(gs->renderer, 255, 255, 0, 128);
    SDL_RenderDrawRect(gs->renderer, &hb);
}

Also add debug_log calls in the module that owns the event, such as src/collision/game_collision.c, src/core/game_update.c, or the relevant focused runtime helper.


Adding Physics to an Entity

Use the same pattern as player_update:

/* Apply gravity while airborne */
if (!entity->on_ground) {
    entity->vy += GRAVITY * dt;
}

/* Integrate position */
entity->x += entity->vx * dt;
entity->y += entity->vy * dt;

/* Floor collision */
if (entity->y + entity->h >= FLOOR_Y) {
    entity->y        = (float)(FLOOR_Y - entity->h);
    entity->vy       = 0.0f;
    entity->on_ground = 1;
} else {
    entity->on_ground = 0;
}

/* Horizontal clamp */
if (entity->x < 0.0f)                entity->x = 0.0f;
if (entity->x > GAME_W - entity->w)  entity->x = (float)(GAME_W - entity->w);

GRAVITY, FLOOR_Y, GAME_W, and GAME_H are all defined in game.h and available to any file that includes it. See Constants Reference for values.


Adding a New Sound Effect

All sound files are .wav format, named with the convention component_descriptor.wav:

SoundFile
Player jumpplayer_jump.wav
Player hitplayer_hit.wav
Coin collectcoin.wav
Bouncepadbouncepad.wav
Birdbird.wav
Fishfish.wav
Spiderspider.wav
Axe trapaxe_trap.wav

Steps to add a new sound:

  1. Place .wav in assets/sounds/<category>/.
  2. Add Mix_Chunk *<name>; to AudioResources in game.h.
  3. Load in game_init (non-fatal — warn but continue):
gs->audio.<name> = Mix_LoadWAV("assets/sounds/<category>/<name>.wav");
if (!gs->audio.<name>) {
    fprintf(stderr, "Warning: could not load <name>.wav: %s\n", Mix_GetError());
}
  1. Free in game_cleanup:
FREE_CHUNK(gs->audio.<name>);
  1. Play wherever needed:
if (gs->audio.<name>) Mix_PlayChannel(-1, gs->audio.<name>, 0);

See Sounds for the full list of available sound files.


Adding Background Music

Background music is loaded via Mix_LoadMUS (not Mix_LoadWAV). Runtime levels provide the active music path through TOML:

// Load from current LevelDef
gs->audio.music = Mix_LoadMUS(def->music_path);

// Play (looping)
Mix_PlayMusic(gs->audio.music, -1);
Mix_VolumeMusic(64);  // 50% -- adjust as needed

// Cleanup
Mix_HaltMusic();
Mix_FreeMusic(gs->audio.music);
gs->audio.music = NULL;

Adding HUD / Text Rendering

SDL2_ttf is already initialized in main.c. The font is in assets/fonts/.

// Load font
TTF_Font *font = TTF_OpenFont("assets/fonts/round9x13.ttf", 13);
if (!font) { fprintf(stderr, "TTF_OpenFont: %s\n", TTF_GetError()); }

// Render text to a surface, then upload to a texture
SDL_Color white = {255, 255, 255, 255};
SDL_Surface *surf = TTF_RenderText_Solid(font, "Score: 0", white);
SDL_Texture *tex  = SDL_CreateTextureFromSurface(renderer, surf);
int text_w = surf->w;
int text_h = surf->h;
SDL_FreeSurface(surf);

// Draw the texture
SDL_Rect dst = {10, 10, text_w, text_h};
SDL_RenderCopy(renderer, tex, NULL, &dst);

// Cleanup
SDL_DestroyTexture(tex);
TTF_CloseFont(font);

The HUD renders hearts (lives), life counter, and score. It is drawn after all game entities so it always appears on top.


Render Layer Order

Always draw in painter’s algorithm order (back to front). The game currently uses 32 layers:

 1. Parallax background    (`assets/sprites/backgrounds/*.png` layers)
 2. Platforms              (`assets/sprites/levels/*_platform.png`, 9-slice pillars)
 3. Floor tiles            (level floor tile at FLOOR_Y, with floor-gap openings)
 4. Float platforms        (`assets/sprites/surfaces/float_platform.png`)
 5. Spike rows             (`assets/sprites/hazards/spike.png`)
 6. Spike platforms        (`assets/sprites/hazards/spike_platform.png`)
 7. Bridges                (`assets/sprites/surfaces/bridge.png`)
 8. Bouncepads medium      (`assets/sprites/surfaces/bouncepad_medium.png`)
 9. Bouncepads small       (`assets/sprites/surfaces/bouncepad_small.png`)
10. Bouncepads high        (`assets/sprites/surfaces/bouncepad_high.png`)
11. Rails                  (`assets/sprites/surfaces/rail.png`)
12. Vines                  (`assets/sprites/surfaces/vine_green.png` / `vine_brown.png`)
13. Ladders                (`assets/sprites/surfaces/ladder.png`)
14. Ropes                  (`assets/sprites/surfaces/rope.png`)
15. Coins                  (`assets/sprites/collectibles/coin.png`)
16. Yellow stars           (`assets/sprites/collectibles/star_yellow.png`)
17. Last star              (`assets/sprites/collectibles/last_star.png`)
18. Blue/fire flames       (`assets/sprites/hazards/blue_flame.png` / `fire_flame.png`)
19. Fish                   (`assets/sprites/entities/fish.png`)
20. Faster fish            (`assets/sprites/entities/faster_fish.png`)
21. Water                  (`assets/sprites/foregrounds/water.png`)
22. Spike blocks           (`assets/sprites/hazards/spike_block.png`)
23. Axe traps              (`assets/sprites/hazards/axe_trap.png`)
24. Circular saws          (`assets/sprites/hazards/circular_saw.png`)
25. Spiders                (`assets/sprites/entities/spider.png`)
26. Jumping spiders        (`assets/sprites/entities/jumping_spider.png`)
27. Birds                  (`assets/sprites/entities/bird.png`)
28. Faster birds           (`assets/sprites/entities/faster_bird.png`)
29. Player                 (`assets/sprites/player/player.png`)
30. Fog                    (`assets/sprites/foregrounds/fog_1.png` / `fog_2.png`)
31. HUD                    (hearts, lives, score -- always on top)
32. Debug overlay          (FPS, hitboxes, event log -- when --debug)

See Architecture for details on the render pipeline.


Sprite Sheet Workflow

To analyze a new sprite sheet:

python3 .agents/scripts/analyze_sprite.py assets/sprites/<category>/<sprite>.png

Frame math:

source_x = (frame_index % num_cols) * frame_w
source_y = (frame_index / num_cols) * frame_h

Standard animation row layout (most assets in this pack):

RowAnimationNotes
0Idle1-4 frames, subtle
1Walk / Run6-8 frames, looping
2Jump (up)2-4 frames, one-shot
3Fall / Land2-4 frames
4Attack4-8 frames, one-shot
5Death / Hurt4-6 frames, one-shot

See Assets for sprite sheet dimensions and Player Module for animation state machine details.


Checklist: Adding a New Entity

  • Create src/<category>/<entity>.h with struct and function declarations (e.g. src/entities/, src/collectibles/, src/hazards/, src/surfaces/)
  • Create src/<category>/<entity>.c with init, update, render, cleanup
  • Add #include "<category>/<entity>.h" to game.h
  • Add texture pointer to TextureResources, plus entity array and count to GameState (by value, not pointer)
  • Load texture in the resource-loading path (src/core/game_resources.c)
  • Call <entity>_init in game_init
  • Call <entity>_update from the relevant src/core/ update helper
  • Call <entity>_render from the relevant src/core/ render helper (correct layer order)
  • Call <entity>_cleanup in game_cleanup (before SDL_DestroyRenderer)
  • Set all freed pointers to NULL
  • Add entity placement to a TOML level file in levels/ (or use the visual level editor)
  • Add hitbox visualization in core/debug.c
  • Add debug_log calls in the module that owns significant entity events
  • Build game with make — no Makefile changes needed for new .c files in existing source directories
  • Build editor with make editor if editor placement/schema behavior changed
  • Run make test
  • Run make validate-levels after any level/schema/editor serializer change
  • Test with --debug flag to verify hitboxes render correctly
  • Run relevant docs lint/build command when documentation pages changed