forked from tas/major_tom
Update shell
This commit is contained in:
207
DESIGN.md
207
DESIGN.md
@@ -83,6 +83,16 @@ Already implemented: `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PA
|
||||
- **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.
|
||||
|
||||
@@ -153,6 +163,32 @@ Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, `
|
||||
7. **Space Freighter** — Normal gravity, tight corridors, turret enemies
|
||||
8. **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
|
||||
@@ -231,6 +267,177 @@ Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, `
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
Reference in New Issue
Block a user