Level generator: add vertical variety with tall levels (46 tiles). Segment generators accept ground_row parameter, SEG_CLIMB connects height zones, transitions inherit predecessor ground row to prevent walkability gaps. Climb segments respect traversal direction. Jetpack boost: add blue flame particles during dash (burst + trail) and continuous idle glow from player back while boost timer is active. Camera: add 30px vertical look-ahead when player velocity exceeds 50 px/s. Fix flame vent pedestal in gen_pit to use ground-relative position instead of map bottom (broken in tall HIGH-zone levels). Add TigerStyle coding guidelines to AGENTS.md adapted for C11. Add tall_test.lvl (40x46) for height zone validation.
11 KiB
AGENTS.md — JNR Engine
Important Files
Before starting any task, always read:
- DESIGN.md — Game design document with vision, mechanics, and level plans
- TODO.md — Current roadmap and next tasks to implement
Project Overview
2D side-scrolling platformer (run-and-gun) written in C11 using SDL2, SDL2_image, and SDL2_mixer.
Binary: jnr. Targets Linux (native), WebAssembly (Emscripten), Windows (MinGW cross-compile).
Build Commands
make # Release build (Linux) → ./jnr
make run # Build + run
make debug # Debug build: -g -O0 -DDEBUG
make DEBUG=1 # Alternative debug flag
make web # WASM build → dist-web/
make web-serve # WASM build + HTTP server on :8080
make windows # Cross-compile → dist-win64/
make clean # Remove all build artifacts
Compiler flags: -Wall -Wextra -std=c11 -I include -I src
There are no test or lint targets. Verify changes by building with make and confirming zero warnings.
Cross-platform prerequisites
- WASM builds require the Emscripten SDK. The
emsdk/directory in the project root is gitignored; source the environment before building:source ~/emsdk/emsdk_env.sh # or wherever emsdk is installed make web - Windows cross-compilation requires MinGW (
x86_64-w64-mingw32-gcc) and vendored SDL2 development libraries indeps/win64/(also gitignored).
Project Structure
include/ Global headers (config.h)
src/
engine/ Engine subsystems (.c/.h pairs): physics, tilemap, entity, camera, etc.
game/ Game logic (.c/.h pairs): player, enemy, level, levelgen, editor, etc.
util/ Header-only utilities: vec2.h, darray.h
main.c Entry point, game mode switching, level transitions
assets/
levels/ .lvl level files (plain text)
sounds/ .wav/.ogg audio
sprites/ PNG spritesheets
tiles/ Tileset PNGs
web/ Emscripten HTML shell
Engine code lives in src/engine/, game code in src/game/. Each subsystem is a .c/.h pair.
Header-only utilities use static inline functions.
Code Style
Formatting
- 4 spaces for indentation (no tabs in source; Makefile uses tabs)
- K&R brace style: opening brace on same line
- Pointer declaration:
Type *name(space before*) constfor input-only pointer params:const Tilemap *map- No-parameter functions use
void:void physics_init(void) - Unused parameters:
(void)param;
Naming Conventions
| Kind | Convention | Example |
|---|---|---|
| Functions | snake_case, module-prefixed |
player_update(), tilemap_load() |
| Types/Structs | PascalCase |
Entity, PlayerData, Tilemap |
| Enums | PascalCase type, UPPER_SNAKE values |
EntityType / ENT_PLAYER |
| Macros/Constants | UPPER_SNAKE_CASE |
MAX_ENTITIES, TILE_SIZE |
| Static (file-scope) vars | s_ prefix |
s_gravity, s_renderer |
| Global vars | g_ prefix |
g_engine, g_spritesheet |
| Local vars | Short snake_case |
dt, pos, em, tx |
| Function pointer types | PascalCase + Fn |
EntityUpdateFn |
Includes
Order within each file:
- Own module header (
"game/player.h") - Other project headers (
"engine/physics.h","game/sprites.h") - Standard library (
<stdlib.h>,<string.h>,<math.h>) - SDL headers (
<SDL2/SDL.h>) - Platform-conditional (
#ifdef __EMSCRIPTEN__)
Paths are forward-slash, relative to src/ or include/: "engine/core.h", "config.h".
Comments
- Section headers:
/* ═══...═══ */box-drawing block - Subsections:
/* ── Name ──────── */light-line style - Inline/doc comments:
/* ... */(C89-style, not//) - Struct field comments: trailing, aligned with whitespace padding
Header Guards
#ifndef JNR_MODULE_H
#define JNR_MODULE_H
...
#endif /* JNR_MODULE_H */
Types
floatfor positions, velocities, timers, physics (notdouble)Vec2(float x, y) for all 2D quantitiesboolfrom<stdbool.h>uint16_tfor tile IDs;uint32_tfor bitfield flags and seedsSDL_Colorfor colors;SDL_Rectfor integer rectangles
Error Handling
- Return
boolfor success/failure - Return
NULLfrom creation functions on failure - Errors to
stderrviafprintf(stderr, "...") - Info to
stdoutviaprintf(...) - Warnings use
"Warning: ..."prefix - Early return on failure; no goto-based cleanup
Memory Management
calloc(1, sizeof(T))for entity data (zero-initialized)free(ptr); ptr = NULL;in destroy callbacksmemset(ptr, 0, sizeof(*ptr))for struct re-initialization- Fixed-size arrays for most collections (entity pool, tile defs)
- Dynamic allocation only for tile layers (
uint16_t *) snprintf/strncpywith explicit size limits for stringsASSET_PATH_MAX(256) for path buffers
TigerStyle Guidelines
Follow the principles from TigerStyle, adapted for this C11 codebase. The design goals are safety, performance, and developer experience, in that order.
Safety
-
Simple, explicit control flow. No recursion. Minimal abstractions — only where they genuinely model the domain. Every abstraction has a cost; prefer straightforward code.
-
Put a limit on everything. All loops and queues must have a fixed upper bound. Use
MAX_ENTITIES,MAX_ENTITY_SPAWNS,MAX_EXIT_ZONES, etc. Where a loop cannot terminate (e.g. the game loop), document why. -
Assert preconditions, postconditions, and invariants. Validate function arguments and return values. A function must not operate blindly on unchecked data. In C, use
assert()or early-return checks withfprintf(stderr, ...)for runtime-recoverable cases. Split compound conditions: preferassert(a); assert(b);overassert(a && b);. -
Assert the positive and negative space. Check what you expect AND what you do not expect. Bugs live at the boundary between valid and invalid data.
-
Static allocation after initialization. Fixed-size arrays for collections (
MAX_ENTITIESpool, tile defs). Dynamic allocation (calloc) only during level loading for tile layers. No allocation or reallocation during gameplay. -
Smallest possible scope for variables. Declare variables close to where they are used. Minimize the number of live variables at any point.
-
Hard limit of ~70 lines per function. When splitting, centralize control flow (switch/if) in the parent function and push pure computation into helpers. Keep leaf functions pure. "Push
ifs up andfors down." -
Zero compiler warnings. All builds must pass
-Wall -Wextrawith zero warnings. -
Handle all errors. Every
fopen,calloc,snprintfreturn must be checked. Early return on failure.
Performance
-
Think about performance in the design phase. The biggest wins (1000x) come from structural decisions, not micro-optimization after the fact.
-
Back-of-the-envelope sketches for the four resources: network, disk, memory, CPU. For a game: frame budget is ~16ms at 60 Hz. Know where time goes.
-
Optimize for the slowest resource first (disk > memory > CPU), weighted by frequency. A cache miss that happens every frame matters more than a disk read that happens once at level load.
-
Batch and amortize. Sprite batching via
renderer_submit(). Particle pools. Tile culling to the viewport. Don't iterate what you don't need to. -
Be explicit with the compiler. Don't rely on the optimizer to fix structural problems. Extract hot inner loops into standalone helpers with primitive arguments when it helps clarity and performance.
Developer Experience
-
Get the nouns and verbs right. Names should capture what a thing is or does. Take time to find the right name. Module-prefixed functions (
player_update,tilemap_load) already enforce this. -
Add units or qualifiers last, sorted by descending significance. For example,
latency_ms_maxnotmax_latency_ms. Related variables then align:latency_ms_min,latency_ms_avg. -
Always say why. Comments explain rationale, not just what the code does. If a design decision isn't obvious, explain the tradeoff. Code alone is not documentation.
-
Comments are sentences. Capital letter, full stop, space after
/*. End-of-line comments can be phrases without punctuation. -
Order matters for readability. Put important things near the top of a file. Public API first, then helpers. In structs, fields before methods.
-
Don't duplicate variables or alias them. One source of truth per value. Reduces the chance of state getting out of sync.
-
Calculate or check variables close to where they are used. Don't introduce variables before they are needed. Minimize the gap between place-of-check and place-of-use.
-
Descriptive commit messages. Imperative mood. Inform the reader about the why, not just the what. The commit message is the permanent record — PR descriptions are not stored in git.
Zero Technical Debt
Do it right the first time. Don't allow known issues to slip through — exponential-complexity algorithms, unbounded loops, potential buffer overflows. What we ship is solid. We may lack features, but what we have meets our design goals. This is the only way to make steady incremental progress.
Architecture Patterns
Entity System
- Fixed pool of
MAX_ENTITIES(512) inEntityManager - Dispatch tables:
update_fn[type],render_fn[type],destroy_fn[type] void *datafor type-specific data (cast in callbacks)- Each entity type:
_register()sets callbacks,_spawn()creates instances - Entity registry maps string names to spawn functions for level loading
Module Pattern
- Public API: declared in header, module-prefixed (
camera_init,camera_follow) - Private helpers:
staticin.conly - File-scope state:
staticvariables withs_prefix - Forward declarations to break circular includes
Level System
.lvlfiles: plain-text directives + tile grid datalevel_load()for handcrafted levels from fileslevel_load_generated()for procedural levels fromTilemapstructs- Exit zones trigger transitions; target strings: file path,
"generate","generate:station", or empty (victory) - Procedural generator: segment-based, theme-driven, difficulty-scaled
Rendering
- Sprite batching: submit to queue via
renderer_submit(), flush layer-by-layer - Draw layers: BG → entities → FG → particles → HUD
- Camera transforms world coords to screen coords
Commit Messages
- Imperative mood, concise
- No co-authored-by or AI attribution
- Example: "Add in-game level editor with auto-discovered tile/entity palettes"
Key Constants (config.h)
| Constant | Value | Notes |
|---|---|---|
SCREEN_WIDTH |
640 | Logical resolution |
SCREEN_HEIGHT |
360 | |
TILE_SIZE |
16 | Pixels per tile |
TICK_RATE |
60 | Fixed timestep Hz |
DEFAULT_GRAVITY |
980.0f | px/s² |
MAX_ENTITIES |
512 | Entity pool size |
MAX_ENTITY_SPAWNS |
128 | Per-level spawn slots |
MAX_EXIT_ZONES |
8 | Per-level exit zones |