Files
major_tom/DESIGN.md
2026-03-05 17:38:46 +00:00

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 to pierce_count times)
  • PROJ_BOUNCY — Ricochets off walls (up to bounce_count times)
  • PROJ_GRAVITY — Affected by level gravity (scaled by gravity_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

  1. Intro Level (Moon) - Low gravity, bright surface, spacey/no obstacles
  2. Mars Surface - Low gravity, red surface, spacey, little to no obstacles, transition area is entry to base
  3. Mars Base - Normal gravity, very vertical, narrow, 90 degree turns, lots of enemies
  4. Derelict Station — Low gravity, dark, flickering lights, abandoned corridors
  5. Desert Planet (Saturn) — High gravity, sand storm wind, bright orange palette
  6. Gas Giant (Jupiter) — Very low gravity, floating platforms, toxic atmosphere
  7. Asteroid Belt — Zero-G sections, small disconnected platforms
  8. Space Freighter — Normal gravity, tight corridors, turret enemies
  9. 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 (ENTITY directive)
  • 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 (WIND directive, affects all entities/particles/projectiles)
  • Drag atmosphere property
  • Parallax scrolling backgrounds (procedural stars + nebula, or from image files)
  • Per-level background color (BG_COLOR directive)
  • Music playback per level (MUSIC directive)
  • 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/kills
  • GET /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.c or a new src/game/stats.h module
  • Kill counting: hook into entity death in level.c damage handling
  • Shot tracking: increment in player.c shoot logic
  • Time tracking: accumulate dt each frame in MODE_PLAY
  • Distance: accumulate abs(vel.x * dt) each frame
  • Jumps/dashes: increment in player.c at point of activation
  • Level snapshots: capture on exit trigger before level_free
  • Submission: call EM_JS function from the victory path in main.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

  1. 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.
  2. On submission, the C code:
    • Serializes the GameStats struct 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
  3. JS sends all three to the backend in the POST body.
  4. 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_completed cannot exceed the total campaign length
  • enemies_killed cannot exceed max possible spawns per level chain
  • shots_hit cannot exceed shots_fired
  • damage_dealt cannot exceed enemies_killed * max_enemy_hp
  • accuracy (shots_hit / shots_fired) above 99% is flagged
  • time_elapsed_ms below 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

  1. The game needs an ending — either a final boss level with empty exit target, or a depth limit on the station generator
  2. A MODE_VICTORY game state showing final stats + leaderboard
  3. The backend service (container image + k8s manifests)
  4. Player name input (simple text prompt in JS, or anonymous with a generated handle)
  5. HMAC-SHA256 implementation in C (or vendored micro-library)
  6. Nonce endpoint on the backend
  7. 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)