20 KiB
Jump 'n Run - Game Design Document
Concept
2D side-scrolling platformer with run-and-gun combat. Inspired by Jazz Jackrabbit 2, Metal Slug, Mega Man, and classic 90s side-scrollers.
Theme: Sci-fi / space western (think Cowboy Bebop). The player is a bounty hunter traveling between planets and space stations, taking on jobs that play out as platformer levels.
Game Structure
Two Main Modes
1. World Map (Spacecraft Navigation)
- Top-down or side-view map showing planets, stations, asteroids
- Player pilots a spacecraft between locations
- Each location is a level (or hub with multiple levels)
- Map could be a simple node graph or free-flight between points
- Unlocking new areas as the story progresses
- Ship could have upgrades (fuel range, shields, scanner)
2. Platformer Levels
- Core gameplay: run, jump, shoot, explore
- Each level is a self-contained planet/station/ship with its own atmosphere
- Levels end with reaching an exit zone (or defeating a boss)
- Collectibles: currency (bounty credits), health pickups, weapon upgrades
- Optional objectives for bonus rewards
Atmosphere System
Each level defines its own atmosphere, affecting gameplay feel:
| Property | Effect | Example Values |
|---|---|---|
GRAVITY |
Fall speed and jump arc | 980 (earth), 400 (moon) |
WIND |
Constant horizontal force on entities | -50 to 50 px/s^2 |
STORM |
Visual effect + periodic strong wind gusts | 0 (calm) to 3 (severe) |
DRAG |
Air resistance (underwater, thick atmo) | 0.0 (none) to 0.9 |
BG_COLOR |
Background clear color | hex color |
PARALLAX_FAR |
Far background image path | assets/bg/stars.png |
PARALLAX_NEAR |
Near background image path | assets/bg/nebula.png |
MUSIC |
Level music track | assets/music/level1.ogg |
PALETTE |
Color mood (warm, cold, toxic, void) | tint/filter values |
Already implemented: GRAVITY, WIND, BG_COLOR, MUSIC, PARALLAX_FAR, PARALLAX_NEAR (all per-level). Parallax backgrounds are procedurally generated (starfield + nebula) when no image path is specified.
Player
- Size: 12x16 px hitbox, 16x16 sprite
- Movement: Run, jump (variable height, coyote time, jump buffer)
- Dash: C key, directional (horizontal, up, diagonal, down while airborne). Brief i-frames during dash. 0.15s duration, 0.4s cooldown.
- Combat: Shoot projectiles (X or Space), directional aiming with UP key (straight up, or diagonal when combined with LEFT/RIGHT)
- Camera: Holding UP while standing still pans the camera upward
- Health: 3 HP (expandable), invincibility frames + knockback on hit
- Death / Respawn: Death animation plays, then 1s delay before respawning at level spawn with full HP, charges, and brief invincibility. Falling off the level kills instantly with 0.3s delay.
- Future abilities:
- Wall slide / wall jump
- Weapon switching (multiple projectile types from the def system)
- Double jump (upgrade)
- Melee attack (close range, stronger)
Enemies
Implemented
-
Grunt — Red spiky ground patrol. Walks back and forth, turns at edges/walls. 2 HP.
-
Flyer — Purple bat-like. Bobs in air, chases player when close, shoots fireballs. 1 HP.
-
Turret — Stationary, rotates to aim at player, fires periodically
-
Charger — Ground patrol, detects player in 200 px horizontal LOS, ALERT → CHARGE (150 px/s) → STUNNED on wall hit. 2 HP.
-
Spawner — Stationary, spawns grunts every 4.5 s (max 3 alive). 3 HP, destructible.
-
Laser Turret — State machine (IDLE → CHARGING → FIRING → COOLDOWN). Per-pixel beam raycast. Fixed variant aims left; tracking variant rotates toward player at 1.5 rad/s.
Planned
- Robot — Slow, heavy ground patrol. Sturdy (4 HP), armored appearance. Walks deliberately, doesn't flinch from knockback. Punishes careless approaches — player must keep distance or use high-damage weapons. Exclusive to space station levels.
- Rocket Turret — Stationary launcher, two-stage attack. Stage 1: rocket plops up out of the turret with a visible arc (telegraph, ~0.6 s hang time), giving the player time to react. Stage 2: rocket ignites boosters and tracks the player with homing guidance. Moderate turn rate so skilled players can dodge or bait it into walls. Destroyable in flight. Exclusive to space station levels.
- Shielder — Has a directional shield, must be hit from behind or above
- Boss — Large, multi-phase encounters. One per world area.
Hazards & Mechanics
Implemented
- Flame Vent — Floor-mounted grate, toggles on/off on a timer
- Force Field — Vertical energy barrier, toggled by switch/timer
- Moving Platform — Horizontal/vertical patrol between two points
- Laser Turret — See Enemies above (also functions as a hazard)
Planned
- Bouncer — Launch pad that shoots the player into the air on contact. Two variants: straight (vertical impulse only) and angled (rotatable, placed at arbitrary angles to launch the player diagonally or sideways). Could also affect enemies and projectiles for puzzle potential.
Weapons / Projectiles
Data-driven system: each weapon type is a ProjectileDef struct describing speed,
damage, lifetime, hitbox, behavior flags, and animations. Adding a new weapon =
adding a new def. See src/game/projectile.h for the full definition.
Behavior flags
PROJ_PIERCING— Passes through enemies (up topierce_counttimes)PROJ_BOUNCY— Ricochets off walls (up tobounce_counttimes)PROJ_GRAVITY— Affected by level gravity (scaled bygravity_scale)PROJ_HOMING— Steers toward nearest valid target
Implemented weapon defs
| Weapon | Speed | Damage | Special | Owner |
|---|---|---|---|---|
WEAPON_PLASMA |
400 | 1 | Default player weapon | Player |
WEAPON_SPREAD |
350 | 1 | Short range, fan pattern | Player |
WEAPON_LASER |
600 | 1 | Pierces 3 enemies | Player |
WEAPON_ROCKET |
200 | 3 | Slight gravity drop | Player |
WEAPON_BOUNCE |
300 | 1 | 3 wall bounces + gravity | Player |
WEAPON_ENEMY_FIRE |
180 | 1 | Enemy fireball | Enemy |
Directional aiming
- Forward (default) — shoots horizontally in facing direction
- Up — hold UP to shoot straight up
- Diagonal up — hold UP + LEFT/RIGHT to shoot at 45 degrees
Levels
Format (.lvl)
Current directives: TILESET, SIZE, SPAWN, GRAVITY, WIND, BG_COLOR, MUSIC, PARALLAX_FAR, PARALLAX_NEAR, TILEDEF, ENTITY, EXIT, LAYER
Needed additions:
STORM,DRAG— Remaining atmosphere settings
Level Ideas
- Intro Level (Moon) - Low gravity, bright surface, spacey/no obstacles
- Mars Surface - Low gravity, red surface, spacey, little to no obstacles, transition area is entry to base
- Mars Base - Normal gravity, very vertical, narrow, 90 degree turns, lots of enemies
- Derelict Station — Low gravity, dark, flickering lights, abandoned corridors
- Desert Planet (Saturn) — High gravity, sand storm wind, bright orange palette
- Gas Giant (Jupiter) — Very low gravity, floating platforms, toxic atmosphere
- Asteroid Belt — Zero-G sections, small disconnected platforms
- Space Freighter — Normal gravity, tight corridors, turret enemies
- Ice World — Normal gravity, strong winds, slippery surface
Old Space Station Campaign
Abandoned orbital station overrun by malfunctioning security systems. Only robotic enemies remain — no organic creatures. Cold, industrial aesthetic with metal walls, exposed pipes, warning lights, and airlock doors.
Enemy roster (station-exclusive):
- Robots — slow, heavy patrols that absorb punishment
- Turrets — standard rotating turrets from earlier levels
- Rocket Turrets — two-stage homing rockets (plop-up telegraph → boost + track)
Level sequence:
- station01.lvl (Docking Bay) — Spacecraft lands at the station exterior. Normal gravity, wide open hangar area easing the player in. A few robots and a turret introduce the new enemy types. Exit leads inside.
- station02.lvl (Reactor Core) — Vertical level, tight corridors around a central reactor shaft. Rocket turrets cover long sightlines, robots block narrow passages. Elevator transition into generated levels.
- generate:old_station (Security Decks) — Procedurally generated interior levels (2-3 before the boss arena). Increasing density of robots, turrets, and rocket turrets. Tight rooms, low ceilings, interlocking corridors. Difficulty scales with depth.
- station03.lvl (Command Bridge / Boss Arena) — Final station level. Large arena with a boss encounter (station security chief or haywire defense mainframe). Heavy use of rocket turrets as stage hazards.
World Map
Structure
- Graph of nodes (planets/stations) connected by routes
- Player selects destination, spacecraft flies there (short animation or instant)
- Some routes may require fuel/upgrades to unlock
- Map reveals new nodes as levels are completed
Implementation Notes
- Separate game state from the platformer (own update/render)
- Needs: node data structure, spacecraft position, route rendering
- Could start simple: linear level select, evolve into open map
Technical TODO
High Priority
- Entity spawn directives in .lvl format (
ENTITYdirective) - Level exit zones and level transitions
- Dash mechanic
- Particle system (death puffs, landing dust, projectile impact sparks, wall slide dust)
- Screen shake on damage / enemy kills
- Sound effects (jump, shoot, hit, enemy death, dash)
- Basic HUD (health hearts + jetpack charges)
Medium Priority
- In-game level editor (tile/entity placement, save/load, test play)
- Wind atmosphere property (
WINDdirective, affects all entities/particles/projectiles) - Drag atmosphere property
- Parallax scrolling backgrounds (procedural stars + nebula, or from image files)
- Per-level background color (
BG_COLORdirective) - Music playback per level (
MUSICdirective) - Weapon switching system
- Pickup entities (health, jetpack refill, drone companion)
- Better tileset art (space-themed)
- Player sprite polish (more animation frames)
- Death / respawn system
- Pause menu
Low Priority (Future)
- World map mode
- Spacecraft navigation
- Boss encounters
- Dialogue / mission briefing system
- Save system
- Controller support (SDL_GameController is initialized)
- Multiple playable characters
Art Direction
- Pixel art, 16x16 tile grid
- Logical resolution: 640x360 (rendered at 2x = 1280x720)
- Nearest-neighbor scaling for crisp pixels
- Dark, moody color palettes with bright projectile/effect accents
- Inspired by: Cowboy Bebop color grading, retro sci-fi, neon-on-dark
Controls
| Action | Key | Status |
|---|---|---|
| Move | Arrow keys | Implemented |
| Jump | Z / Space | Implemented |
| Shoot | X | Implemented |
| Aim up | UP (+ shoot) | Implemented |
| Aim diag | UP+LEFT/RIGHT (+ shoot) | Implemented |
| Dash | C | Implemented |
| Look up | UP (stand still) | Implemented |
| Pause | Escape | Implemented |
Game Analytics & Highscores
Track comprehensive play session analytics and submit them to a backend service for leaderboards and gameplay insights. Data flows from C → JS (via EM_JS) → backend API (via fetch). Desktop builds can write to a local file as fallback.
Metrics to track (GameStats struct)
Per-run (reset on new game / restart):
| Metric | Type | Notes |
|---|---|---|
levels_completed |
int | Incremented on each level exit trigger |
enemies_killed |
int | Total kills across all levels |
kills_by_type[N] |
int[] | Kills broken down by enemy type |
deaths |
int | Player death/respawn count |
time_elapsed_ms |
uint32_t | Wall-clock play time (accumulate dt) |
shots_fired |
int | Total projectiles spawned by player |
shots_hit |
int | Player projectiles that connected with enemy |
damage_taken |
int | Total HP lost (before death resets) |
damage_dealt |
int | Total HP dealt to enemies |
dashes_used |
int | Dash activations |
jumps |
int | Jump count |
distance_traveled |
float | Horizontal pixels traversed |
pickups_collected |
int | Health, jetpack, weapon pickups |
longest_kill_streak |
int | Max kills without taking damage |
current_kill_streak |
int | (internal, not submitted) |
Per-level snapshot (ring buffer or array, flushed on level exit):
- Level name / generator tag
- Time spent in level
- Kills in level
- Deaths in level
- Health remaining on exit
Data flow
C (GameStats) JS (shell.html) Backend API
│ │ │
├─ on victory/game-over ────→│ │
│ EM_JS: submit_run() ├── POST /api/runs ────────→│
│ │ { stats JSON } │── store in DB
│ │ │
├─ on leaderboard open ─────→│ │
│ EM_JS: fetch_leaderboard()├── GET /api/leaderboard ──→│
│ │←─ JSON [ top N runs ] ─────│
│←─ KEEPALIVE callback ──────│ │
│ write to C memory │ │
Backend service
Small HTTP API deployed alongside the game on k3s. Receives run data as JSON, stores in a lightweight DB (SQLite or Postgres), serves leaderboard queries. Endpoints:
POST /api/runs— submit a completed run (all metrics above)GET /api/leaderboard— top N runs, sortable by score/time/killsGET /api/stats— aggregate stats (total runs, total kills, avg time)
A composite score formula ranks runs for the leaderboard, e.g.:
score = (enemies_killed * 100) + (levels_completed * 500) - (deaths * 200) - (time_elapsed_ms / 1000)
Integration points in C
- GameStats struct in
main.cor a newsrc/game/stats.hmodule - Kill counting: hook into entity death in
level.cdamage handling - Shot tracking: increment in
player.cshoot logic - Time tracking: accumulate
dteach frame inMODE_PLAY - Distance: accumulate abs(vel.x * dt) each frame
- Jumps/dashes: increment in
player.cat point of activation - Level snapshots: capture on exit trigger before level_free
- Submission: call
EM_JSfunction from the victory path inmain.c
Anti-tamper: HMAC signature
All submissions are signed client-side in C/WASM before reaching JS, preventing casual forgery (console injection, proxy interception, modified payloads). The signature lives in compiled WASM — not trivially visible like plain JS — raising the effort required to cheat.
Scheme: HMAC-SHA256
- A shared secret key is embedded in the C source (compiled into WASM). Not truly hidden from a determined reverse-engineer, but opaque to casual inspection.
- On submission, the C code:
- Serializes the
GameStatsstruct to a canonical JSON string - Requests a one-time nonce from the backend (
GET /api/nonce) - Computes
HMAC-SHA256(secret, nonce + json_payload) - Passes the JSON, nonce, and hex signature to JS via
EM_JS
- Serializes the
- JS sends all three to the backend in the POST body.
- Backend recomputes the HMAC with its copy of the secret, verifies the signature matches, and checks the nonce hasn't been used before (replay protection).
What this prevents:
- Browser console
fetch("/api/runs", { body: fakeStats })— no valid sig - Proxy/MITM payload modification — signature won't match
- Replay attacks — nonce is single-use
What this does NOT prevent:
- Reverse-engineering the WASM binary to extract the key
- Memory editing during gameplay (modifying stats before signing)
- A fully custom WASM build that signs fabricated data
Implementation:
- SHA-256 and HMAC can be implemented in ~200 lines of C (no external dependency). Alternatively use a small embedded library like micro-ecc or TweetNaCl.
- The secret key should be split across multiple static arrays and reassembled at runtime to deter simple string scanning of the binary.
- Native builds (Linux/Windows) use the same HMAC logic, posting via libcurl or a lightweight HTTP client.
Anti-tamper: server-side plausibility checks
The backend validates every submission against known game constraints. This is independent of the HMAC and catches cheats that produce valid signatures (memory edits, custom builds).
Hard limits (reject immediately):
levels_completedcannot exceed the total campaign lengthenemies_killedcannot exceed max possible spawns per level chainshots_hitcannot exceedshots_fireddamage_dealtcannot exceedenemies_killed * max_enemy_hpaccuracy(shots_hit / shots_fired) above 99% is flaggedtime_elapsed_msbelow a minimum threshold per level is impossible (speed-of-light check: level_width / max_player_speed)
Soft limits (flag for review, don't reject):
- Zero deaths across the entire campaign
- Kill streak equal to total kills (never took damage)
- Unusually low time relative to levels completed
- Distance traveled below expected minimum for the level chain
Rate limiting:
- Max 1 submission per IP per 60 seconds
- Max 10 submissions per IP per hour
- Duplicate payload detection (exact same stats = reject)
Schema:
runs table:
id, player_name, submitted_at, ip_hash,
signature_valid (bool), plausibility_flags (bitmask),
levels_completed, enemies_killed, kills_by_type (json),
deaths, time_elapsed_ms, shots_fired, shots_hit,
damage_taken, damage_dealt, dashes_used, jumps,
distance_traveled, pickups_collected, longest_kill_streak,
level_snapshots (json), composite_score
Flagged runs are stored but excluded from the public leaderboard.
Requirements before this works
- The game needs an ending — either a final boss level with empty exit target, or a depth limit on the station generator
- A
MODE_VICTORYgame state showing final stats + leaderboard - The backend service (container image + k8s manifests)
- Player name input (simple text prompt in JS, or anonymous with a generated handle)
- HMAC-SHA256 implementation in C (or vendored micro-library)
- Nonce endpoint on the backend
- Plausibility rule set derived from game constants (MAX_ENTITIES, level chain length, player speed, enemy HP values)
Reference Games
- Jazz Jackrabbit 2 (movement feel, weapon variety, level design)
- Metal Slug (run-and-gun, enemy variety, visual flair)
- Mega Man X (dash, tight controls)
- Cowboy Bebop (aesthetic, tone, music style)