6 Commits

Author SHA1 Message Date
478c44212b Fix #28: Add lives system, game over screen, and fix leaderboard score 0
All checks were successful
CI / build (pull_request) Successful in 32s
- Add 3-life system: each death costs a life, game over at 0 lives
- Add game over screen with arcade-style 3-character name entry
- Submit player name with score to analytics backend on game over
- Fix score 0 on leaderboard by periodically stashing stats to JS
  so the beforeunload fallback has real data when users close the tab
- Display lives counter in HUD next to health hearts
- Move player respawn control from level.c to main.c via
  player_death_pending flag for proper lives tracking
2026-03-20 05:30:00 +00:00
c62aae16dc update levels
All checks were successful
Deploy / deploy (push) Successful in 1m16s
2026-03-16 20:42:03 +00:00
3b45572d38 Add game state debug log with binary ring buffer
All checks were successful
CI / build (pull_request) Successful in 32s
Deploy / deploy (push) Successful in 1m17s
Implement src/engine/debuglog module that records a comprehensive
snapshot of game state every tick into a 4 MB in-memory ring buffer.

Activated by --debug-log command-line flag. Press F12 during gameplay
to dump the ring buffer to a human-readable debug_log.txt file. The
buffer also auto-flushes every 10 seconds as a safety net.

Each tick snapshot captures: input state (held/pressed/released bitmasks),
full player state (position, velocity, health, dash, aim, timers),
camera position, physics globals, level name, and a variable-length
list of all active entity positions/velocities/health.

New files:
- src/engine/debuglog.h — API and snapshot data structures
- src/engine/debuglog.c — ring buffer, record, and dump logic

Modified files:
- include/config.h — DEBUGLOG_BUFFER_SIZE constant
- src/engine/input.h/c — input_get_snapshot() to pack input bitmasks
- src/engine/core.c — debuglog_record_tick() call after update
- src/main.c — CLI flag, init/shutdown, F12 hotkey, set_level calls

Closes #19
2026-03-16 20:33:03 +00:00
66a7b9e7e6 Fix downward dash not damaging enemies and add post-dash invincibility
Some checks failed
CI / build (pull_request) Successful in 32s
Deploy / deploy (push) Failing after 1m17s
Stomping was guarded by the invincibility check, so during a downward
dash the player could never deal stomp damage. Move the invincibility
guard to only protect against taking damage, not dealing it.

Extend dash invincibility by PLAYER_DASH_INV_GRACE (0.15s) past the
dash duration so the player is briefly protected after landing.

Closes #15
2026-03-16 20:20:05 +00:00
tas
59f76d6aa7 Update .gitea/workflows/deploy.yaml
Some checks failed
Deploy / deploy (push) Failing after 1m17s
2026-03-16 20:15:19 +00:00
tas
29c620a9e8 Update .gitea/workflows/deploy.yaml
Some checks failed
Deploy / deploy (push) Failing after 1m17s
2026-03-16 20:04:51 +00:00
18 changed files with 984 additions and 100 deletions

View File

@@ -19,14 +19,6 @@ jobs:
git config --global http.https://git.kimchi.sslVerify false
git clone --depth 1 https://${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}@git.kimchi/tas/major_tom.git .
- name: Debug registry auth
run: |
curl -s -D- http://git.kimchi/v2/ | head -15
TOKEN=$(curl -s "http://git.kimchi/v2/token?service=container_registry&scope=repository:tas/major_tom:push,pull" -u "${{
secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}" | jq -r .token)
echo "Token received: ${TOKEN:0:20}..."
curl -s -D- -H "Authorization: Bearer $TOKEN" http://git.kimchi/v2/tas/major_tom/tags/list | head -15
- name: Build and push container image
run: |
set -ex
@@ -46,15 +38,13 @@ jobs:
echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV"
- name: Deploy to k3s
- name: Restart deployment on k3s
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
kubectl delete pod -l app=${{ env.DEPLOYMENT }} -n ${{ env.NAMESPACE }}
kubectl set image deployment/${{ env.DEPLOYMENT }} \
${{ env.DEPLOYMENT }}="${{ env.IMAGE_TAG }}" \
-n ${{ env.NAMESPACE }}
kubectl rollout status deployment/${{ env.DEPLOYMENT }} \
-n ${{ env.NAMESPACE }} --timeout=60s
POD_NAME=$(kubectl get pods -l app=jnr-web -n jnr-web -o jsonpath='{.items[0].metadata.name}')
echo "Deleting running pod" "$POD_NAME"

View File

@@ -1,42 +1,51 @@
# Mars Surface - Red Dusty Plains
# ================================
# First Mars level: low gravity, wide-open terrain with wind.
# Spacecraft landing intro from moon. Charger enemies patrol
# the dusty landscape. Gun pickup midway through.
# Level created with in-game editor
TILESET assets/tiles/mars_tileset.png
SIZE 250 23
SPAWN 3 18
SPAWN 3 19
GRAVITY 370
WIND 25
BG_COLOR 30 12 8
PARALLAX_STYLE 5
MUSIC assets/sounds/kaffe_og_kage.ogg
PARALLAX_STYLE 5
ENTITY spacecraft 1 14
# Charger patrols across flat sections
ENTITY spacecraft 1 15
ENTITY charger 40 18
ENTITY charger 75 18
ENTITY charger 120 14
ENTITY charger 165 18
ENTITY charger 120 11
ENTITY charger 165 17
ENTITY charger 200 18
# Grunts near structures
ENTITY grunt 55 18
ENTITY grunt 140 18
# Health and gun pickups
ENTITY powerup_hp 90 15
ENTITY powerup_gun 130 12
ENTITY powerup_gun 130 11
ENTITY grunt 132 18
ENTITY grunt 116 18
ENTITY grunt 28 19
ENTITY powerup_jet 177 19
ENTITY grunt 174 21
ENTITY grunt 169 21
ENTITY grunt 164 21
ENTITY grunt 159 21
ENTITY grunt 202 18
ENTITY grunt 213 13
ENTITY grunt 20 19
ENTITY powerup_fuel 24 8
ENTITY powerup_fuel 90 11
ENTITY charger 127 18
ENTITY charger 175 21
ENTITY asteroid 54 5
ENTITY asteroid 53 5
ENTITY asteroid 48 5
EXIT 246 17 2 3 assets/levels/mars02.lvl
# Tile definitions (Mars tileset)
TILEDEF 1 0 0 1
TILEDEF 2 1 0 1
TILEDEF 3 2 0 1
TILEDEF 4 0 1 2
TILEDEF 5 1 1 0
TILEDEF 6 2 1 0
LAYER collision
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
@@ -48,15 +57,18 @@ LAYER collision
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 1 1 2 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 2 1 1 1 1 1 1 1 2 1 1 1 2 1 1 1 1 0 0 0 0 0 1 2 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 2 1 1 2 1 1 2 1 1 1 1 1 0 0 0 0 0 1 2 1 1 1 1 1 2 1 1 1 1 2 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 2 1 1 1 2 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 2 1 1 2 1 1 1 1 1 1 1 2 1 1 1 1 2 1 1 1 1 1 1 1 1 1
1 3 1 1 3 1 1 3 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 3 1 1 3 1 1 3 1 0 0 0 0 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 3 1 3 1 0 0 0 0 0 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 1 1 3 1 1 3 1 3 1 1 3 1 0 0 0 0 0 0 1 1 3 1 1 3 1 1 3 1 1 1 1 3 1 1 3 1 3 1 1 3 1 1 1 3 1 3 1 1 1 3 1 1 1 3 1 1 3 1 1 3 1 1 3 0 0 0 0 0 1 3 1 1 3 1 3 1 1 1 3 1 1 3 1 1 1 3 1 1 3 1 3 1 1 1 3 1 1 3 1 1 3 1 3 1 1 1 3 1 3 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 1 3 1 1 3 1 1 3 1 3 1 1 1 3 1 3 1 1 1 3 1 3 1 1 1 3 1 1 3 1 1 3 1 3 1 1 3 1 3
3 1 3 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 3 1 3 3 1 3 0 0 0 0 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 1 3 0 0 0 0 0 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 3 3 1 3 3 1 3 1 3 3 1 3 0 0 0 0 0 0 3 3 1 3 3 1 3 3 1 3 3 3 3 1 3 3 1 3 1 3 3 1 3 3 3 1 3 1 3 3 3 1 3 3 3 1 3 3 1 3 3 1 3 3 1 0 0 0 0 0 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 3 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 3 1 3 1 3 3 3 1 3 1 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 3 1 3 3 1 3 3 1 3 1 3 3 3 1 3 1 3 3 3 1 3 1 3 3 3 1 3 3 1 3 3 1 3 1 3 3 1 3 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 1 1 1 1 2 1 1 1 1 2 1 1 2 1 1 1 1 1 2 1 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 3 1 3 1 0 0 0 0 0 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 1 1 3 1 1 3 1 3 1 1 3 1 0 0 0 0 0 0 1 1 3 1 1 3 1 1 3 1 1 1 1 3 1 1 3 1 3 1 1 3 1 1 1 3 1 3 1 1 1 3 1 1 1 3 1 1 3 1 1 3 1 1 3 0 0 0 0 0 1 5 5 5 5 5 5 5 5 5 3 1 1 3 1 1 1 3 1 1 3 1 5 5 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
3 1 3 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 3 1 3 3 1 3 0 0 0 0 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 1 3 0 0 0 0 0 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 3 3 1 3 3 1 3 1 3 3 1 3 0 0 0 0 0 0 3 3 1 3 3 1 3 3 1 3 3 3 3 1 3 3 1 3 1 3 3 1 3 3 3 1 3 1 3 3 3 1 3 3 3 1 3 3 1 3 3 1 3 3 1 0 0 0 0 0 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 1 3 1 3 3 1 3 1
2 2 2 3 1 3 3 2 1 1 2 2 3 1 1 1 1 1 2 3 2 1 1 1 1 1 1 1 2 3 3 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2 1 3 3 2 2 3 2 2 1 3 2 2 2 1 1 1 3 1 2 2 2 2 1 3 3 3 3 3 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

View File

@@ -1,15 +1,12 @@
# Moon Surface - Crater Fields
# ============================
# Second moon level: tighter platforming over deep craters.
# Spacecraft landing intro. Unarmed — pure jump-and-run with asteroids.
# Level created with in-game editor
TILESET assets/tiles/moon_tileset.png
SIZE 200 23
SPAWN 3 18
GRAVITY 300
BG_COLOR 5 5 15
PARALLAX_STYLE 4
MUSIC assets/sounds/algardalgar.ogg
PARALLAX_STYLE 4
PLAYER_UNARMED
ENTITY spacecraft 1 14
@@ -20,10 +17,22 @@ ENTITY asteroid 105 3
ENTITY asteroid 130 0
ENTITY asteroid 155 2
ENTITY asteroid 180 1
ENTITY powerup_fuel 43 16
ENTITY powerup_fuel 46 11
ENTITY powerup_fuel 75 15
ENTITY powerup_fuel 94 12
ENTITY powerup_fuel 106 16
ENTITY powerup_fuel 130 12
ENTITY powerup_fuel 166 12
ENTITY asteroid 167 0
ENTITY asteroid 117 0
ENTITY asteroid 96 0
ENTITY asteroid 66 0
ENTITY asteroid 44 0
ENTITY asteroid 40 0
EXIT 196 17 2 3 assets/levels/moon03.lvl
# Tile definitions
TILEDEF 1 0 0 1
TILEDEF 2 1 0 1
TILEDEF 3 2 0 1
@@ -53,3 +62,4 @@ LAYER collision
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1
1 2 2 2 1 1 1 2 1 2 2 1 2 2 1 0 0 0 0 0 1 2 1 1 2 1 1 1 2 0 0 0 0 0 0 0 1 1 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 1 1 1 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 2 2 1 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 2 1 1 2 2 2 1 1 1 2 0 0 0 0 0 0 1 1 1 1 1 2 1 1 1 2 1 2 1
1 1 1 3 1 3 1 1 3 1 1 3 1 1 1 0 0 0 0 0 1 3 1 3 1 1 1 1 3 0 0 0 0 0 0 0 1 1 3 1 3 1 1 3 1 1 3 1 1 0 0 0 0 0 0 0 0 0 0 3 1 1 3 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 3 3 1 1 3 3 3 1 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 3 3 1 3 3 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 3 3 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 3 1 1 1 3 3 1 1 3 3 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 3 1 1 1 1 1 0 0 0 0 0 0 3 1 3 1 3 1 1 3 1 1 1 1 1

View File

@@ -1,15 +1,12 @@
# Moon Surface - Dark Side
# ========================
# Third moon level: the hardest moon terrain with massive chasms.
# Spacecraft landing intro. Unarmed until gun powerup near exit.
# Level created with in-game editor
TILESET assets/tiles/moon_tileset.png
SIZE 150 23
SPAWN 3 18
GRAVITY 300
BG_COLOR 5 5 15
PARALLAX_STYLE 4
MUSIC assets/sounds/algardalgar.ogg
PARALLAX_STYLE 4
PLAYER_UNARMED
ENTITY spacecraft 1 14
@@ -18,13 +15,16 @@ ENTITY asteroid 50 2
ENTITY asteroid 75 1
ENTITY asteroid 100 3
ENTITY asteroid 125 0
# Gun powerup near the exit — the player finally gets armed
ENTITY powerup_gun 130 18
ENTITY asteroid 25 1
ENTITY asteroid 60 0
ENTITY asteroid 86 0
ENTITY asteroid 98 0
ENTITY asteroid 110 0
ENTITY asteroid 120 1
EXIT 146 17 2 3 assets/levels/mars01.lvl
# Tile definitions
TILEDEF 1 0 0 1
TILEDEF 2 1 0 1
TILEDEF 3 2 0 1
@@ -40,17 +40,18 @@ LAYER collision
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 2 1 1 1 2 2 1 2 1 1 1 0 0 0 0 0 0 1 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 2 2 2 1 1 2 1 2 2 0 0 0 0 0 0 0 0 0 0 1 1 1 2 1 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 1 2 1 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 2 1 1 1 1 0 0 0 0 0 0 2 1 1 1 2 2 1 1 1 2 2 1 1 2 1 1 2 1 1 2 1
1 1 1 3 3 1 3 1 1 3 1 3 0 0 0 0 0 0 1 3 1 1 1 1 1 1 3 0 0 0 0 0 0 0 3 3 3 1 3 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 3 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 3 3 1 3 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 3 3 1 0 0 0 0 0 0 1 3 1 1 1 1 1 3 1 1 1 3 1 1 3 1 1 1 1 3 1
1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 2 1 1 1 2 2 1 2 1 1 1 0 0 0 0 0 0 1 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 2 2 2 1 1 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 1 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 1 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 1 1 0 0 0 0 0 0 0 0 2 1 1 1 2 2 1 1 1 2 2 1 1 2 1 1 2 1 1 2 1
1 1 1 3 3 1 3 1 1 3 1 3 0 0 0 0 0 0 1 3 1 1 1 1 1 1 3 0 0 0 0 0 0 0 3 3 3 1 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 3 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 3 3 1 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 3 0 0 0 0 0 0 0 0 1 3 1 1 1 1 1 3 1 1 1 3 1 1 3 1 1 1 1 3 1

View File

@@ -49,4 +49,7 @@ typedef enum TransitionStyle {
#define MAX_ASSETS 128
#define ASSET_PATH_MAX 256
/* ── Debug log ──────────────────────────────────────── */
#define DEBUGLOG_BUFFER_SIZE (4 * 1024 * 1024) /* 4 MB ring buffer */
#endif /* JNR_CONFIG_H */

View File

@@ -3,6 +3,7 @@
#include "engine/renderer.h"
#include "engine/audio.h"
#include "engine/assets.h"
#include "engine/debuglog.h"
#include <stdio.h>
#ifdef __EMSCRIPTEN__
@@ -118,6 +119,7 @@ static void engine_frame(void) {
if (s_callbacks.update) {
s_callbacks.update(DT);
}
debuglog_record_tick();
input_consume();
g_engine.tick++;
s_accumulator -= DT;

474
src/engine/debuglog.c Normal file
View File

@@ -0,0 +1,474 @@
#include "engine/debuglog.h"
#include "engine/core.h"
#include "engine/input.h"
#include "engine/physics.h"
#include "engine/entity.h"
#include "game/level.h"
#include "game/player.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ═══════════════════════════════════════════════════
* File-scope state
* ═══════════════════════════════════════════════════ */
/* Ring buffer header stored at the start of the memory block. */
typedef struct RingHeader {
uint32_t magic; /* 0x44424C47 = "DBLG" */
uint32_t version;
uint32_t write_cursor; /* byte offset into data region */
uint32_t total_written; /* total snapshots written (wraps) */
uint32_t data_size; /* usable data bytes after header */
} RingHeader;
#define RING_MAGIC 0x44424C47
#define RING_VERSION 1
/* Flush the buffer to disk every this many ticks (10 s at 60 Hz). */
#define FLUSH_INTERVAL 600
static bool s_enabled;
static uint8_t *s_buffer; /* full allocation: header + data */
static RingHeader *s_header;
static uint8_t *s_data; /* start of ring data region */
static Level *s_level;
static char s_level_name[32]; /* human-readable level label */
static int s_flush_counter;
/* Path used for periodic safety-net flushes. */
static const char *s_flush_path = "debug_log_autosave.bin";
/* ═══════════════════════════════════════════════════
* Internal helpers
* ═══════════════════════════════════════════════════ */
/* Write raw bytes into the ring, wrapping at the boundary. */
static void ring_write(const void *src, uint32_t len) {
uint32_t cursor = s_header->write_cursor;
uint32_t cap = s_header->data_size;
const uint8_t *p = (const uint8_t *)src;
uint32_t first = cap - cursor;
if (first >= len) {
memcpy(s_data + cursor, p, len);
} else {
memcpy(s_data + cursor, p, first);
memcpy(s_data, p + first, len - first);
}
s_header->write_cursor = (cursor + len) % cap;
}
/* Read raw bytes from an arbitrary ring offset, wrapping. */
static void ring_read(uint32_t offset, void *dst, uint32_t len) {
uint32_t cap = s_header->data_size;
offset = offset % cap;
uint8_t *out = (uint8_t *)dst;
uint32_t first = cap - offset;
if (first >= len) {
memcpy(out, s_data + offset, len);
} else {
memcpy(out, s_data + offset, first);
memcpy(out + first, s_data, len - first);
}
}
/* Flush the entire buffer (header + data) to a binary file. */
static void flush_to_file(const char *path) {
FILE *f = fopen(path, "wb");
if (!f) {
fprintf(stderr, "Warning: debuglog flush failed — cannot open %s\n", path);
return;
}
uint32_t total = (uint32_t)sizeof(RingHeader) + s_header->data_size;
size_t written = fwrite(s_buffer, 1, total, f);
fclose(f);
if (written != total) {
fprintf(stderr, "Warning: debuglog flush incomplete (%zu / %u bytes)\n",
written, total);
}
}
/* Find the player entity in the current level, or NULL. */
static Entity *find_player(void) {
if (!s_level) return NULL;
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
/* Pack player-specific flags into a single byte.
* Bit layout: 0=on_ground, 1=jumping, 2=dashing, 3=invincible, 4=has_gun */
static uint8_t pack_player_flags(const Entity *player) {
uint8_t f = 0;
if (player->body.on_ground) f |= (1 << 0);
PlayerData *pd = (PlayerData *)player->data;
if (pd) {
if (pd->jumping) f |= (1 << 1);
if (pd->dash_timer > 0) f |= (1 << 2);
if (pd->inv_timer > 0) f |= (1 << 3);
if (pd->has_gun) f |= (1 << 4);
}
return f;
}
/* Pack entity-level flags into a single byte.
* Bit layout: 0=active, 1=facing_left, 2=dead, 3=invincible */
static uint8_t pack_entity_flags(const Entity *e) {
uint8_t f = 0;
if (e->active) f |= (1 << 0);
if (e->flags & ENTITY_FACING_LEFT) f |= (1 << 1);
if (e->flags & ENTITY_DEAD) f |= (1 << 2);
if (e->flags & ENTITY_INVINCIBLE) f |= (1 << 3);
return f;
}
/* Names for entity types — kept in sync with EntityType enum. */
static const char *entity_type_name(uint8_t type) {
switch ((EntityType)type) {
case ENT_NONE: return "NONE";
case ENT_PLAYER: return "PLAYER";
case ENT_ENEMY_GRUNT: return "GRUNT";
case ENT_ENEMY_FLYER: return "FLYER";
case ENT_PROJECTILE: return "PROJ";
case ENT_PICKUP: return "PICKUP";
case ENT_PARTICLE: return "PARTICLE";
case ENT_TURRET: return "TURRET";
case ENT_MOVING_PLATFORM: return "PLATFORM";
case ENT_FLAME_VENT: return "FLAME";
case ENT_FORCE_FIELD: return "FORCEFIELD";
case ENT_POWERUP: return "POWERUP";
case ENT_DRONE: return "DRONE";
case ENT_ASTEROID: return "ASTEROID";
case ENT_SPACECRAFT: return "SPACECRAFT";
case ENT_LASER_TURRET: return "LASER_TURRET";
case ENT_ENEMY_CHARGER: return "CHARGER";
case ENT_SPAWNER: return "SPAWNER";
default: return "???";
}
}
/* Names for aim directions. */
static const char *aim_dir_name(uint8_t aim) {
switch ((AimDir)aim) {
case AIM_FORWARD: return "FORWARD";
case AIM_UP: return "UP";
case AIM_DIAG_UP: return "DIAG_UP";
default: return "???";
}
}
/* Names for input actions. */
static const char *action_name(int a) {
switch ((Action)a) {
case ACTION_LEFT: return "LEFT";
case ACTION_RIGHT: return "RIGHT";
case ACTION_UP: return "UP";
case ACTION_DOWN: return "DOWN";
case ACTION_JUMP: return "JUMP";
case ACTION_SHOOT: return "SHOOT";
case ACTION_DASH: return "DASH";
case ACTION_PAUSE: return "PAUSE";
default: return "???";
}
}
/* ═══════════════════════════════════════════════════
* Public API
* ═══════════════════════════════════════════════════ */
void debuglog_init(void) {
uint32_t total = (uint32_t)sizeof(RingHeader) + DEBUGLOG_BUFFER_SIZE;
s_buffer = calloc(1, total);
if (!s_buffer) {
fprintf(stderr, "Warning: debuglog_init failed to allocate %u bytes\n", total);
return;
}
s_header = (RingHeader *)s_buffer;
s_data = s_buffer + sizeof(RingHeader);
s_header->magic = RING_MAGIC;
s_header->version = RING_VERSION;
s_header->write_cursor = 0;
s_header->total_written = 0;
s_header->data_size = DEBUGLOG_BUFFER_SIZE;
s_enabled = false;
s_level = NULL;
s_flush_counter = 0;
printf("Debug log initialized (%u KB ring buffer)\n",
DEBUGLOG_BUFFER_SIZE / 1024);
}
void debuglog_shutdown(void) {
if (!s_buffer) return;
if (s_enabled && s_header->total_written > 0) {
flush_to_file(s_flush_path);
printf("Debug log flushed to %s on shutdown (%u snapshots)\n",
s_flush_path, s_header->total_written);
}
free(s_buffer);
s_buffer = NULL;
s_header = NULL;
s_data = NULL;
s_enabled = false;
s_level = NULL;
}
void debuglog_enable(void) {
s_enabled = true;
printf("Debug log recording enabled\n");
}
bool debuglog_is_enabled(void) {
return s_enabled;
}
void debuglog_set_level(Level *lvl, const char *name) {
s_level = lvl;
if (name && name[0]) {
snprintf(s_level_name, sizeof(s_level_name), "%s", name);
} else {
s_level_name[0] = '\0';
}
}
void debuglog_record_tick(void) {
if (!s_enabled || !s_buffer) return;
/* Count active entities (skip player — recorded separately). */
uint16_t ent_count = 0;
if (s_level) {
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES && ent_count < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type != ENT_PLAYER) ent_count++;
}
}
uint32_t frame_size = (uint32_t)sizeof(TickSnapshot)
+ (uint32_t)(ent_count * sizeof(EntitySnapshot));
/* Don't write if the frame won't fit in the ring at all. */
if (frame_size > s_header->data_size) return;
/* Build the header portion on the stack. */
TickSnapshot snap;
memset(&snap, 0, sizeof(snap));
snap.tick = g_engine.tick;
snap.frame_size = frame_size;
/* Input. */
input_get_snapshot(&snap.input);
/* Player state. */
Entity *player = find_player();
if (player) {
snap.player_x = player->body.pos.x;
snap.player_y = player->body.pos.y;
snap.player_vx = player->body.vel.x;
snap.player_vy = player->body.vel.y;
snap.player_health = (int8_t)player->health;
snap.player_flags = pack_player_flags(player);
PlayerData *pd = (PlayerData *)player->data;
if (pd) {
snap.player_dash_timer = pd->dash_timer;
snap.player_dash_charges = (int8_t)pd->dash_charges;
snap.player_inv_timer = pd->inv_timer;
snap.player_coyote = pd->coyote_timer;
snap.player_aim_dir = (uint8_t)pd->aim_dir;
}
}
/* Camera. */
if (s_level) {
snap.cam_x = s_level->camera.pos.x;
snap.cam_y = s_level->camera.pos.y;
}
/* Physics globals. */
snap.gravity = physics_get_gravity();
snap.wind = physics_get_wind();
/* Level name (truncated to 31 chars + NUL). */
if (s_level_name[0]) {
snprintf(snap.level_name, sizeof(snap.level_name), "%s", s_level_name);
}
snap.entity_count = ent_count;
/* Write header into ring. */
ring_write(&snap, sizeof(TickSnapshot));
/* Write entity snapshots. */
if (s_level && ent_count > 0) {
EntityManager *em = &s_level->entities;
for (int i = 0; i < MAX_ENTITIES; i++) {
Entity *e = &em->entities[i];
if (!e->active || e->type == ENT_PLAYER) continue;
EntitySnapshot es;
es.type = (uint8_t)e->type;
es.flags = pack_entity_flags(e);
es.health = (int16_t)e->health;
es.pos_x = e->body.pos.x;
es.pos_y = e->body.pos.y;
es.vel_x = e->body.vel.x;
es.vel_y = e->body.vel.y;
ring_write(&es, sizeof(EntitySnapshot));
}
}
s_header->total_written++;
/* Periodic safety-net flush. */
s_flush_counter++;
if (s_flush_counter >= FLUSH_INTERVAL) {
s_flush_counter = 0;
flush_to_file(s_flush_path);
}
}
void debuglog_dump(const char *path) {
if (!s_buffer || s_header->total_written == 0) {
printf("Debug log: nothing to dump (0 snapshots recorded)\n");
return;
}
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "Error: cannot open %s for writing\n", path);
return;
}
fprintf(f, "=== Debug Log Dump ===\n");
fprintf(f, "Total snapshots written: %u\n\n", s_header->total_written);
/* Walk backwards from write_cursor to find snapshots.
* We read forward through the ring, starting from the oldest data. */
/* To find the oldest snapshot: if the buffer has wrapped, oldest
* starts at write_cursor. Otherwise it starts at 0. */
uint32_t cap = s_header->data_size;
uint32_t total_data_bytes;
uint32_t read_pos;
/* Estimate how much data we've written total. If total_written
* snapshots have been recorded and we assume average frame size
* is frame_size, the buffer may have wrapped. We use a simpler
* approach: scan forward from the oldest point. */
if (s_header->total_written * sizeof(TickSnapshot) >= cap) {
/* Buffer has likely wrapped. Start reading at write_cursor
* (which is where the oldest data begins). */
read_pos = s_header->write_cursor;
total_data_bytes = cap;
} else {
read_pos = 0;
total_data_bytes = s_header->write_cursor;
}
uint32_t bytes_read = 0;
uint32_t snapshots_dumped = 0;
uint32_t first_tick = 0;
uint32_t last_tick = 0;
while (bytes_read + sizeof(TickSnapshot) <= total_data_bytes) {
TickSnapshot snap;
ring_read(read_pos, &snap, sizeof(TickSnapshot));
/* Sanity check. */
if (snap.frame_size < sizeof(TickSnapshot) ||
snap.frame_size > cap) {
break;
}
if (bytes_read + snap.frame_size > total_data_bytes) {
break;
}
if (snapshots_dumped == 0) first_tick = snap.tick;
last_tick = snap.tick;
fprintf(f, "--- Tick %u ---\n", snap.tick);
/* Input. */
fprintf(f, "Input:");
for (int a = 0; a < ACTION_COUNT && a < 8; a++) {
if (snap.input.held & (1 << a)) {
fprintf(f, " [%s]", action_name(a));
}
if (snap.input.pressed & (1 << a)) {
fprintf(f, " [%s_pressed]", action_name(a));
}
if (snap.input.released & (1 << a)) {
fprintf(f, " [%s_released]", action_name(a));
}
}
fprintf(f, "\n");
/* Player. */
fprintf(f, "Player: pos=(%.1f, %.1f) vel=(%.1f, %.1f) hp=%d",
snap.player_x, snap.player_y,
snap.player_vx, snap.player_vy,
snap.player_health);
fprintf(f, " on_ground=%d", (snap.player_flags >> 0) & 1);
fprintf(f, " jumping=%d", (snap.player_flags >> 1) & 1);
fprintf(f, " dashing=%d", (snap.player_flags >> 2) & 1);
fprintf(f, " inv=%d", (snap.player_flags >> 3) & 1);
fprintf(f, " has_gun=%d", (snap.player_flags >> 4) & 1);
fprintf(f, " aim=%s", aim_dir_name(snap.player_aim_dir));
fprintf(f, " dash_charges=%d dash_timer=%.2f inv_timer=%.2f coyote=%.3f\n",
snap.player_dash_charges, snap.player_dash_timer,
snap.player_inv_timer, snap.player_coyote);
/* Camera + physics. */
fprintf(f, "Camera: (%.1f, %.1f)\n", snap.cam_x, snap.cam_y);
fprintf(f, "Physics: gravity=%.1f wind=%.1f\n",
snap.gravity, snap.wind);
if (snap.level_name[0]) {
fprintf(f, "Level: %s\n", snap.level_name);
}
/* Entities. */
fprintf(f, "Entities (%u active):\n", snap.entity_count);
uint32_t ent_offset = (read_pos + sizeof(TickSnapshot)) % cap;
uint16_t count = snap.entity_count;
if (count > MAX_ENTITIES) count = MAX_ENTITIES;
for (uint16_t i = 0; i < count; i++) {
EntitySnapshot es;
ring_read(ent_offset, &es, sizeof(EntitySnapshot));
ent_offset = (ent_offset + sizeof(EntitySnapshot)) % cap;
fprintf(f, " [%u] %-12s pos=(%.0f, %.0f) vel=(%.0f, %.0f) hp=%d\n",
i, entity_type_name(es.type),
es.pos_x, es.pos_y,
es.vel_x, es.vel_y,
es.health);
}
fprintf(f, "\n");
read_pos = (read_pos + snap.frame_size) % cap;
bytes_read += snap.frame_size;
snapshots_dumped++;
}
/* Write summary at the top. */
if (snapshots_dumped > 0) {
float seconds = (float)(last_tick - first_tick) / TICK_RATE;
fprintf(f, "=== Summary: ticks %u to %u (%u snapshots, %.1f seconds) ===\n",
first_tick, last_tick, snapshots_dumped, seconds);
}
fclose(f);
printf("Debug log dumped to %s (%u snapshots)\n", path, snapshots_dumped);
}

91
src/engine/debuglog.h Normal file
View File

@@ -0,0 +1,91 @@
#ifndef JNR_DEBUGLOG_H
#define JNR_DEBUGLOG_H
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
/* Forward declaration — game layer type, registered via pointer. */
typedef struct Level Level;
/* ═══════════════════════════════════════════════════
* Snapshot data structures
* ═══════════════════════════════════════════════════ */
/* Packed input snapshot — 3 bytes (ACTION_COUNT actions × 3 states). */
typedef struct InputSnapshot {
uint8_t held; /* bitmask of ACTION_* held this tick */
uint8_t pressed; /* bitmask of ACTION_* pressed this tick */
uint8_t released; /* bitmask of ACTION_* released this tick */
} InputSnapshot;
/* Per-entity summary — 20 bytes each. */
typedef struct EntitySnapshot {
uint8_t type; /* EntityType */
uint8_t flags; /* active, facing, dead, invincible */
int16_t health;
float pos_x, pos_y;
float vel_x, vel_y;
} EntitySnapshot;
/* Full tick snapshot — fixed-size header + variable entity list.
* Written sequentially into the ring buffer; frame_size stores
* the total byte count so the reader can skip forward. */
typedef struct TickSnapshot {
uint32_t tick; /* g_engine.tick */
uint32_t frame_size; /* total bytes of this record */
/* Input */
InputSnapshot input; /* 3 bytes */
uint8_t _pad0; /* align to 4 bytes */
/* Player state (expanded) */
float player_x, player_y;
float player_vx, player_vy;
int8_t player_health;
uint8_t player_flags; /* on_ground, jumping, dashing, inv, has_gun */
uint8_t player_aim_dir;
int8_t player_dash_charges;
float player_dash_timer;
float player_inv_timer;
float player_coyote;
/* Camera */
float cam_x, cam_y;
/* Physics globals */
float gravity;
float wind;
/* Level info */
char level_name[32]; /* truncated level path/tag */
/* Entity summary */
uint16_t entity_count; /* number of active entities */
uint16_t _pad1; /* alignment padding */
/* EntitySnapshot entities[] follows immediately in the buffer. */
} TickSnapshot;
/* ═══════════════════════════════════════════════════
* Public API
* ═══════════════════════════════════════════════════ */
/* Allocate ring buffer. Safe to call even if logging stays disabled. */
void debuglog_init(void);
/* Final flush + free. */
void debuglog_shutdown(void);
/* Turn on recording. */
void debuglog_enable(void);
/* Query state. */
bool debuglog_is_enabled(void);
/* Register the active Level pointer so the debuglog can read game state.
* Pass NULL before freeing a level.
* name is an optional label (e.g. file path); NULL or "" to clear. */
void debuglog_set_level(Level *lvl, const char *name);
/* Called once per tick from engine_frame, after update, before consume.
* Captures input + game state into the ring buffer. */
void debuglog_record_tick(void);
/* Dump the ring buffer contents to a human-readable text file. */
void debuglog_dump(const char *path);
#endif /* JNR_DEBUGLOG_H */

View File

@@ -1,4 +1,5 @@
#include "engine/input.h"
#include "engine/debuglog.h"
#include <string.h>
static bool s_current[ACTION_COUNT];
@@ -196,6 +197,19 @@ bool input_key_held(SDL_Scancode key) {
return s_key_state && s_key_state[key];
}
/* ── Debug log snapshot ───────────────────────────── */
void input_get_snapshot(InputSnapshot *out) {
out->held = 0;
out->pressed = 0;
out->released = 0;
for (int i = 0; i < ACTION_COUNT && i < 8; i++) {
if (s_current[i]) out->held |= (uint8_t)(1 << i);
if (s_latched_pressed[i]) out->pressed |= (uint8_t)(1 << i);
if (s_latched_released[i]) out->released |= (uint8_t)(1 << i);
}
}
void input_shutdown(void) {
/* Nothing to clean up */
}

View File

@@ -50,4 +50,10 @@ int input_mouse_scroll(void);
bool input_key_pressed(SDL_Scancode key);
bool input_key_held(SDL_Scancode key);
/* Pack current input state into a compact bitmask snapshot.
* Used by the debug log to record per-tick input without
* exposing internal arrays. */
typedef struct InputSnapshot InputSnapshot; /* defined in debuglog.h */
void input_get_snapshot(InputSnapshot *out);
#endif /* JNR_INPUT_H */

View File

@@ -103,20 +103,39 @@ EM_JS(void, js_analytics_session_start, (), {
});
});
/* Stash current stats into Module._analyticsLastStats so the
* beforeunload fallback has real data if the user closes the tab.
* Called periodically from C (e.g. once per second). */
EM_JS(void, js_analytics_stash_stats, (int score, int level_reached,
int lives_used, int duration_secs), {
if (!Module._analyticsUrl) return;
Module._analyticsLastStats = JSON.stringify({
score: score,
level_reached: level_reached > 0 ? level_reached : 1,
lives_used: lives_used,
duration_seconds: duration_secs,
end_reason: 'quit'
});
});
/* Internal helper: send the session-end POST (used by both the C wrapper
* and the beforeunload fallback). */
EM_JS(void, js_analytics_send_end, (int score, int level_reached,
int lives_used, int duration_secs,
const char *end_reason_ptr), {
const char *end_reason_ptr,
const char *player_name_ptr), {
/* Helper that performs the actual end request given a session id. */
function doEnd(sid, endReason, score, levelReached, livesUsed, durationSecs) {
var body = JSON.stringify({
function doEnd(sid, endReason, playerName, score, levelReached,
livesUsed, durationSecs) {
var payload = {
score: score,
level_reached: levelReached > 0 ? levelReached : 1,
lives_used: livesUsed,
duration_seconds: durationSecs,
end_reason: endReason
});
};
if (playerName) payload.player_name = playerName;
var body = JSON.stringify(payload);
/* Stash stats for the beforeunload fallback */
Module._analyticsLastStats = body;
@@ -143,6 +162,7 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
if (!Module._analyticsUrl) return;
var endReason = UTF8ToString(end_reason_ptr);
var playerName = player_name_ptr ? UTF8ToString(player_name_ptr) : '';
/* If session start is still in-flight, wait for it before ending. */
if (Module._analyticsStartPending) {
@@ -153,7 +173,8 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
var sid = Module._analyticsSessionId;
if (sid) {
Module._analyticsSessionId = null;
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
doEnd(sid, endReason, playerName, score, level_reached,
lives_used, duration_secs);
}
});
return;
@@ -164,7 +185,8 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
var sid = Module._analyticsSessionId;
/* Clear synchronously before the async request to prevent races */
Module._analyticsSessionId = null;
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
doEnd(sid, endReason, playerName, score, level_reached,
lives_used, duration_secs);
});
/* ── C wrappers ─────────────────────────────────────────────────── */
@@ -177,14 +199,26 @@ void analytics_session_start(void) {
js_analytics_session_start();
}
void analytics_session_end(GameStats *stats, const char *end_reason) {
void analytics_session_end(GameStats *stats, const char *end_reason,
const char *player_name) {
stats_update_score(stats);
js_analytics_send_end(
stats->score,
stats->levels_completed > 0 ? stats->levels_completed : 1,
stats->deaths,
(int)stats->time_elapsed,
end_reason
end_reason,
player_name
);
}
void analytics_stash_stats(GameStats *stats) {
stats_update_score(stats);
js_analytics_stash_stats(
stats->score,
stats->levels_completed > 0 ? stats->levels_completed : 1,
stats->deaths,
(int)stats->time_elapsed
);
}
@@ -197,9 +231,15 @@ void analytics_init(void) {
void analytics_session_start(void) {}
void analytics_session_end(GameStats *stats, const char *end_reason) {
void analytics_session_end(GameStats *stats, const char *end_reason,
const char *player_name) {
(void)stats;
(void)end_reason;
(void)player_name;
}
void analytics_stash_stats(GameStats *stats) {
(void)stats;
}
#endif /* __EMSCRIPTEN__ */

View File

@@ -12,7 +12,14 @@ void analytics_session_start(void);
/* End the current analytics session with final stats.
* Computes the composite score before sending.
* end_reason: "death", "quit", "timeout", or "completed". */
void analytics_session_end(GameStats *stats, const char *end_reason);
* end_reason: "death", "quit", "timeout", or "completed".
* player_name may be NULL or empty if no name was entered. */
void analytics_session_end(GameStats *stats, const char *end_reason,
const char *player_name);
/* Stash current stats into JS so the beforeunload fallback has
* real data if the user closes the tab mid-game. Call periodically
* (e.g. once per second or on level completion). No-op on native. */
void analytics_stash_stats(GameStats *stats);
#endif /* JNR_ANALYTICS_H */

View File

@@ -306,8 +306,7 @@ static void handle_collisions(EntityManager *em) {
}
/* ── Enemy contact damage to player ──── */
if (player && !(player->flags & ENTITY_INVINCIBLE) &&
entity_is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
if (player && entity_is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
if (physics_overlap(&a->body, &player->body)) {
/* Check if player is stomping (falling onto enemy from above) */
bool stomping = (player->body.vel.y > 0) &&
@@ -319,7 +318,7 @@ static void handle_collisions(EntityManager *em) {
damage_entity(a, 2);
if (a->flags & ENTITY_DEAD) stats_record_kill();
player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f;
} else {
} else if (!(player->flags & ENTITY_INVINCIBLE)) {
/* Charger deals extra damage and knockback while charging */
int dmg = a->damage;
if (a->type == ENT_ENEMY_CHARGER) {
@@ -592,18 +591,14 @@ void level_update(Level *level, float dt) {
/* Fallback direct exit zone check (for levels without ship exit) */
check_exit_zones(level);
/* Check for player respawn (skip if player boarded exit ship) */
/* Check for player death (skip if player boarded exit ship).
* Don't auto-respawn — set a flag for main.c to handle lives. */
if (!level->exit_ship_boarded) {
for (int i = 0; i < level->entities.count; i++) {
Entity *e = &level->entities.entities[i];
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
stats_record_death();
player_respawn(e, level->map.player_spawn);
Vec2 center = vec2(
e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f
);
camera_follow(&level->camera, center, vec2_zero(), dt);
level->player_death_pending = true;
break;
}
}
@@ -760,6 +755,18 @@ void level_render(Level *level, float interpolation) {
renderer_draw_rect(pos, size, heart_color, LAYER_HUD, cam);
}
/* Draw lives counter next to hearts */
{
GameStats *st = stats_get_active();
if (st && st->lives > 0) {
char lives_buf[8];
snprintf(lives_buf, sizeof(lives_buf), "x%d", st->lives);
int lx = 8 + player->max_health * 14 + 4;
font_draw_text(g_engine.renderer, lives_buf, lx, 8,
(SDL_Color){200, 200, 200, 255});
}
}
/* Draw jetpack charge indicators */
int charges, max_charges;
float recharge_pct;
@@ -818,6 +825,17 @@ void level_render(Level *level, float interpolation) {
renderer_flush(cam);
}
void level_respawn_player(Level *level) {
level->player_death_pending = false;
for (int i = 0; i < level->entities.count; i++) {
Entity *e = &level->entities.entities[i];
if (e->active && e->type == ENT_PLAYER) {
player_respawn(e, level->map.player_spawn);
break;
}
}
}
void level_free(Level *level) {
audio_stop_music();

View File

@@ -27,6 +27,9 @@ typedef struct Level {
bool exit_ship_spawned; /* exit spacecraft has been spawned */
bool exit_ship_boarded; /* player has entered the ship */
int exit_zone_idx; /* which exit zone the ship is for */
/* ── Death / game over ────────────────── */
bool player_death_pending; /* player died, awaiting main.c */
} Level;
bool level_load(Level *level, const char *path);
@@ -39,4 +42,8 @@ void level_free(Level *level);
* The target path is stored in level->exit_target. */
bool level_exit_triggered(const Level *level);
/* Respawn the player at the level's spawn point. Called by main.c
* after acknowledging a death (decrementing lives). */
void level_respawn_player(Level *level);
#endif /* JNR_LEVEL_H */

View File

@@ -308,8 +308,8 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
pd->dash_timer = PLAYER_DASH_DURATION;
pd->dash_dir = vec2(0.0f, 1.0f);
/* Brief invincibility during dash */
pd->inv_timer = PLAYER_DASH_DURATION;
/* Invincibility during dash + short grace period after */
pd->inv_timer = PLAYER_DASH_DURATION + PLAYER_DASH_INV_GRACE;
self->flags |= ENTITY_INVINCIBLE;
/* Jetpack burst downward */

View File

@@ -39,6 +39,7 @@
/* Invincibility after taking damage */
#define PLAYER_INV_TIME 1.5f /* seconds of invincibility */
#define PLAYER_DASH_INV_GRACE 0.15f /* extra invincibility after dash */
/* Aim direction (for shooting) */
typedef enum AimDir {

View File

@@ -16,6 +16,7 @@ typedef struct GameStats {
int damage_taken; /* total HP lost */
int damage_dealt; /* total HP dealt to enemies */
float time_elapsed; /* wall-clock seconds */
int lives; /* remaining lives (set by main) */
} GameStats;
/* Reset all stats to zero (call at session start). */

View File

@@ -1,6 +1,7 @@
#include "engine/core.h"
#include "engine/input.h"
#include "engine/font.h"
#include "engine/debuglog.h"
#include "game/level.h"
#include "game/levelgen.h"
#include "game/editor.h"
@@ -26,6 +27,7 @@ typedef enum GameMode {
MODE_EDITOR,
MODE_PAUSED,
MODE_TRANSITION,
MODE_GAMEOVER,
} GameMode;
static Level s_level;
@@ -34,6 +36,7 @@ static GameMode s_mode = MODE_PLAY;
static bool s_use_procgen = false;
static bool s_dump_lvl = false;
static bool s_use_editor = false;
static bool s_use_debuglog = false;
static uint32_t s_gen_seed = 0;
static char s_edit_path[256] = {0};
static char s_level_path[ASSET_PATH_MAX] = {0}; /* path of active play-mode level */
@@ -54,10 +57,22 @@ static int s_mars_depth = 0;
static GameStats s_stats;
static bool s_session_active = false;
/* ── Lives system ── */
#define STARTING_LIVES 3
static int s_lives = STARTING_LIVES;
static float s_stash_timer = 0.0f; /* timer for periodic stats stash */
#define STASH_INTERVAL 2.0f /* seconds between stats stashes */
/* ── Pause menu state ── */
#define PAUSE_ITEM_COUNT 3
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
/* ── Game over state ── */
#define NAME_MAX_LEN 3
static char s_go_name[NAME_MAX_LEN + 1] = "AAA"; /* player name for scoreboard */
static int s_go_cursor = 0; /* which character is selected */
static bool s_go_submitted = false; /* name has been submitted */
/* ── Level transition state ── */
static TransitionState s_transition;
static char s_pending_target[ASSET_PATH_MAX] = {0}; /* exit target stashed during transition */
@@ -94,6 +109,7 @@ static const char *theme_name(LevelTheme t) {
static bool load_level_file(const char *path) {
if (!level_load(&s_level, path)) return false;
snprintf(s_level_path, sizeof(s_level_path), "%s", path);
debuglog_set_level(&s_level, path);
return true;
}
@@ -192,6 +208,7 @@ static void load_generated_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0'; /* generated levels have no file path */
debuglog_set_level(&s_level, "generated");
}
static void load_station_level(void) {
@@ -217,6 +234,7 @@ static void load_station_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0'; /* generated levels have no file path */
debuglog_set_level(&s_level, "generated:station");
}
static void load_mars_base_level(void) {
@@ -248,26 +266,31 @@ static void load_mars_base_level(void) {
g_engine.running = false;
}
s_level_path[0] = '\0';
debuglog_set_level(&s_level, "generated:mars_base");
}
/* ── Analytics session helpers ── */
static void begin_session(void) {
stats_reset(&s_stats);
s_lives = STARTING_LIVES;
s_stats.lives = s_lives;
s_stash_timer = 0.0f;
stats_set_active(&s_stats);
analytics_session_start();
s_session_active = true;
}
static void end_session(const char *reason) {
static void end_session(const char *reason, const char *player_name) {
if (!s_session_active) return;
s_session_active = false;
stats_set_active(NULL);
analytics_session_end(&s_stats, reason);
analytics_session_end(&s_stats, reason, player_name);
}
/* ── Switch to editor mode ── */
static void enter_editor(void) {
if (s_mode == MODE_PLAY) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
}
s_mode = MODE_EDITOR;
@@ -307,6 +330,7 @@ static void enter_test_play(void) {
/* ── Return from test play to editor ── */
static void return_to_editor(void) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
s_mode = MODE_EDITOR;
s_testing_from_editor = false;
@@ -315,6 +339,7 @@ static void return_to_editor(void) {
/* ── Restart current level (file-based or generated) ── */
static void restart_level(void) {
debuglog_set_level(NULL, NULL);
level_free(&s_level);
if (s_level_path[0]) {
if (!load_level_file(s_level_path)) {
@@ -329,10 +354,11 @@ static void restart_level(void) {
/* ── Level load dispatch — loads the next level based on target string ── */
static void dispatch_level_load(const char *target) {
debuglog_set_level(NULL, NULL);
if (target[0] == '\0') {
/* Empty target = victory / end of game. */
printf("Level complete! (no next level)\n");
end_session("completed");
end_session("completed", NULL);
level_free(&s_level);
s_station_depth = 0;
s_mars_depth = 0;
@@ -420,7 +446,7 @@ static void pause_update(void) {
break;
case 1: /* Restart */
s_mode = MODE_PLAY;
end_session("quit");
end_session("quit", NULL);
restart_level();
begin_session();
break;
@@ -428,7 +454,7 @@ static void pause_update(void) {
if (s_testing_from_editor) {
return_to_editor();
} else {
end_session("quit");
end_session("quit", NULL);
g_engine.running = false;
}
break;
@@ -452,11 +478,12 @@ static void game_update(float dt) {
return;
}
end_session("quit");
end_session("quit", NULL);
/* Tear down whatever mode we are in. */
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|| s_mode == MODE_TRANSITION) {
|| s_mode == MODE_TRANSITION || s_mode == MODE_GAMEOVER) {
debuglog_set_level(NULL, NULL);
transition_reset(&s_transition);
level_free(&s_level);
}
@@ -500,6 +527,11 @@ static void game_update(float dt) {
return;
}
if (s_mode == MODE_GAMEOVER) {
gameover_update();
return;
}
if (s_mode == MODE_TRANSITION) {
transition_update(&s_transition, dt, &s_level.camera);
@@ -524,6 +556,11 @@ static void game_update(float dt) {
/* ── Play mode ── */
/* F12: dump debug log to text file. */
if (input_key_pressed(SDL_SCANCODE_F12) && debuglog_is_enabled()) {
debuglog_dump("debug_log.txt");
}
/* Pause on escape (return to editor during test play) */
if (input_pressed(ACTION_PAUSE)) {
if (s_testing_from_editor) {
@@ -539,6 +576,7 @@ static void game_update(float dt) {
if (!s_testing_from_editor && input_key_pressed(SDL_SCANCODE_E)) {
/* Load the current level file into the editor if available */
snprintf(s_edit_path, sizeof(s_edit_path), "%s", s_level_path);
debuglog_set_level(NULL, NULL);
level_free(&s_level);
enter_editor();
return;
@@ -549,7 +587,8 @@ static void game_update(float dt) {
bool r_pressed = input_key_held(SDL_SCANCODE_R);
if (r_pressed && !r_was_pressed) {
printf("\n=== Regenerating level ===\n");
end_session("quit");
end_session("quit", NULL);
debuglog_set_level(NULL, NULL);
level_free(&s_level);
s_gen_seed = (uint32_t)time(NULL);
s_use_procgen = true;
@@ -565,6 +604,25 @@ static void game_update(float dt) {
s_stats.time_elapsed += dt;
}
/* Check for player death — decrement lives or trigger game over */
if (s_level.player_death_pending) {
s_level.player_death_pending = false;
s_lives--;
s_stats.lives = s_lives;
if (s_lives <= 0) {
enter_gameover();
return;
}
level_respawn_player(&s_level);
}
/* Periodically stash stats so beforeunload has real data */
s_stash_timer += dt;
if (s_stash_timer >= STASH_INTERVAL && s_session_active) {
analytics_stash_stats(&s_stats);
s_stash_timer = 0.0f;
}
/* Check for level exit transition */
if (level_exit_triggered(&s_level)) {
const char *target = s_level.exit_target;
@@ -629,6 +687,140 @@ static void pause_render(void) {
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
}
/* ── Enter game over mode ── */
static void enter_gameover(void) {
s_mode = MODE_GAMEOVER;
s_go_cursor = 0;
s_go_submitted = false;
memcpy(s_go_name, "AAA", NAME_MAX_LEN + 1);
/* Finalize score for display */
stats_update_score(&s_stats);
}
/* ── Game over: handle name entry input ── */
static void gameover_update(void) {
if (s_go_submitted) {
/* After submission, wait for confirm to restart */
bool confirm = input_pressed(ACTION_JUMP)
|| input_key_pressed(SDL_SCANCODE_RETURN)
|| input_key_pressed(SDL_SCANCODE_RETURN2);
if (confirm) {
end_session("death", s_go_name);
debuglog_set_level(NULL, NULL);
level_free(&s_level);
s_station_depth = 0;
s_mars_depth = 0;
s_mode = MODE_PLAY;
if (!load_level_file("assets/levels/moon01.lvl")) {
g_engine.running = false;
}
begin_session();
}
return;
}
/* Cycle letter up/down */
if (input_pressed(ACTION_UP)) {
s_go_name[s_go_cursor]++;
if (s_go_name[s_go_cursor] > 'Z') s_go_name[s_go_cursor] = 'A';
}
if (input_pressed(ACTION_DOWN)) {
s_go_name[s_go_cursor]--;
if (s_go_name[s_go_cursor] < 'A') s_go_name[s_go_cursor] = 'Z';
}
/* Move cursor left/right */
if (input_pressed(ACTION_LEFT)) {
if (s_go_cursor > 0) s_go_cursor--;
}
if (input_pressed(ACTION_RIGHT)) {
if (s_go_cursor < NAME_MAX_LEN - 1) s_go_cursor++;
}
/* Confirm name */
bool confirm = input_pressed(ACTION_JUMP)
|| input_key_pressed(SDL_SCANCODE_RETURN)
|| input_key_pressed(SDL_SCANCODE_RETURN2);
if (confirm) {
s_go_submitted = true;
}
}
/* ── Game over: render overlay ── */
static void gameover_render(void) {
SDL_Renderer *r = g_engine.renderer;
/* Dark overlay */
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(r, 0, 0, 0, 180);
SDL_Rect overlay = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
SDL_RenderFillRect(r, &overlay);
SDL_Color col_title = {220, 50, 50, 255};
SDL_Color col_text = {200, 200, 200, 255};
SDL_Color col_score = {255, 220, 80, 255};
SDL_Color col_dim = {120, 120, 130, 255};
int cy = SCREEN_HEIGHT / 2 - 50;
font_draw_text_centered(r, "GAME OVER", cy, SCREEN_WIDTH, col_title);
cy += 20;
/* Show final score */
{
char score_buf[32];
snprintf(score_buf, sizeof(score_buf), "SCORE: %d", s_stats.score);
font_draw_text_centered(r, score_buf, cy, SCREEN_WIDTH, col_score);
cy += 20;
}
if (!s_go_submitted) {
font_draw_text_centered(r, "ENTER YOUR NAME", cy, SCREEN_WIDTH, col_text);
cy += 16;
/* Draw name characters with cursor highlight */
{
char name_display[16];
snprintf(name_display, sizeof(name_display), "%c %c %c",
s_go_name[0], s_go_name[1], s_go_name[2]);
int nw = font_text_width(name_display);
int nx = (SCREEN_WIDTH - nw) / 2;
int spacing = nw / 3;
for (int i = 0; i < NAME_MAX_LEN; i++) {
char ch[2] = { s_go_name[i], '\0' };
SDL_Color c = (i == s_go_cursor) ? col_score : col_text;
int cx = nx + i * spacing;
font_draw_text(r, ch, cx, cy, c);
/* Draw cursor arrow above selected character */
if (i == s_go_cursor) {
font_draw_text(r, "^", cx, cy + 10, col_score);
}
}
cy += 24;
}
font_draw_text_centered(r, "UP/DOWN=LETTER LEFT/RIGHT=MOVE",
cy, SCREEN_WIDTH, col_dim);
cy += 12;
font_draw_text_centered(r, "JUMP=CONFIRM", cy, SCREEN_WIDTH, col_dim);
} else {
/* Name submitted — show confirmation */
{
char msg[32];
snprintf(msg, sizeof(msg), "NAME: %s", s_go_name);
font_draw_text_centered(r, msg, cy, SCREEN_WIDTH, col_text);
cy += 20;
}
font_draw_text_centered(r, "PRESS JUMP TO CONTINUE",
cy, SCREEN_WIDTH, col_dim);
}
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
}
static void game_render(float interpolation) {
if (s_mode == MODE_EDITOR) {
editor_render(&s_editor, interpolation);
@@ -640,19 +832,25 @@ static void game_render(float interpolation) {
/* Render the level (frozen) with the transition overlay on top. */
level_render(&s_level, interpolation);
transition_render(&s_transition);
} else if (s_mode == MODE_GAMEOVER) {
/* Render frozen game frame with game over overlay. */
level_render(&s_level, interpolation);
gameover_render();
} else {
level_render(&s_level, interpolation);
}
}
static void game_shutdown(void) {
end_session("quit");
end_session("quit", NULL);
debuglog_set_level(NULL, NULL);
/* Always free both — editor may have been initialized even if we're
* currently in play mode (e.g. shutdown during test play). editor_free
* and level_free are safe to call on zeroed/already-freed structs. */
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|| s_mode == MODE_TRANSITION || s_testing_from_editor) {
|| s_mode == MODE_TRANSITION || s_mode == MODE_GAMEOVER
|| s_testing_from_editor) {
level_free(&s_level);
}
if (s_mode == MODE_EDITOR || s_use_editor) {
@@ -675,6 +873,8 @@ int main(int argc, char *argv[]) {
if (i + 1 < argc) {
s_gen_seed = (uint32_t)atoi(argv[++i]);
}
} else if (strcmp(argv[i], "--debug-log") == 0) {
s_use_debuglog = true;
} else if (strcmp(argv[i], "--edit") == 0 || strcmp(argv[i], "-e") == 0) {
s_use_editor = true;
/* Optional: next arg is a file path */
@@ -687,6 +887,7 @@ int main(int argc, char *argv[]) {
printf(" --dump, -d Dump generated level to assets/levels/generated.lvl\n");
printf(" --seed N, -s N Set RNG seed for generation\n");
printf(" --edit [file], -e [file] Open level editor (optionally load a .lvl file)\n");
printf(" --debug-log Record game state every tick (F12 to dump)\n");
printf("\nIn-game:\n");
printf(" R Regenerate level with new random seed\n");
printf(" E Open level editor\n");
@@ -739,6 +940,11 @@ int main(int argc, char *argv[]) {
return 1;
}
debuglog_init();
if (s_use_debuglog) {
debuglog_enable();
}
engine_set_callbacks((GameCallbacks){
.init = game_init,
.update = game_update,
@@ -747,6 +953,7 @@ int main(int argc, char *argv[]) {
});
engine_run();
debuglog_shutdown();
engine_shutdown();
return 0;