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
Turrets now emit orange-white spark particles when taking non-lethal
damage, giving clear visual feedback on hits. On death, turrets get a
dedicated metal explosion effect (shrapnel, hot sparks, flash, smoke)
instead of the generic death puff, with stronger screen shake.
Closes#14
Enemies that fall past the bottom of the level are now instantly destroyed,
preventing them from accumulating off-screen. Cliff detection uses a
speed-scaled lookahead so faster enemies reverse earlier. Charger deals
double damage during charge and knockback scales with the attacker's speed.
Revert to git.kimchi, add curl diagnostics to understand why
auth fails even after login succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Gitea container registry token service scopes tokens to ROOT_URL
(git.schick-web.site). Pushing to the internal hostname (git.kimchi)
causes auth failures because the token domain doesn't match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
buildah login succeeds but push doesn't pick up the stored auth.
Skip login and pass --creds directly to each push command instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unnecessary registries.conf write (host already has it).
Add set -ex and echo markers between commands to pinpoint the hang.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
--tls-verify=false on login/push alone was not sufficient to prevent
the deploy from hanging. Register git.kimchi as an insecure registry
via registries.conf and add --tls-verify=false to buildah bud as well.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the editor is active, the JS shell level-select dropdown was
unconditionally switching to MODE_PLAY. Now it detects MODE_EDITOR
and calls editor_load() to load the selected level into the editor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the WASM build into a multi-stage Containerfile so the emscripten
compilation happens inside the Docker build. This eliminates the separate
container action step, enables Docker layer caching for faster rebuilds,
and makes the Containerfile self-contained.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the player's current score using the font renderer,
right-aligned with an 8px margin from the top-right screen edge.
Closes#5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The score computation used wrong weights and terms compared to DESIGN.md.
Updated to: enemies_killed*100 + levels_completed*500 - deaths*200 - time_elapsed
Closes#6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Double pressing the down arrow while airborne triggers a free downward
dash that doesn't consume jetpack charges. Uses a 0.3s window for the
double-tap detection, resets on landing.
Closes#9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mars02.lvl (Mars Base interior) incorrectly used PARALLAX_STYLE 5
(Mars surface), causing atmosphere dust particles to spawn indoors.
Changed to PARALLAX_STYLE 2 (Interior) to match the level's setting.
Closes#4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When wind is zero or near-zero, dust particles were always spawning at
the left viewport edge (the "upwind" edge for wind >= 0). Without wind
force to carry them across, they accumulated on the left side. Now
particles spawn across the full viewport when wind is calm (< 5 px/s²),
with random drift directions for even distribution.
Closes#2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set the Horchposten backend URL in the shell data attribute and replace
the plaintext key lookup with XOR-encoded byte arrays decoded at runtime,
so the API key never appears as a readable string in source, HTML, or the
compiled WASM binary.
1. Race condition: session_end now waits for an in-flight
session_start promise before sending, so quick restarts
don't drop the end call.
2. beforeunload: replaced sendBeacon (which can't set headers)
with fetch(..., keepalive: true) so the X-API-Key header
is included and the backend doesn't 401.
3. Stats double-counting: removed stats_record_damage_dealt
and stats_record_kill from damage_entity (which was called
for all damage including player deaths). Now only recorded
at player-sourced call sites (projectile hits, stomps).
4. Removed const-cast: analytics_session_end now takes
GameStats* (non-const) since stats_update_score mutates it.
5. beforeunload now uses stashed stats from the last C-side
session_end call instead of hardcoded zeroes. Session ID is
cleared synchronously before async fetch to prevent races.
6. Removed unused stdint.h include from stats.h.
Implements session-based analytics tracking that sends gameplay
stats to the Horchposten API. Adds stats.{c,h} for accumulating
per-session metrics (kills, deaths, shots, dashes, jumps, pickups,
damage, time) and analytics.{c,h} with EM_JS bridge for fetch()
calls to the backend. Client ID is persisted in localStorage.
Session start/end hooks are wired into all game lifecycle events
(level transitions, restart, quit, tab close via sendBeacon).
Analytics URL/key are configured via data attributes on the canvas
container. Non-WASM builds compile with no-op stubs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
static-web-server's default cache-control sends max-age=31536000 (1 year)
for .js files but only 1 day for .wasm. After redeployment, Cloudflare CDN
serves the cached old .js with a fresh .wasm, causing EM_ASM address table
mismatches and runtime crashes. Disable built-in cache headers at the origin
so Cloudflare respects new content on each deploy.
Also update AGENTS.md: add deploy commands, fix emsdk path, document the
Cloudflare cache-purge requirement, and correct stale MAX_ENTITY_SPAWNS
and MAX_EXIT_ZONES values.
Beam raycast now skips the tile the turret is mounted on, so
wall-embedded laser turrets can shoot outward.
Extract entity_is_enemy() into engine/entity.c as single source
of truth for enemy-type checks. Replaces triplicated type lists
in level.c, drone.c, and projectile.c that caused the collision
bug when new enemy types were added.