Add pause menu, laser turret, charger/spawner enemies, and Mars campaign
Implement four feature phases: Phase 1 - Pause menu: extract bitmap font into shared engine/font module, add MODE_PAUSED with Resume/Restart/Quit overlay. Phase 2 - Laser turret hazard: ENT_LASER_TURRET with charge/fire/ cooldown state machine, per-pixel beam raycast, two variants (fixed and tracking). Registered in entity registry with editor icons. Phase 3 - Charger and Spawner enemies: charger ground patrol with detect/telegraph/charge/stun cycle (2s charge timeout), spawner that periodically creates grunts up to a global cap of 3. Phase 4 - Mars campaign: two handcrafted levels (mars01 surface, mars02 base), mars_tileset.png, PARALLAX_STYLE_MARS with salmon sky and red mesas, THEME_MARS_SURFACE/THEME_MARS_BASE for the procedural generator with per-theme gravity/tileset/parallax. Moon campaign now chains moon03 -> mars01 -> mars02 -> victory. Also fix review findings: deterministic seed on generated level restart, NULL checks on calloc in spawn functions, charge timeout to prevent infinite charge on flat terrain, and stop suppressing stderr in Makefile web-serve target so real errors are visible.
This commit is contained in:
6
Makefile
6
Makefile
@@ -128,9 +128,9 @@ web-serve: web
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Serving at http://localhost:$(WEB_PORT)/jnr.html"
|
@echo "Serving at http://localhost:$(WEB_PORT)/jnr.html"
|
||||||
@echo "Ctrl+C to stop."
|
@echo "Ctrl+C to stop."
|
||||||
@python3 -m http.server $(WEB_PORT) --directory dist-web 2>/dev/null || \
|
@python3 -m http.server $(WEB_PORT) --directory dist-web || \
|
||||||
python -m http.server $(WEB_PORT) --directory dist-web 2>/dev/null || \
|
python -m http.server $(WEB_PORT) --directory dist-web || \
|
||||||
echo "Python not found."
|
echo "Error: Python not found. Install python3 to serve."
|
||||||
|
|
||||||
# ── Windows cross-compilation ───────────────────
|
# ── Windows cross-compilation ───────────────────
|
||||||
WIN_DIST := dist-win64
|
WIN_DIST := dist-win64
|
||||||
|
|||||||
38
TODO.md
38
TODO.md
@@ -67,3 +67,41 @@ blue-white flare, 18 particles mixed into regular burst) and
|
|||||||
`particle_emit_jetpack_boost_trail()` (blue sparks + pale blue wisps,
|
`particle_emit_jetpack_boost_trail()` (blue sparks + pale blue wisps,
|
||||||
3 particles/frame). Both activate only when `jetpack_boost_timer > 0`.
|
3 particles/frame). Both activate only when `jetpack_boost_timer > 0`.
|
||||||
Burst fires on dash start, trail emits each frame during dash.
|
Burst fires on dash start, trail emits each frame during dash.
|
||||||
|
|
||||||
|
## ~~Pause menu~~ ✓
|
||||||
|
Implemented: extracted bitmap font from `editor.c` into shared `engine/font`
|
||||||
|
module (`font.h`/`font.c`). Added `MODE_PAUSED` game state to `main.c` with
|
||||||
|
semi-transparent overlay, menu items (Resume / Restart / Quit), up/down
|
||||||
|
navigation, confirm with jump/enter. Restart reloads file-based levels or
|
||||||
|
regenerates procedural levels.
|
||||||
|
|
||||||
|
## ~~Laser turret hazard~~ ✓
|
||||||
|
Implemented: `ENT_LASER_TURRET` entity with state machine (IDLE → CHARGING →
|
||||||
|
FIRING → COOLDOWN). Beam uses per-pixel raycast via `tilemap_is_solid()`.
|
||||||
|
Player damage via point-to-line distance check. Rendering with
|
||||||
|
`SDL_RenderDrawLine` and perpendicular offset for beam thickness.
|
||||||
|
Two variants: `laser_turret` (fixed, aims left) and `laser_turret_track`
|
||||||
|
(rotates toward player at 1.5 rad/s during idle/charge, locks on fire).
|
||||||
|
Both registered in entity registry with editor icons.
|
||||||
|
|
||||||
|
## ~~New enemies: Charger and Spawner~~ ✓
|
||||||
|
Implemented in `enemy.h`/`enemy.c`:
|
||||||
|
- **Charger** — Ground patrol → ALERT (0.5 s telegraph) → CHARGE (150 px/s
|
||||||
|
rush) → STUNNED (0.8 s on wall hit, reverses). 2 HP. Detects player in
|
||||||
|
horizontal line-of-sight within 200 px.
|
||||||
|
- **Spawner** — Stationary. Spawns grunts every 4.5 s (max 3 alive via
|
||||||
|
`count_alive_grunts()`). Pulses before spawn. 3 HP, destructible. Purple
|
||||||
|
color scheme. Both registered in entity registry with editor icons.
|
||||||
|
|
||||||
|
## ~~Mars campaign~~ ✓
|
||||||
|
Implemented: two handcrafted levels plus procedural generator support.
|
||||||
|
- **mars01.lvl** (250×23, Mars Surface): low gravity (370), wind, wide-open
|
||||||
|
red terrain, charger + grunt enemies, spacecraft intro. New
|
||||||
|
`mars_tileset.png` and `PARALLAX_STYLE_MARS` (salmon sky, red mesas, dust).
|
||||||
|
- **mars02.lvl** (40×46, Mars Base): normal gravity (700), tall vertical
|
||||||
|
corridors with narrow passages, turrets, laser turrets (fixed + tracking),
|
||||||
|
spawners, chargers, grunts. Victory exit at bottom.
|
||||||
|
- Generator: `THEME_MARS_SURFACE` / `THEME_MARS_BASE` with per-theme gravity,
|
||||||
|
bg color, parallax style, tileset path, segment probabilities, and height
|
||||||
|
zone assignment. Mars themes added to procedural progression (6 options).
|
||||||
|
- Moon campaign now chains to Mars: moon03 → mars01 → mars02 → victory.
|
||||||
|
|||||||
62
assets/levels/mars01.lvl
Normal file
62
assets/levels/mars01.lvl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
TILESET assets/tiles/mars_tileset.png
|
||||||
|
SIZE 250 23
|
||||||
|
SPAWN 3 18
|
||||||
|
GRAVITY 370
|
||||||
|
WIND 25
|
||||||
|
BG_COLOR 30 12 8
|
||||||
|
PARALLAX_STYLE 5
|
||||||
|
MUSIC assets/sounds/algardalgar.ogg
|
||||||
|
|
||||||
|
ENTITY spacecraft 1 14
|
||||||
|
|
||||||
|
# Charger patrols across flat sections
|
||||||
|
ENTITY charger 40 18
|
||||||
|
ENTITY charger 75 18
|
||||||
|
ENTITY charger 120 14
|
||||||
|
ENTITY charger 165 18
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||||
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 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
|
||||||
108
assets/levels/mars02.lvl
Normal file
108
assets/levels/mars02.lvl
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Mars Base - Underground Corridors
|
||||||
|
# =================================
|
||||||
|
# Second Mars level: normal gravity, tall vertical layout.
|
||||||
|
# Narrow corridors with 90-degree turns, heavy enemy presence.
|
||||||
|
# Spacecraft intro from mars surface. Laser turrets, spawners,
|
||||||
|
# turrets, and chargers guard the base interior.
|
||||||
|
|
||||||
|
TILESET assets/tiles/mars_tileset.png
|
||||||
|
SIZE 40 46
|
||||||
|
SPAWN 3 7
|
||||||
|
GRAVITY 700
|
||||||
|
BG_COLOR 20 10 6
|
||||||
|
PARALLAX_STYLE 5
|
||||||
|
MUSIC assets/sounds/algardalgar.ogg
|
||||||
|
|
||||||
|
ENTITY spacecraft 1 3
|
||||||
|
|
||||||
|
# Upper zone - entry corridors (rows 0-17)
|
||||||
|
ENTITY turret 18 6
|
||||||
|
ENTITY laser_turret 30 6
|
||||||
|
ENTITY grunt 10 8
|
||||||
|
ENTITY grunt 25 8
|
||||||
|
ENTITY charger 14 16
|
||||||
|
ENTITY powerup_hp 20 14
|
||||||
|
|
||||||
|
# Mid zone - vertical shaft and crossover (rows 17-30)
|
||||||
|
ENTITY laser_turret_track 6 20
|
||||||
|
ENTITY spawner 34 20
|
||||||
|
ENTITY turret 22 24
|
||||||
|
ENTITY grunt 28 24
|
||||||
|
ENTITY charger 10 28
|
||||||
|
ENTITY powerup_jet 18 22
|
||||||
|
|
||||||
|
# Lower zone - base core (rows 30-43)
|
||||||
|
ENTITY laser_turret 15 32
|
||||||
|
ENTITY laser_turret_track 32 36
|
||||||
|
ENTITY spawner 8 38
|
||||||
|
ENTITY turret 28 38
|
||||||
|
ENTITY charger 20 42
|
||||||
|
ENTITY grunt 35 42
|
||||||
|
ENTITY powerup_hp 24 34
|
||||||
|
ENTITY powerup_gun 12 42
|
||||||
|
|
||||||
|
# Exit at bottom right — victory (end of Mars campaign)
|
||||||
|
EXIT 36 40 2 3
|
||||||
|
|
||||||
|
# Tile definitions (Mars tileset)
|
||||||
|
TILEDEF 1 0 0 1
|
||||||
|
TILEDEF 2 1 0 1
|
||||||
|
TILEDEF 3 2 0 1
|
||||||
|
TILEDEF 4 0 1 2
|
||||||
|
|
||||||
|
# Collision layer (40 wide x 46 tall)
|
||||||
|
# Layout: narrow corridors with vertical shafts connecting zones
|
||||||
|
# W = wall (1), P = platform (4), 0 = air
|
||||||
|
#
|
||||||
|
# Upper zone (rows 0-17): entry from top-left, horizontal corridor
|
||||||
|
# with a drop into the shaft system
|
||||||
|
# Mid zone (rows 17-30): vertical shaft on the left, rooms on right
|
||||||
|
# Lower zone (rows 30-43): base core, final gauntlet
|
||||||
|
# Bottom rows (44-45): solid floor
|
||||||
|
LAYER collision
|
||||||
|
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
|
||||||
|
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 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 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 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 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 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 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 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 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 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 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 1
|
||||||
|
1 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 0 0 0 0 0 0 0 0 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 1
|
||||||
|
1 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 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 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 1
|
||||||
|
1 1 1 1 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 1 1 1 1 1 1 0 0 1 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 1 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 1
|
||||||
|
1 0 0 0 0 0 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 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 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 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 0 0 0 0 4 4 4 0 0 0 0 4 4 4 0 0 0 0 4 4 4 0 0 0 0 0 0 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 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 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
|
||||||
|
1 4 4 4 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 0 0 0 4 4 4 0 0 0 0 0 0 4 4 4 0 1 0 0 0 4 4 4 0 0 0 0 1 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 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 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 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 0 4 4 4 0 0 0 1 0 0 0 0 4 4 4 0 0 0 0 0 0 4 4 4 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 1 1 0 0 0 1 1 1 1 0 0 0 1 1 1 1 0 0 0 0 0 0 1
|
||||||
|
1 0 0 0 0 0 0 0 1 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 1
|
||||||
|
1 0 0 0 0 0 0 0 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 1
|
||||||
|
1 0 0 0 0 0 0 0 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 1
|
||||||
|
1 4 4 4 0 0 0 0 1 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 1
|
||||||
|
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
|
||||||
|
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
|
||||||
|
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
|
||||||
@@ -22,7 +22,7 @@ ENTITY asteroid 125 0
|
|||||||
# Gun powerup near the exit — the player finally gets armed
|
# Gun powerup near the exit — the player finally gets armed
|
||||||
ENTITY powerup_gun 130 18
|
ENTITY powerup_gun 130 18
|
||||||
|
|
||||||
EXIT 146 17 2 3 assets/levels/level01.lvl
|
EXIT 146 17 2 3 assets/levels/mars01.lvl
|
||||||
|
|
||||||
# Tile definitions
|
# Tile definitions
|
||||||
TILEDEF 1 0 0 1
|
TILEDEF 1 0 0 1
|
||||||
|
|||||||
BIN
assets/tiles/mars_tileset.png
Normal file
BIN
assets/tiles/mars_tileset.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -28,6 +28,9 @@ typedef enum EntityType {
|
|||||||
ENT_DRONE,
|
ENT_DRONE,
|
||||||
ENT_ASTEROID,
|
ENT_ASTEROID,
|
||||||
ENT_SPACECRAFT,
|
ENT_SPACECRAFT,
|
||||||
|
ENT_LASER_TURRET,
|
||||||
|
ENT_ENEMY_CHARGER,
|
||||||
|
ENT_SPAWNER,
|
||||||
ENT_TYPE_COUNT
|
ENT_TYPE_COUNT
|
||||||
} EntityType;
|
} EntityType;
|
||||||
|
|
||||||
|
|||||||
121
src/engine/font.c
Normal file
121
src/engine/font.c
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "engine/font.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Minimal 4x7 bitmap font
|
||||||
|
*
|
||||||
|
* Each character is 4 pixels wide, 7 pixels tall.
|
||||||
|
* Stored as 7 rows of 4 bits (packed in a uint32_t).
|
||||||
|
* Covers ASCII 32-95 (space through underscore).
|
||||||
|
* Lowercase maps to uppercase automatically.
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* 4-bit rows packed: row0 in bits 24-27, row1 in 20-23, etc.
|
||||||
|
* Bit order: MSB = leftmost pixel. */
|
||||||
|
static const uint32_t s_font_glyphs[64] = {
|
||||||
|
/* */ 0x000000,
|
||||||
|
/* ! */ 0x4444404,
|
||||||
|
/* " */ 0xAA0000,
|
||||||
|
/* # */ 0xAFAFA0,
|
||||||
|
/* $ */ 0x4E6E40,
|
||||||
|
/* % */ 0x924924,
|
||||||
|
/* & */ 0x4A4AC0,
|
||||||
|
/* ' */ 0x440000,
|
||||||
|
/* ( */ 0x248840,
|
||||||
|
/* ) */ 0x842240,
|
||||||
|
/* * */ 0xA4A000,
|
||||||
|
/* + */ 0x04E400,
|
||||||
|
/* , */ 0x000048,
|
||||||
|
/* - */ 0x00E000,
|
||||||
|
/* . */ 0x000040,
|
||||||
|
/* / */ 0x224880,
|
||||||
|
/* 0 */ 0x6999960,
|
||||||
|
/* 1 */ 0x2622620,
|
||||||
|
/* 2 */ 0x6912460,
|
||||||
|
/* 3 */ 0x6921960,
|
||||||
|
/* 4 */ 0x2AAF220,
|
||||||
|
/* 5 */ 0xF88E1E0,
|
||||||
|
/* 6 */ 0x688E960,
|
||||||
|
/* 7 */ 0xF112440,
|
||||||
|
/* 8 */ 0x6966960,
|
||||||
|
/* 9 */ 0x6997120,
|
||||||
|
/* : */ 0x040400,
|
||||||
|
/* ; */ 0x040480,
|
||||||
|
/* < */ 0x248420,
|
||||||
|
/* = */ 0x0E0E00,
|
||||||
|
/* > */ 0x842480,
|
||||||
|
/* ? */ 0x6920400,
|
||||||
|
/* @ */ 0x69B9860,
|
||||||
|
/* A */ 0x699F990,
|
||||||
|
/* B */ 0xE99E9E0,
|
||||||
|
/* C */ 0x6988960,
|
||||||
|
/* D */ 0xE999E00,
|
||||||
|
/* E */ 0xF8E8F00,
|
||||||
|
/* F */ 0xF8E8800,
|
||||||
|
/* G */ 0x698B960,
|
||||||
|
/* H */ 0x99F9900,
|
||||||
|
/* I */ 0xE444E00,
|
||||||
|
/* J */ 0x7111960,
|
||||||
|
/* K */ 0x9ACA900,
|
||||||
|
/* L */ 0x8888F00,
|
||||||
|
/* M */ 0x9FF9900,
|
||||||
|
/* N */ 0x9DDB900,
|
||||||
|
/* O */ 0x6999600,
|
||||||
|
/* P */ 0xE99E800,
|
||||||
|
/* Q */ 0x6999A70,
|
||||||
|
/* R */ 0xE99EA90,
|
||||||
|
/* S */ 0x698E960,
|
||||||
|
/* T */ 0xF444400,
|
||||||
|
/* U */ 0x9999600,
|
||||||
|
/* V */ 0x999A400,
|
||||||
|
/* W */ 0x999FF90,
|
||||||
|
/* X */ 0x996690,
|
||||||
|
/* Y */ 0x996440,
|
||||||
|
/* Z */ 0xF12480,
|
||||||
|
/* [ */ 0x688860,
|
||||||
|
/* \ */ 0x884220,
|
||||||
|
/* ] */ 0x622260,
|
||||||
|
/* ^ */ 0x4A0000,
|
||||||
|
/* _ */ 0x00000F,
|
||||||
|
};
|
||||||
|
|
||||||
|
void font_draw_char(SDL_Renderer *r, char ch, int x, int y, SDL_Color col) {
|
||||||
|
int idx = 0;
|
||||||
|
if (ch >= 'a' && ch <= 'z') ch -= 32; /* to uppercase */
|
||||||
|
if (ch >= 32 && ch <= 95) idx = ch - 32;
|
||||||
|
else return;
|
||||||
|
|
||||||
|
uint32_t glyph = s_font_glyphs[idx];
|
||||||
|
SDL_SetRenderDrawColor(r, col.r, col.g, col.b, col.a);
|
||||||
|
|
||||||
|
for (int row = 0; row < FONT_H; row++) {
|
||||||
|
int shift = (FONT_H - 1 - row) * 4;
|
||||||
|
int bits = (glyph >> shift) & 0xF;
|
||||||
|
for (int col_bit = 0; col_bit < FONT_W; col_bit++) {
|
||||||
|
if (bits & (1 << (FONT_W - 1 - col_bit))) {
|
||||||
|
SDL_RenderDrawPoint(r, x + col_bit, y + row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void font_draw_text(SDL_Renderer *r, const char *text, int x, int y, SDL_Color col) {
|
||||||
|
while (*text) {
|
||||||
|
font_draw_char(r, *text, x, y, col);
|
||||||
|
x += FONT_W + FONT_SPACING;
|
||||||
|
text++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void font_draw_text_centered(SDL_Renderer *r, const char *text, int y,
|
||||||
|
int total_width, SDL_Color col) {
|
||||||
|
int w = font_text_width(text);
|
||||||
|
int x = (total_width - w) / 2;
|
||||||
|
font_draw_text(r, text, x, y, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
int font_text_width(const char *text) {
|
||||||
|
int len = (int)strlen(text);
|
||||||
|
if (len == 0) return 0;
|
||||||
|
return len * (FONT_W + FONT_SPACING) - FONT_SPACING;
|
||||||
|
}
|
||||||
32
src/engine/font.h
Normal file
32
src/engine/font.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef JNR_FONT_H
|
||||||
|
#define JNR_FONT_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Minimal 4x7 bitmap font
|
||||||
|
*
|
||||||
|
* Each character is 4 pixels wide, 7 pixels tall.
|
||||||
|
* Covers ASCII 32-95 (space through underscore).
|
||||||
|
* Lowercase maps to uppercase automatically.
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
#define FONT_W 4
|
||||||
|
#define FONT_H 7
|
||||||
|
#define FONT_SPACING 1 /* 1px gap between characters */
|
||||||
|
|
||||||
|
/* Draw a single character at pixel position (x, y). */
|
||||||
|
void font_draw_char(SDL_Renderer *r, char ch, int x, int y, SDL_Color col);
|
||||||
|
|
||||||
|
/* Draw a null-terminated string at pixel position (x, y). */
|
||||||
|
void font_draw_text(SDL_Renderer *r, const char *text, int x, int y, SDL_Color col);
|
||||||
|
|
||||||
|
/* Draw text centered horizontally on the given y, within a region of
|
||||||
|
* total_width pixels starting at x=0. */
|
||||||
|
void font_draw_text_centered(SDL_Renderer *r, const char *text, int y,
|
||||||
|
int total_width, SDL_Color col);
|
||||||
|
|
||||||
|
/* Return the pixel width of a string (without trailing spacing). */
|
||||||
|
int font_text_width(const char *text);
|
||||||
|
|
||||||
|
#endif /* JNR_FONT_H */
|
||||||
@@ -900,6 +900,156 @@ static void generate_moon_near(Parallax *p, SDL_Renderer *renderer) {
|
|||||||
p->near_layer.owns_texture = true;
|
p->near_layer.owns_texture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Mars: salmon sky, red mesas, dust ──────────────── */
|
||||||
|
|
||||||
|
static void generate_mars_far(Parallax *p, SDL_Renderer *renderer) {
|
||||||
|
int w = SCREEN_WIDTH;
|
||||||
|
int h = SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||||
|
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||||
|
if (!tex) return;
|
||||||
|
|
||||||
|
SDL_SetRenderTarget(renderer, tex);
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
|
unsigned int saved_seed = (unsigned int)rand();
|
||||||
|
srand(91);
|
||||||
|
|
||||||
|
/* Salmon/butterscotch sky gradient (upper half) */
|
||||||
|
for (int y = 0; y < h / 2; y++) {
|
||||||
|
float t = (float)y / (float)(h / 2);
|
||||||
|
uint8_t r = clamp_u8((int)(60 + 40 * t));
|
||||||
|
uint8_t g = clamp_u8((int)(25 + 20 * t));
|
||||||
|
uint8_t b = clamp_u8((int)(15 + 10 * t));
|
||||||
|
SDL_SetRenderDrawColor(renderer, r, g, b, (uint8_t)(20 + (int)(30 * t)));
|
||||||
|
SDL_Rect row = {0, y, w, 1};
|
||||||
|
SDL_RenderFillRect(renderer, &row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dim stars visible through thin atmosphere */
|
||||||
|
for (int i = 0; i < 30; i++) {
|
||||||
|
int x = (int)(randf() * w);
|
||||||
|
int y = (int)(randf() * h * 0.35f);
|
||||||
|
uint8_t bright = (uint8_t)(50 + (int)(randf() * 60));
|
||||||
|
SDL_SetRenderDrawColor(renderer, bright, clamp_u8(bright - 15),
|
||||||
|
clamp_u8(bright - 30), (uint8_t)(60 + (int)(randf() * 40)));
|
||||||
|
SDL_Rect dot = {x, y, 1, 1};
|
||||||
|
SDL_RenderFillRect(renderer, &dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Distant mesa/mountain silhouette — flat-topped with vertical cliffs. */
|
||||||
|
int base_y = (int)(h * 0.65f);
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
float t = (float)x / (float)w;
|
||||||
|
/* Mesa profile: flat plateaus interrupted by steep drops. */
|
||||||
|
float mesa = sinf(t * 6.28f * 1.5f + 0.8f) * 12.0f;
|
||||||
|
float ridge = sinf(t * 6.28f * 4.1f + 2.0f) * 6.0f;
|
||||||
|
float detail = sinf(t * 6.28f * 9.7f) * 3.0f;
|
||||||
|
/* Flatten tops: clamp positive values to create plateaus */
|
||||||
|
float profile = mesa + ridge + detail;
|
||||||
|
if (profile > 8.0f) profile = 8.0f + (profile - 8.0f) * 0.2f;
|
||||||
|
int peak = base_y - (int)profile;
|
||||||
|
if (peak > base_y + 5) peak = base_y + 5;
|
||||||
|
|
||||||
|
for (int y = peak; y < h; y++) {
|
||||||
|
int depth = y - peak;
|
||||||
|
/* Dark reddish-brown silhouette */
|
||||||
|
uint8_t r = clamp_u8(25 + depth / 4);
|
||||||
|
uint8_t g = clamp_u8(10 + depth / 8);
|
||||||
|
uint8_t b = clamp_u8(8 + depth / 10);
|
||||||
|
uint8_t a = (uint8_t)(depth < 2 ? 100 : 160);
|
||||||
|
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
||||||
|
SDL_Rect px = {x, y, 1, 1};
|
||||||
|
SDL_RenderFillRect(renderer, &px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srand(saved_seed);
|
||||||
|
SDL_SetRenderTarget(renderer, NULL);
|
||||||
|
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||||
|
|
||||||
|
p->far_layer.texture = tex;
|
||||||
|
p->far_layer.tex_w = w;
|
||||||
|
p->far_layer.tex_h = h;
|
||||||
|
p->far_layer.scroll_x = 0.03f;
|
||||||
|
p->far_layer.scroll_y = 0.03f;
|
||||||
|
p->far_layer.active = true;
|
||||||
|
p->far_layer.owns_texture = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generate_mars_near(Parallax *p, SDL_Renderer *renderer) {
|
||||||
|
int w = SCREEN_WIDTH;
|
||||||
|
int h = SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||||
|
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||||
|
if (!tex) return;
|
||||||
|
|
||||||
|
SDL_SetRenderTarget(renderer, tex);
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
|
||||||
|
unsigned int saved_seed = (unsigned int)rand();
|
||||||
|
srand(203);
|
||||||
|
|
||||||
|
/* Dust haze bands in warm reds and oranges */
|
||||||
|
typedef struct { uint8_t r, g, b; } DustColor;
|
||||||
|
DustColor dust_palette[] = {
|
||||||
|
{100, 40, 20}, /* rust */
|
||||||
|
{ 80, 30, 15}, /* dark rust */
|
||||||
|
{ 90, 50, 25}, /* sandy rust */
|
||||||
|
{110, 45, 30}, /* bright rust */
|
||||||
|
{ 70, 35, 35}, /* muted red */
|
||||||
|
};
|
||||||
|
int dust_count = sizeof(dust_palette) / sizeof(dust_palette[0]);
|
||||||
|
|
||||||
|
for (int band = 0; band < 5; band++) {
|
||||||
|
float cy = randf() * h * 0.6f + h * 0.2f;
|
||||||
|
DustColor col = dust_palette[band % dust_count];
|
||||||
|
int blobs = 15 + (int)(randf() * 20);
|
||||||
|
for (int b = 0; b < blobs; b++) {
|
||||||
|
int bx = (int)(randf() * w);
|
||||||
|
int by = (int)(cy + (randf() - 0.5f) * 60.0f);
|
||||||
|
int bw = 20 + (int)(randf() * 40);
|
||||||
|
int bh = 4 + (int)(randf() * 8);
|
||||||
|
uint8_t br = clamp_u8(col.r + (int)(randf() * 20 - 10));
|
||||||
|
uint8_t bg = clamp_u8(col.g + (int)(randf() * 15 - 7));
|
||||||
|
uint8_t bb = clamp_u8(col.b + (int)(randf() * 15 - 7));
|
||||||
|
SDL_SetRenderDrawColor(renderer, br, bg, bb,
|
||||||
|
(uint8_t)(5 + (int)(randf() * 10)));
|
||||||
|
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
||||||
|
SDL_RenderFillRect(renderer, &rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scattered dust particles */
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
int x = (int)(randf() * w);
|
||||||
|
int y = (int)(randf() * h);
|
||||||
|
DustColor col = dust_palette[(int)(randf() * dust_count)];
|
||||||
|
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
||||||
|
(uint8_t)(20 + (int)(randf() * 30)));
|
||||||
|
SDL_Rect dot = {x, y, 1 + (int)(randf() * 2), 1};
|
||||||
|
SDL_RenderFillRect(renderer, &dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
srand(saved_seed);
|
||||||
|
SDL_SetRenderTarget(renderer, NULL);
|
||||||
|
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||||
|
|
||||||
|
p->near_layer.texture = tex;
|
||||||
|
p->near_layer.tex_w = w;
|
||||||
|
p->near_layer.tex_h = h;
|
||||||
|
p->near_layer.scroll_x = 0.10f;
|
||||||
|
p->near_layer.scroll_y = 0.06f;
|
||||||
|
p->near_layer.active = true;
|
||||||
|
p->near_layer.owns_texture = true;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Themed parallax dispatcher ─────────────────────── */
|
/* ── Themed parallax dispatcher ─────────────────────── */
|
||||||
|
|
||||||
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style) {
|
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style) {
|
||||||
@@ -920,6 +1070,10 @@ void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle
|
|||||||
generate_moon_far(p, renderer);
|
generate_moon_far(p, renderer);
|
||||||
generate_moon_near(p, renderer);
|
generate_moon_near(p, renderer);
|
||||||
break;
|
break;
|
||||||
|
case PARALLAX_STYLE_MARS:
|
||||||
|
generate_mars_far(p, renderer);
|
||||||
|
generate_mars_near(p, renderer);
|
||||||
|
break;
|
||||||
case PARALLAX_STYLE_DEFAULT:
|
case PARALLAX_STYLE_DEFAULT:
|
||||||
default:
|
default:
|
||||||
parallax_generate_stars(p, renderer);
|
parallax_generate_stars(p, renderer);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ typedef enum ParallaxStyle {
|
|||||||
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
|
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
|
||||||
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
|
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
|
||||||
PARALLAX_STYLE_MOON, /* moon surface: craters, grey terrain */
|
PARALLAX_STYLE_MOON, /* moon surface: craters, grey terrain */
|
||||||
|
PARALLAX_STYLE_MARS, /* Mars: salmon sky, red mesas, dust */
|
||||||
} ParallaxStyle;
|
} ParallaxStyle;
|
||||||
|
|
||||||
/* Generate both layers with a unified style */
|
/* Generate both layers with a unified style */
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "engine/renderer.h"
|
#include "engine/renderer.h"
|
||||||
#include "engine/assets.h"
|
#include "engine/assets.h"
|
||||||
#include "engine/camera.h"
|
#include "engine/camera.h"
|
||||||
|
#include "engine/font.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -104,124 +105,7 @@ void editor_load_vfs_file(const char *path) {
|
|||||||
|
|
||||||
#endif /* __EMSCRIPTEN__ */
|
#endif /* __EMSCRIPTEN__ */
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* Bitmap font provided by engine/font.h (included above). */
|
||||||
* Minimal 4x6 bitmap font
|
|
||||||
*
|
|
||||||
* Each character is 4 pixels wide, 6 pixels tall.
|
|
||||||
* Stored as 6 rows of 4 bits (packed in a uint32_t).
|
|
||||||
* Covers ASCII 32-95 (space through underscore).
|
|
||||||
* Lowercase maps to uppercase automatically.
|
|
||||||
* ═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
#define FONT_W 4
|
|
||||||
#define FONT_H 7
|
|
||||||
|
|
||||||
/* 4-bit rows packed: row0 in bits 20-23, row1 in 16-19, etc.
|
|
||||||
* Bit order: MSB = leftmost pixel */
|
|
||||||
static const uint32_t s_font_glyphs[64] = {
|
|
||||||
/* */ 0x000000,
|
|
||||||
/* ! */ 0x4444404,
|
|
||||||
/* " */ 0xAA0000,
|
|
||||||
/* # */ 0xAFAFA0,
|
|
||||||
/* $ */ 0x4E6E40, /* simplified $ */
|
|
||||||
/* % */ 0x924924, /* simplified % */
|
|
||||||
/* & */ 0x4A4AC0,
|
|
||||||
/* ' */ 0x440000,
|
|
||||||
/* ( */ 0x248840,
|
|
||||||
/* ) */ 0x842240,
|
|
||||||
/* * */ 0xA4A000,
|
|
||||||
/* + */ 0x04E400,
|
|
||||||
/* , */ 0x000048,
|
|
||||||
/* - */ 0x00E000,
|
|
||||||
/* . */ 0x000040,
|
|
||||||
/* / */ 0x224880,
|
|
||||||
/* 0 */ 0x6999960,
|
|
||||||
/* 1 */ 0x2622620,
|
|
||||||
/* 2 */ 0x6912460,
|
|
||||||
/* 3 */ 0x6921960,
|
|
||||||
/* 4 */ 0x2AAF220,
|
|
||||||
/* 5 */ 0xF88E1E0,
|
|
||||||
/* 6 */ 0x688E960,
|
|
||||||
/* 7 */ 0xF112440,
|
|
||||||
/* 8 */ 0x6966960,
|
|
||||||
/* 9 */ 0x6997120,
|
|
||||||
/* : */ 0x040400,
|
|
||||||
/* ; */ 0x040480,
|
|
||||||
/* < */ 0x248420,
|
|
||||||
/* = */ 0x0E0E00,
|
|
||||||
/* > */ 0x842480,
|
|
||||||
/* ? */ 0x6920400,
|
|
||||||
/* @ */ 0x69B9860,
|
|
||||||
/* A */ 0x699F990,
|
|
||||||
/* B */ 0xE99E9E0,
|
|
||||||
/* C */ 0x6988960,
|
|
||||||
/* D */ 0xE999E00,
|
|
||||||
/* E */ 0xF8E8F00, /* simplified E/F overlap */
|
|
||||||
/* F */ 0xF8E8800,
|
|
||||||
/* G */ 0x698B960,
|
|
||||||
/* H */ 0x99F9900,
|
|
||||||
/* I */ 0xE444E00,
|
|
||||||
/* J */ 0x7111960,
|
|
||||||
/* K */ 0x9ACA900,
|
|
||||||
/* L */ 0x8888F00,
|
|
||||||
/* M */ 0x9FF9900,
|
|
||||||
/* N */ 0x9DDB900,
|
|
||||||
/* O */ 0x6999600,
|
|
||||||
/* P */ 0xE99E800,
|
|
||||||
/* Q */ 0x6999A70,
|
|
||||||
/* R */ 0xE99EA90,
|
|
||||||
/* S */ 0x698E960, /* reuse from earlier; close enough */
|
|
||||||
/* T */ 0xF444400,
|
|
||||||
/* U */ 0x9999600,
|
|
||||||
/* V */ 0x999A400,
|
|
||||||
/* W */ 0x999FF90, /* simplified W */
|
|
||||||
/* X */ 0x996690, /* simplified X */
|
|
||||||
/* Y */ 0x996440,
|
|
||||||
/* Z */ 0xF12480, /* simplified Z */
|
|
||||||
/* [ */ 0x688860,
|
|
||||||
/* \ */ 0x884220,
|
|
||||||
/* ] */ 0x622260,
|
|
||||||
/* ^ */ 0x4A0000,
|
|
||||||
/* _ */ 0x00000F,
|
|
||||||
};
|
|
||||||
|
|
||||||
static void draw_char(SDL_Renderer *r, char ch, int x, int y, SDL_Color col) {
|
|
||||||
int idx = 0;
|
|
||||||
if (ch >= 'a' && ch <= 'z') ch -= 32; /* to uppercase */
|
|
||||||
if (ch >= 32 && ch <= 95) idx = ch - 32;
|
|
||||||
else return;
|
|
||||||
|
|
||||||
uint32_t glyph = s_font_glyphs[idx];
|
|
||||||
SDL_SetRenderDrawColor(r, col.r, col.g, col.b, col.a);
|
|
||||||
|
|
||||||
for (int row = 0; row < FONT_H; row++) {
|
|
||||||
/* Extract 4 bits for this row */
|
|
||||||
int shift = (FONT_H - 1 - row) * 4;
|
|
||||||
int bits = (glyph >> shift) & 0xF;
|
|
||||||
for (int col_bit = 0; col_bit < FONT_W; col_bit++) {
|
|
||||||
if (bits & (1 << (FONT_W - 1 - col_bit))) {
|
|
||||||
SDL_RenderDrawPoint(r, x + col_bit, y + row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void draw_text(SDL_Renderer *r, const char *text, int x, int y, SDL_Color col) {
|
|
||||||
while (*text) {
|
|
||||||
draw_char(r, *text, x, y, col);
|
|
||||||
x += FONT_W + 1; /* 1px spacing */
|
|
||||||
text++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Unused for now but useful for future centered text layouts */
|
|
||||||
#if 0
|
|
||||||
static int text_width(const char *text) {
|
|
||||||
int len = (int)strlen(text);
|
|
||||||
if (len == 0) return 0;
|
|
||||||
return len * (FONT_W + 1) - 1;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
* 8x8 pixel mini-icons for the entity palette
|
* 8x8 pixel mini-icons for the entity palette
|
||||||
@@ -263,6 +147,14 @@ static const uint64_t s_icon_bitmaps[ICON_COUNT] = {
|
|||||||
0x001C3E7F7F3E1C00ULL,
|
0x001C3E7F7F3E1C00ULL,
|
||||||
/* ICON_SPACECRAFT: ship */
|
/* ICON_SPACECRAFT: ship */
|
||||||
0x0018183C7E7E2400ULL,
|
0x0018183C7E7E2400ULL,
|
||||||
|
/* ICON_LASER: laser turret (box with beam line) */
|
||||||
|
0x003C3C18187E0000ULL,
|
||||||
|
/* ICON_LASER_TRACK: tracking laser (box with rotating beam) */
|
||||||
|
0x003C3C1818660000ULL,
|
||||||
|
/* ICON_CHARGER: arrow/charging creature */
|
||||||
|
0x0018187E7E181800ULL,
|
||||||
|
/* ICON_SPAWNER: pulsing core with dots */
|
||||||
|
0x24003C3C3C002400ULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static void draw_icon(SDL_Renderer *r, EditorIcon icon,
|
static void draw_icon(SDL_Renderer *r, EditorIcon icon,
|
||||||
@@ -1429,7 +1321,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
draw_icon(r, (EditorIcon)reg->icon,
|
draw_icon(r, (EditorIcon)reg->icon,
|
||||||
(int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
(int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
||||||
} else if (reg && reg->display[0] && zw >= 6) {
|
} else if (reg && reg->display[0] && zw >= 6) {
|
||||||
draw_char(r, reg->display[0], (int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
font_draw_char(r, reg->display[0], (int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1441,7 +1333,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||||
SDL_Rect sr = {(int)sp.x, (int)sp.y, (int)(zs + 0.5f), (int)(zs + 0.5f)};
|
SDL_Rect sr = {(int)sp.x, (int)sp.y, (int)(zs + 0.5f), (int)(zs + 0.5f)};
|
||||||
SDL_RenderDrawRect(r, &sr);
|
SDL_RenderDrawRect(r, &sr);
|
||||||
draw_text(r, "SP", (int)sp.x + 1, (int)sp.y + 1, COL_SPAWN);
|
font_draw_text(r, "SP", (int)sp.x + 1, (int)sp.y + 1, COL_SPAWN);
|
||||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1458,9 +1350,9 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
SDL_RenderFillRect(r, &er);
|
SDL_RenderFillRect(r, &er);
|
||||||
SDL_SetRenderDrawColor(r, COL_EXIT.r, COL_EXIT.g, COL_EXIT.b, 220);
|
SDL_SetRenderDrawColor(r, COL_EXIT.r, COL_EXIT.g, COL_EXIT.b, 220);
|
||||||
SDL_RenderDrawRect(r, &er);
|
SDL_RenderDrawRect(r, &er);
|
||||||
draw_text(r, "EXIT", (int)sp.x + 1, (int)sp.y + 1, COL_EXIT);
|
font_draw_text(r, "EXIT", (int)sp.x + 1, (int)sp.y + 1, COL_EXIT);
|
||||||
if (ez->target[0]) {
|
if (ez->target[0]) {
|
||||||
draw_text(r, ez->target, (int)sp.x + 1, (int)sp.y + 8, COL_EXIT);
|
font_draw_text(r, ez->target, (int)sp.x + 1, (int)sp.y + 8, COL_EXIT);
|
||||||
}
|
}
|
||||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
}
|
}
|
||||||
@@ -1514,7 +1406,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
for (int i = 0; i < TOOL_COUNT; i++) {
|
for (int i = 0; i < TOOL_COUNT; i++) {
|
||||||
int bx = i * 35 + 2;
|
int bx = i * 35 + 2;
|
||||||
SDL_Color tc = (i == (int)ed->tool) ? COL_HIGHLIGHT : COL_TEXT_DIM;
|
SDL_Color tc = (i == (int)ed->tool) ? COL_HIGHLIGHT : COL_TEXT_DIM;
|
||||||
draw_text(r, s_tool_names[i], bx, text_y, tc);
|
font_draw_text(r, s_tool_names[i], bx, text_y, tc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Separator */
|
/* Separator */
|
||||||
@@ -1527,17 +1419,17 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
for (int i = 0; i < EDITOR_LAYER_COUNT; i++) {
|
for (int i = 0; i < EDITOR_LAYER_COUNT; i++) {
|
||||||
int bx = layer_start + i * 25;
|
int bx = layer_start + i * 25;
|
||||||
SDL_Color lc = (i == (int)ed->active_layer) ? COL_HIGHLIGHT : COL_TEXT_DIM;
|
SDL_Color lc = (i == (int)ed->active_layer) ? COL_HIGHLIGHT : COL_TEXT_DIM;
|
||||||
draw_text(r, s_layer_names[i], bx, text_y, lc);
|
font_draw_text(r, s_layer_names[i], bx, text_y, lc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Grid & Layers indicators */
|
/* Grid & Layers indicators */
|
||||||
int grid_x = layer_start + EDITOR_LAYER_COUNT * 25 + 4;
|
int grid_x = layer_start + EDITOR_LAYER_COUNT * 25 + 4;
|
||||||
draw_text(r, ed->show_grid ? "[G]RID" : "[G]rid", grid_x, text_y,
|
font_draw_text(r, ed->show_grid ? "[G]RID" : "[G]rid", grid_x, text_y,
|
||||||
ed->show_grid ? COL_TEXT : COL_TEXT_DIM);
|
ed->show_grid ? COL_TEXT : COL_TEXT_DIM);
|
||||||
|
|
||||||
/* Tileset switch hint */
|
/* Tileset switch hint */
|
||||||
int ts_x = grid_x + 7 * (FONT_W + 1) + 4;
|
int ts_x = grid_x + 7 * (FONT_W + 1) + 4;
|
||||||
draw_text(r, "[T]SET", ts_x, text_y, COL_TEXT_DIM);
|
font_draw_text(r, "[T]SET", ts_x, text_y, COL_TEXT_DIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Right palette panel ── */
|
/* ── Right palette panel ── */
|
||||||
@@ -1565,7 +1457,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
{
|
{
|
||||||
const char *ts_name = strrchr(ed->map.tileset_path, '/');
|
const char *ts_name = strrchr(ed->map.tileset_path, '/');
|
||||||
ts_name = ts_name ? ts_name + 1 : ed->map.tileset_path;
|
ts_name = ts_name ? ts_name + 1 : ed->map.tileset_path;
|
||||||
draw_text(r, ts_name[0] ? ts_name : "TILES",
|
font_draw_text(r, ts_name[0] ? ts_name : "TILES",
|
||||||
px + 2, py + (label_h - FONT_H) / 2, COL_TEXT);
|
px + 2, py + (label_h - FONT_H) / 2, COL_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,10 +1537,10 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
SDL_Color fc = (flags & TILE_HAZARD) ? (SDL_Color){255, 80, 40, 255} :
|
SDL_Color fc = (flags & TILE_HAZARD) ? (SDL_Color){255, 80, 40, 255} :
|
||||||
(flags & TILE_PLATFORM) ? (SDL_Color){80, 200, 255, 255} :
|
(flags & TILE_PLATFORM) ? (SDL_Color){80, 200, 255, 255} :
|
||||||
(flags & TILE_SOLID) ? COL_TEXT : COL_TEXT_DIM;
|
(flags & TILE_SOLID) ? COL_TEXT : COL_TEXT_DIM;
|
||||||
draw_text(r, fname, px + 2, ent_section_y - FONT_H - 2, fc);
|
font_draw_text(r, fname, px + 2, ent_section_y - FONT_H - 2, fc);
|
||||||
/* Show [F] hint */
|
/* Show [F] hint */
|
||||||
int fw = (int)strlen(fname) * (FONT_W + 1);
|
int fw = (int)strlen(fname) * (FONT_W + 1);
|
||||||
draw_text(r, "[F]", px + 2 + fw + 2, ent_section_y - FONT_H - 2, COL_TEXT_DIM);
|
font_draw_text(r, "[F]", px + 2 + fw + 2, ent_section_y - FONT_H - 2, COL_TEXT_DIM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1659,7 +1551,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
/* ── Entity palette (bottom section) ── */
|
/* ── Entity palette (bottom section) ── */
|
||||||
{
|
{
|
||||||
int label_h = FONT_H + 6;
|
int label_h = FONT_H + 6;
|
||||||
draw_text(r, "ENTITIES", px + 2, ent_section_y + (label_h - FONT_H) / 2, COL_TEXT);
|
font_draw_text(r, "ENTITIES", px + 2, ent_section_y + (label_h - FONT_H) / 2, COL_TEXT);
|
||||||
|
|
||||||
int pal_y_start = ent_section_y + label_h;
|
int pal_y_start = ent_section_y + label_h;
|
||||||
int ent_area_h = py + ph - pal_y_start;
|
int ent_area_h = py + ph - pal_y_start;
|
||||||
@@ -1685,7 +1577,7 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
|
|
||||||
/* Name */
|
/* Name */
|
||||||
SDL_Color nc = (i == ed->selected_entity) ? COL_HIGHLIGHT : COL_TEXT;
|
SDL_Color nc = (i == ed->selected_entity) ? COL_HIGHLIGHT : COL_TEXT;
|
||||||
draw_text(r, ent->display, px + 13, ey + 2, nc);
|
font_draw_text(r, ent->display, px + 13, ey + 2, nc);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_RenderSetClipRect(r, NULL);
|
SDL_RenderSetClipRect(r, NULL);
|
||||||
@@ -1712,6 +1604,6 @@ void editor_render(Editor *ed, float interpolation) {
|
|||||||
ed->camera.zoom * 100.0f,
|
ed->camera.zoom * 100.0f,
|
||||||
ed->has_file ? ed->file_path : "new level",
|
ed->has_file ? ed->file_path : "new level",
|
||||||
ed->dirty ? " *" : "");
|
ed->dirty ? " *" : "");
|
||||||
draw_text(r, status, 2, sy + (EDITOR_STATUS_H - FONT_H) / 2, COL_TEXT);
|
font_draw_text(r, status, 2, sy + (EDITOR_STATUS_H - FONT_H) / 2, COL_TEXT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
312
src/game/enemy.c
312
src/game/enemy.c
@@ -3,6 +3,8 @@
|
|||||||
#include "game/projectile.h"
|
#include "game/projectile.h"
|
||||||
#include "engine/physics.h"
|
#include "engine/physics.h"
|
||||||
#include "engine/renderer.h"
|
#include "engine/renderer.h"
|
||||||
|
#include "engine/particle.h"
|
||||||
|
#include "engine/audio.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
@@ -270,3 +272,313 @@ Entity *flyer_spawn(EntityManager *em, Vec2 pos) {
|
|||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════
|
||||||
|
* CHARGER — detects player, telegraphs, then rushes
|
||||||
|
* ════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
#define CHARGER_ALERT_TIME 0.5f /* telegraph before charge */
|
||||||
|
#define CHARGER_STUN_TIME 0.8f /* stun duration on wall */
|
||||||
|
|
||||||
|
static EntityManager *s_charger_em = NULL;
|
||||||
|
|
||||||
|
static void charger_update(Entity *self, float dt, const Tilemap *map) {
|
||||||
|
ChargerData *cd = (ChargerData *)self->data;
|
||||||
|
if (!cd) return;
|
||||||
|
|
||||||
|
Body *body = &self->body;
|
||||||
|
|
||||||
|
/* Death sequence */
|
||||||
|
if (self->flags & ENTITY_DEAD) {
|
||||||
|
cd->death_timer -= dt;
|
||||||
|
body->vel.x = 0;
|
||||||
|
if (cd->death_timer <= 0) {
|
||||||
|
particle_emit_death_puff(body->pos, (SDL_Color){220, 120, 40, 255});
|
||||||
|
entity_destroy(s_charger_em, self);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* State machine */
|
||||||
|
switch (cd->state) {
|
||||||
|
case CHARGER_PATROL: {
|
||||||
|
body->vel.x = cd->patrol_dir * CHARGER_PATROL_SPEED;
|
||||||
|
|
||||||
|
/* Face walk direction */
|
||||||
|
if (cd->patrol_dir < 0) self->flags |= ENTITY_FACING_LEFT;
|
||||||
|
else self->flags &= ~ENTITY_FACING_LEFT;
|
||||||
|
|
||||||
|
physics_update(body, dt, map);
|
||||||
|
|
||||||
|
/* Reverse at walls */
|
||||||
|
if (body->on_wall_left || body->on_wall_right) {
|
||||||
|
cd->patrol_dir = -cd->patrol_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reverse at ledge */
|
||||||
|
if (body->on_ground) {
|
||||||
|
float cx = (cd->patrol_dir > 0) ?
|
||||||
|
body->pos.x + body->size.x + 2.0f :
|
||||||
|
body->pos.x - 2.0f;
|
||||||
|
float cy = body->pos.y + body->size.y + 4.0f;
|
||||||
|
if (!tilemap_is_solid(map, world_to_tile(cx), world_to_tile(cy))) {
|
||||||
|
cd->patrol_dir = -cd->patrol_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detect player — horizontal line-of-sight */
|
||||||
|
Entity *player = find_player(s_charger_em);
|
||||||
|
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
|
||||||
|
float px = player->body.pos.x + player->body.size.x * 0.5f;
|
||||||
|
float mx = body->pos.x + body->size.x * 0.5f;
|
||||||
|
float dy = fabsf(player->body.pos.y - body->pos.y);
|
||||||
|
float dx = px - mx;
|
||||||
|
|
||||||
|
/* Must be roughly same height and within range */
|
||||||
|
if (dy < body->size.y * 1.5f && fabsf(dx) < CHARGER_DETECT_RANGE) {
|
||||||
|
cd->state = CHARGER_ALERT;
|
||||||
|
cd->state_timer = CHARGER_ALERT_TIME;
|
||||||
|
/* Face the player */
|
||||||
|
cd->patrol_dir = (dx > 0) ? 1.0f : -1.0f;
|
||||||
|
if (cd->patrol_dir < 0) self->flags |= ENTITY_FACING_LEFT;
|
||||||
|
else self->flags &= ~ENTITY_FACING_LEFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CHARGER_ALERT: {
|
||||||
|
/* Freeze in place during telegraph */
|
||||||
|
body->vel.x = 0;
|
||||||
|
physics_update(body, dt, map);
|
||||||
|
cd->state_timer -= dt;
|
||||||
|
if (cd->state_timer <= 0) {
|
||||||
|
cd->state = CHARGER_CHARGE;
|
||||||
|
cd->state_timer = CHARGER_CHARGE_TIME;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CHARGER_CHARGE: {
|
||||||
|
body->vel.x = cd->patrol_dir * CHARGER_CHARGE_SPEED;
|
||||||
|
physics_update(body, dt, map);
|
||||||
|
cd->state_timer -= dt;
|
||||||
|
|
||||||
|
/* Hit a wall -> stunned */
|
||||||
|
if (body->on_wall_left || body->on_wall_right) {
|
||||||
|
cd->state = CHARGER_STUNNED;
|
||||||
|
cd->state_timer = CHARGER_STUN_TIME;
|
||||||
|
body->vel.x = 0;
|
||||||
|
particle_emit_spark(body->pos, (SDL_Color){255, 200, 80, 255});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ran off a ledge or charge timed out -> back to patrol */
|
||||||
|
if (!body->on_ground || cd->state_timer <= 0) {
|
||||||
|
cd->state = CHARGER_PATROL;
|
||||||
|
body->vel.x = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CHARGER_STUNNED: {
|
||||||
|
body->vel.x = 0;
|
||||||
|
physics_update(body, dt, map);
|
||||||
|
cd->state_timer -= dt;
|
||||||
|
if (cd->state_timer <= 0) {
|
||||||
|
cd->state = CHARGER_PATROL;
|
||||||
|
/* Reverse direction after stun */
|
||||||
|
cd->patrol_dir = -cd->patrol_dir;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation — use grunt anims as placeholder */
|
||||||
|
if (cd->state == CHARGER_CHARGE) {
|
||||||
|
animation_set(&self->anim, &anim_grunt_walk);
|
||||||
|
} else {
|
||||||
|
animation_set(&self->anim, &anim_grunt_idle);
|
||||||
|
}
|
||||||
|
animation_update(&self->anim, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void charger_render(Entity *self, const Camera *cam) {
|
||||||
|
Body *body = &self->body;
|
||||||
|
ChargerData *cd = (ChargerData *)self->data;
|
||||||
|
|
||||||
|
/* Colored rect fallback — orange/amber color scheme */
|
||||||
|
SDL_Color color;
|
||||||
|
if (cd && cd->state == CHARGER_ALERT) {
|
||||||
|
/* Flash during telegraph */
|
||||||
|
float blink = sinf(cd->state_timer * 30.0f);
|
||||||
|
uint8_t r = (blink > 0) ? 255 : 200;
|
||||||
|
color = (SDL_Color){r, 160, 40, 255};
|
||||||
|
} else if (cd && cd->state == CHARGER_STUNNED) {
|
||||||
|
color = (SDL_Color){160, 160, 100, 255};
|
||||||
|
} else if (cd && cd->state == CHARGER_CHARGE) {
|
||||||
|
color = (SDL_Color){255, 120, 30, 255};
|
||||||
|
} else {
|
||||||
|
color = (SDL_Color){220, 140, 40, 255};
|
||||||
|
}
|
||||||
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
||||||
|
|
||||||
|
/* Direction indicator: small triangle in facing direction */
|
||||||
|
float cx = body->pos.x + body->size.x * 0.5f;
|
||||||
|
float cy = body->pos.y + 2.0f;
|
||||||
|
bool left = (self->flags & ENTITY_FACING_LEFT) != 0;
|
||||||
|
float arrow_x = left ? body->pos.x - 3.0f : body->pos.x + body->size.x + 1.0f;
|
||||||
|
renderer_draw_rect(vec2(arrow_x, cy), vec2(2, 3),
|
||||||
|
(SDL_Color){255, 200, 60, 255}, LAYER_ENTITIES, cam);
|
||||||
|
(void)cx;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void charger_destroy(Entity *self) {
|
||||||
|
free(self->data);
|
||||||
|
self->data = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void charger_register(EntityManager *em) {
|
||||||
|
entity_register(em, ENT_ENEMY_CHARGER,
|
||||||
|
charger_update, charger_render, charger_destroy);
|
||||||
|
s_charger_em = em;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *charger_spawn(EntityManager *em, Vec2 pos) {
|
||||||
|
Entity *e = entity_spawn(em, ENT_ENEMY_CHARGER, pos);
|
||||||
|
if (!e) return NULL;
|
||||||
|
|
||||||
|
e->body.size = vec2(CHARGER_WIDTH, CHARGER_HEIGHT);
|
||||||
|
e->body.gravity_scale = 1.0f;
|
||||||
|
e->health = CHARGER_HEALTH;
|
||||||
|
e->max_health = CHARGER_HEALTH;
|
||||||
|
e->damage = 1;
|
||||||
|
|
||||||
|
ChargerData *cd = calloc(1, sizeof(ChargerData));
|
||||||
|
if (!cd) { entity_destroy(em, e); return NULL; }
|
||||||
|
cd->state = CHARGER_PATROL;
|
||||||
|
cd->patrol_dir = 1.0f;
|
||||||
|
cd->state_timer = 0;
|
||||||
|
cd->death_timer = 0.3f;
|
||||||
|
e->data = cd;
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════
|
||||||
|
* SPAWNER — stationary, periodically spawns grunts
|
||||||
|
* ════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
#define SPAWNER_PULSE_TIME 1.0f /* pulse warning before spawn */
|
||||||
|
|
||||||
|
static EntityManager *s_spawner_em = NULL;
|
||||||
|
|
||||||
|
/* Count how many grunts are currently alive. */
|
||||||
|
static int count_alive_grunts(EntityManager *em) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < em->count; i++) {
|
||||||
|
Entity *e = &em->entities[i];
|
||||||
|
if (e->active && e->type == ENT_ENEMY_GRUNT &&
|
||||||
|
!(e->flags & ENTITY_DEAD)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void spawner_update(Entity *self, float dt, const Tilemap *map) {
|
||||||
|
(void)map;
|
||||||
|
SpawnerData *sd = (SpawnerData *)self->data;
|
||||||
|
if (!sd) return;
|
||||||
|
|
||||||
|
/* Death sequence */
|
||||||
|
if (self->flags & ENTITY_DEAD) {
|
||||||
|
sd->death_timer -= dt;
|
||||||
|
if (sd->death_timer <= 0) {
|
||||||
|
particle_emit_death_puff(self->body.pos, (SDL_Color){180, 60, 180, 255});
|
||||||
|
entity_destroy(s_spawner_em, self);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sd->spawn_timer -= dt;
|
||||||
|
|
||||||
|
/* Pulse warning when about to spawn */
|
||||||
|
if (sd->spawn_timer < SPAWNER_PULSE_TIME && sd->spawn_timer > 0) {
|
||||||
|
sd->pulse_timer += dt;
|
||||||
|
} else {
|
||||||
|
sd->pulse_timer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spawn a grunt when timer expires */
|
||||||
|
if (sd->spawn_timer <= 0) {
|
||||||
|
sd->spawn_timer = SPAWNER_INTERVAL;
|
||||||
|
|
||||||
|
if (count_alive_grunts(s_spawner_em) < SPAWNER_MAX_ALIVE) {
|
||||||
|
/* Spawn grunt just below the spawner */
|
||||||
|
Vec2 spawn_pos = vec2(
|
||||||
|
self->body.pos.x,
|
||||||
|
self->body.pos.y + self->body.size.y + 2.0f
|
||||||
|
);
|
||||||
|
Entity *grunt = grunt_spawn(s_spawner_em, spawn_pos);
|
||||||
|
if (grunt) {
|
||||||
|
particle_emit_spark(spawn_pos, (SDL_Color){180, 100, 220, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void spawner_render(Entity *self, const Camera *cam) {
|
||||||
|
Body *body = &self->body;
|
||||||
|
SpawnerData *sd = (SpawnerData *)self->data;
|
||||||
|
|
||||||
|
/* Pulsing purple color when about to spawn */
|
||||||
|
SDL_Color color;
|
||||||
|
if (sd && sd->pulse_timer > 0) {
|
||||||
|
float pulse = sinf(sd->pulse_timer * 12.0f) * 0.5f + 0.5f;
|
||||||
|
uint8_t r = (uint8_t)(140 + 80 * pulse);
|
||||||
|
uint8_t b = (uint8_t)(180 + 60 * pulse);
|
||||||
|
color = (SDL_Color){r, 50, b, 255};
|
||||||
|
} else {
|
||||||
|
color = (SDL_Color){140, 50, 160, 255};
|
||||||
|
}
|
||||||
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
||||||
|
|
||||||
|
/* Small inner dot to distinguish from other hazards */
|
||||||
|
float cx = body->pos.x + body->size.x * 0.5f - 2.0f;
|
||||||
|
float cy = body->pos.y + body->size.y * 0.5f - 2.0f;
|
||||||
|
SDL_Color dot = {255, 200, 255, 255};
|
||||||
|
renderer_draw_rect(vec2(cx, cy), vec2(4, 4), dot, LAYER_ENTITIES, cam);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void spawner_destroy(Entity *self) {
|
||||||
|
free(self->data);
|
||||||
|
self->data = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void spawner_register(EntityManager *em) {
|
||||||
|
entity_register(em, ENT_SPAWNER,
|
||||||
|
spawner_update, spawner_render, spawner_destroy);
|
||||||
|
s_spawner_em = em;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *spawner_spawn(EntityManager *em, Vec2 pos) {
|
||||||
|
Entity *e = entity_spawn(em, ENT_SPAWNER, pos);
|
||||||
|
if (!e) return NULL;
|
||||||
|
|
||||||
|
e->body.size = vec2(SPAWNER_WIDTH, SPAWNER_HEIGHT);
|
||||||
|
e->body.gravity_scale = 0.0f; /* stationary */
|
||||||
|
e->health = SPAWNER_HEALTH;
|
||||||
|
e->max_health = SPAWNER_HEALTH;
|
||||||
|
e->damage = 0; /* no contact damage, not invincible */
|
||||||
|
|
||||||
|
SpawnerData *sd = calloc(1, sizeof(SpawnerData));
|
||||||
|
if (!sd) { entity_destroy(em, e); return NULL; }
|
||||||
|
sd->spawn_timer = SPAWNER_INTERVAL;
|
||||||
|
sd->death_timer = 0.4f;
|
||||||
|
sd->pulse_timer = 0;
|
||||||
|
e->data = sd;
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,4 +44,52 @@ typedef struct FlyerData {
|
|||||||
void flyer_register(EntityManager *em);
|
void flyer_register(EntityManager *em);
|
||||||
Entity *flyer_spawn(EntityManager *em, Vec2 pos);
|
Entity *flyer_spawn(EntityManager *em, Vec2 pos);
|
||||||
|
|
||||||
|
/* ── Charger enemy ─────────────────────────────────── */
|
||||||
|
/* Ground patrol that detects the player, telegraphs */
|
||||||
|
/* briefly, then charges at high speed. Stuns on wall. */
|
||||||
|
|
||||||
|
#define CHARGER_WIDTH 14
|
||||||
|
#define CHARGER_HEIGHT 16
|
||||||
|
#define CHARGER_PATROL_SPEED 30.0f /* slow patrol (px/s) */
|
||||||
|
#define CHARGER_CHARGE_SPEED 150.0f /* charge rush speed (px/s) */
|
||||||
|
#define CHARGER_DETECT_RANGE 200.0f /* horizontal detect (px) */
|
||||||
|
#define CHARGER_CHARGE_TIME 2.0f /* max charge duration (s) */
|
||||||
|
#define CHARGER_HEALTH 2
|
||||||
|
|
||||||
|
typedef enum ChargerState {
|
||||||
|
CHARGER_PATROL,
|
||||||
|
CHARGER_ALERT, /* telegraph before charging */
|
||||||
|
CHARGER_CHARGE, /* full-speed horizontal rush */
|
||||||
|
CHARGER_STUNNED, /* hit a wall, briefly vulnerable */
|
||||||
|
} ChargerState;
|
||||||
|
|
||||||
|
typedef struct ChargerData {
|
||||||
|
ChargerState state;
|
||||||
|
float patrol_dir; /* 1.0 or -1.0 */
|
||||||
|
float state_timer; /* countdown for current state */
|
||||||
|
float death_timer;
|
||||||
|
} ChargerData;
|
||||||
|
|
||||||
|
void charger_register(EntityManager *em);
|
||||||
|
Entity *charger_spawn(EntityManager *em, Vec2 pos);
|
||||||
|
|
||||||
|
/* ── Spawner enemy ─────────────────────────────────── */
|
||||||
|
/* Stationary, periodically spawns grunt enemies up to */
|
||||||
|
/* a cap. Destructible. */
|
||||||
|
|
||||||
|
#define SPAWNER_WIDTH 16
|
||||||
|
#define SPAWNER_HEIGHT 16
|
||||||
|
#define SPAWNER_HEALTH 3
|
||||||
|
#define SPAWNER_INTERVAL 4.5f /* seconds between spawns */
|
||||||
|
#define SPAWNER_MAX_ALIVE 3 /* max grunts alive at once */
|
||||||
|
|
||||||
|
typedef struct SpawnerData {
|
||||||
|
float spawn_timer; /* countdown to next spawn */
|
||||||
|
float death_timer;
|
||||||
|
float pulse_timer; /* visual pulse before spawn */
|
||||||
|
} SpawnerData;
|
||||||
|
|
||||||
|
void spawner_register(EntityManager *em);
|
||||||
|
Entity *spawner_spawn(EntityManager *em, Vec2 pos);
|
||||||
|
|
||||||
#endif /* JNR_ENEMY_H */
|
#endif /* JNR_ENEMY_H */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "game/enemy.h"
|
#include "game/enemy.h"
|
||||||
#include "game/projectile.h"
|
#include "game/projectile.h"
|
||||||
#include "game/hazards.h"
|
#include "game/hazards.h"
|
||||||
|
#include "game/laser_turret.h"
|
||||||
#include "game/powerup.h"
|
#include "game/powerup.h"
|
||||||
#include "game/drone.h"
|
#include "game/drone.h"
|
||||||
#include "game/spacecraft.h"
|
#include "game/spacecraft.h"
|
||||||
@@ -40,6 +41,10 @@ static Entity *spawn_powerup_gun(EntityManager *em, Vec2 pos) {
|
|||||||
return powerup_spawn_gun(em, pos);
|
return powerup_spawn_gun(em, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Entity *spawn_laser_track(EntityManager *em, Vec2 pos) {
|
||||||
|
return laser_turret_spawn_tracking(em, pos);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Registry population ─────────────────────────── */
|
/* ── Registry population ─────────────────────────── */
|
||||||
|
|
||||||
static void reg_add(const char *name, const char *display,
|
static void reg_add(const char *name, const char *display,
|
||||||
@@ -89,6 +94,10 @@ void entity_registry_populate(void) {
|
|||||||
reg_add("powerup_gun", "Gun Pickup", spawn_powerup_gun, (SDL_Color){200, 200, 220, 255}, 12, 12, ICON_GUN);
|
reg_add("powerup_gun", "Gun Pickup", spawn_powerup_gun, (SDL_Color){200, 200, 220, 255}, 12, 12, ICON_GUN);
|
||||||
reg_add("asteroid", "Asteroid", asteroid_spawn, (SDL_Color){140, 110, 80, 255}, ASTEROID_WIDTH, ASTEROID_HEIGHT, ICON_ASTEROID);
|
reg_add("asteroid", "Asteroid", asteroid_spawn, (SDL_Color){140, 110, 80, 255}, ASTEROID_WIDTH, ASTEROID_HEIGHT, ICON_ASTEROID);
|
||||||
reg_add("spacecraft", "Spacecraft", spacecraft_spawn, (SDL_Color){187, 187, 187, 255}, SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT, ICON_SPACECRAFT);
|
reg_add("spacecraft", "Spacecraft", spacecraft_spawn, (SDL_Color){187, 187, 187, 255}, SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT, ICON_SPACECRAFT);
|
||||||
|
reg_add("laser_turret", "Laser Turret", laser_turret_spawn, (SDL_Color){200, 80, 50, 255}, LASER_WIDTH, LASER_HEIGHT, ICON_LASER);
|
||||||
|
reg_add("laser_turret_track", "Laser Track", spawn_laser_track, (SDL_Color){220, 100, 60, 255}, LASER_WIDTH, LASER_HEIGHT, ICON_LASER_TRACK);
|
||||||
|
reg_add("charger", "Charger", charger_spawn, (SDL_Color){220, 140, 40, 255}, CHARGER_WIDTH, CHARGER_HEIGHT, ICON_CHARGER);
|
||||||
|
reg_add("spawner", "Spawner", spawner_spawn, (SDL_Color){140, 50, 160, 255}, SPAWNER_WIDTH, SPAWNER_HEIGHT, ICON_SPAWNER);
|
||||||
|
|
||||||
printf("Entity registry: %d types registered\n", g_entity_registry.count);
|
printf("Entity registry: %d types registered\n", g_entity_registry.count);
|
||||||
}
|
}
|
||||||
@@ -111,6 +120,9 @@ void entity_registry_init(EntityManager *em) {
|
|||||||
drone_register(em);
|
drone_register(em);
|
||||||
asteroid_register(em);
|
asteroid_register(em);
|
||||||
spacecraft_register(em);
|
spacecraft_register(em);
|
||||||
|
laser_turret_register(em);
|
||||||
|
charger_register(em);
|
||||||
|
spawner_register(em);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntityRegEntry *entity_registry_find(const char *name) {
|
const EntityRegEntry *entity_registry_find(const char *name) {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ typedef enum EditorIcon {
|
|||||||
ICON_GUN = 11, /* weapon pickup */
|
ICON_GUN = 11, /* weapon pickup */
|
||||||
ICON_ASTEROID = 12, /* rock */
|
ICON_ASTEROID = 12, /* rock */
|
||||||
ICON_SPACECRAFT = 13, /* ship */
|
ICON_SPACECRAFT = 13, /* ship */
|
||||||
|
ICON_LASER = 14, /* laser turret */
|
||||||
|
ICON_LASER_TRACK = 15, /* tracking laser */
|
||||||
|
ICON_CHARGER = 16, /* charging enemy */
|
||||||
|
ICON_SPAWNER = 17, /* spawner enemy */
|
||||||
ICON_COUNT,
|
ICON_COUNT,
|
||||||
ICON_NONE = -1 /* no icon (fallback) */
|
ICON_NONE = -1 /* no icon (fallback) */
|
||||||
} EditorIcon;
|
} EditorIcon;
|
||||||
|
|||||||
351
src/game/laser_turret.c
Normal file
351
src/game/laser_turret.c
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
#include "game/laser_turret.h"
|
||||||
|
#include "game/player.h"
|
||||||
|
#include "game/sprites.h"
|
||||||
|
#include "engine/core.h"
|
||||||
|
#include "engine/physics.h"
|
||||||
|
#include "engine/renderer.h"
|
||||||
|
#include "engine/particle.h"
|
||||||
|
#include "engine/camera.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Laser Turret
|
||||||
|
*
|
||||||
|
* Cycles through four states:
|
||||||
|
* IDLE (brief) -> CHARGING (1.5s, thin flickering
|
||||||
|
* indicator line) -> FIRING (2.5s, thick damaging
|
||||||
|
* beam) -> COOLDOWN (2s, beam fades) -> IDLE ...
|
||||||
|
*
|
||||||
|
* The beam is a raycast from the turret center along
|
||||||
|
* aim_angle until a solid tile or the map edge.
|
||||||
|
*
|
||||||
|
* Fixed variant: aim_angle set at spawn, never moves.
|
||||||
|
* Tracking variant: aim_angle rotates toward the
|
||||||
|
* player during IDLE and CHARGING, then locks when
|
||||||
|
* FIRING begins.
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
static EntityManager *s_laser_em = NULL;
|
||||||
|
|
||||||
|
/* ── Helpers ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
static Entity *find_player(EntityManager *em) {
|
||||||
|
for (int i = 0; i < em->count; i++) {
|
||||||
|
Entity *e = &em->entities[i];
|
||||||
|
if (e->active && e->type == ENT_PLAYER) return e;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raycast from (ox, oy) along (dx, dy) in tile steps.
|
||||||
|
* Returns the world-space endpoint where the beam hits
|
||||||
|
* a solid tile or the map boundary. Step size is one
|
||||||
|
* pixel for accuracy. Max range prevents runaway. */
|
||||||
|
static void beam_raycast(const Tilemap *map,
|
||||||
|
float ox, float oy, float dx, float dy,
|
||||||
|
float *out_x, float *out_y) {
|
||||||
|
float max_range = 800.0f; /* pixels */
|
||||||
|
float step = 1.0f;
|
||||||
|
float x = ox, y = oy;
|
||||||
|
float dist = 0.0f;
|
||||||
|
|
||||||
|
while (dist < max_range) {
|
||||||
|
x += dx * step;
|
||||||
|
y += dy * step;
|
||||||
|
dist += step;
|
||||||
|
|
||||||
|
int tx = (int)(x / TILE_SIZE);
|
||||||
|
int ty = (int)(y / TILE_SIZE);
|
||||||
|
if (tx < 0 || ty < 0 || tx >= map->width || ty >= map->height) break;
|
||||||
|
if (tilemap_is_solid(map, tx, ty)) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_x = x;
|
||||||
|
*out_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Update ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void laser_turret_update(Entity *self, float dt, const Tilemap *map) {
|
||||||
|
LaserTurretData *ld = (LaserTurretData *)self->data;
|
||||||
|
if (!ld) return;
|
||||||
|
|
||||||
|
/* Death sequence */
|
||||||
|
if (self->flags & ENTITY_DEAD) {
|
||||||
|
self->timer -= dt;
|
||||||
|
if (self->timer <= 0) {
|
||||||
|
particle_emit_death_puff(self->body.pos, (SDL_Color){255, 60, 60, 255});
|
||||||
|
entity_destroy(s_laser_em, self);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Turret center for raycasting and aiming */
|
||||||
|
float cx = self->body.pos.x + self->body.size.x * 0.5f;
|
||||||
|
float cy = self->body.pos.y + self->body.size.y * 0.5f;
|
||||||
|
|
||||||
|
/* Tracking: rotate toward player during IDLE and CHARGING */
|
||||||
|
if (ld->tracking && (ld->state == LASER_IDLE || ld->state == LASER_CHARGING)) {
|
||||||
|
Entity *player = find_player(s_laser_em);
|
||||||
|
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
|
||||||
|
float px = player->body.pos.x + player->body.size.x * 0.5f;
|
||||||
|
float py = player->body.pos.y + player->body.size.y * 0.5f;
|
||||||
|
float target_angle = atan2f(py - cy, px - cx);
|
||||||
|
|
||||||
|
/* Rotate toward target at limited speed */
|
||||||
|
float diff = target_angle - ld->aim_angle;
|
||||||
|
/* Normalize to [-PI, PI] */
|
||||||
|
while (diff > 3.14159f) diff -= 6.28318f;
|
||||||
|
while (diff < -3.14159f) diff += 6.28318f;
|
||||||
|
|
||||||
|
float max_rot = LASER_TRACK_SPEED * dt;
|
||||||
|
if (diff > max_rot) ld->aim_angle += max_rot;
|
||||||
|
else if (diff < -max_rot) ld->aim_angle -= max_rot;
|
||||||
|
else ld->aim_angle = target_angle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update facing direction from aim */
|
||||||
|
float dx = cosf(ld->aim_angle);
|
||||||
|
if (dx < 0) self->flags |= ENTITY_FACING_LEFT;
|
||||||
|
else self->flags &= ~ENTITY_FACING_LEFT;
|
||||||
|
|
||||||
|
/* Compute beam endpoint via raycast */
|
||||||
|
float dir_x = cosf(ld->aim_angle);
|
||||||
|
float dir_y = sinf(ld->aim_angle);
|
||||||
|
beam_raycast(map, cx, cy, dir_x, dir_y, &ld->beam_end_x, &ld->beam_end_y);
|
||||||
|
|
||||||
|
/* Damage cooldown */
|
||||||
|
if (ld->damage_cd > 0) ld->damage_cd -= dt;
|
||||||
|
|
||||||
|
/* State machine */
|
||||||
|
ld->timer -= dt;
|
||||||
|
if (ld->timer <= 0) {
|
||||||
|
switch (ld->state) {
|
||||||
|
case LASER_IDLE:
|
||||||
|
ld->state = LASER_CHARGING;
|
||||||
|
ld->timer = LASER_CHARGE_TIME;
|
||||||
|
break;
|
||||||
|
case LASER_CHARGING:
|
||||||
|
ld->state = LASER_FIRING;
|
||||||
|
ld->timer = LASER_FIRE_TIME;
|
||||||
|
ld->damage_cd = 0;
|
||||||
|
break;
|
||||||
|
case LASER_FIRING:
|
||||||
|
ld->state = LASER_COOLDOWN;
|
||||||
|
ld->timer = LASER_COOLDOWN_TIME;
|
||||||
|
break;
|
||||||
|
case LASER_COOLDOWN:
|
||||||
|
ld->state = LASER_IDLE;
|
||||||
|
ld->timer = 0.3f; /* brief idle before next cycle */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* During FIRING: check beam-player overlap and deal damage. */
|
||||||
|
if (ld->state == LASER_FIRING && ld->damage_cd <= 0) {
|
||||||
|
Entity *player = find_player(s_laser_em);
|
||||||
|
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
|
||||||
|
/* Test player AABB against beam line (approximated as thin rect). */
|
||||||
|
float bx0 = cx, by0 = cy;
|
||||||
|
float bx1 = ld->beam_end_x, by1 = ld->beam_end_y;
|
||||||
|
float beam_half_w = 3.0f; /* beam thickness for collision */
|
||||||
|
|
||||||
|
/* Build an AABB enclosing the beam segment */
|
||||||
|
float min_x = (bx0 < bx1 ? bx0 : bx1) - beam_half_w;
|
||||||
|
float min_y = (by0 < by1 ? by0 : by1) - beam_half_w;
|
||||||
|
float max_x = (bx0 > bx1 ? bx0 : bx1) + beam_half_w;
|
||||||
|
float max_y = (by0 > by1 ? by0 : by1) + beam_half_w;
|
||||||
|
|
||||||
|
/* Quick AABB pre-filter */
|
||||||
|
Body *pb = &player->body;
|
||||||
|
if (pb->pos.x + pb->size.x > min_x && pb->pos.x < max_x &&
|
||||||
|
pb->pos.y + pb->size.y > min_y && pb->pos.y < max_y) {
|
||||||
|
/* Finer check: point-to-line distance for player center. */
|
||||||
|
float pcx = pb->pos.x + pb->size.x * 0.5f;
|
||||||
|
float pcy = pb->pos.y + pb->size.y * 0.5f;
|
||||||
|
float lx = bx1 - bx0, ly = by1 - by0;
|
||||||
|
float len_sq = lx * lx + ly * ly;
|
||||||
|
float dist_to_beam;
|
||||||
|
if (len_sq < 0.01f) {
|
||||||
|
dist_to_beam = sqrtf((pcx - bx0) * (pcx - bx0) +
|
||||||
|
(pcy - by0) * (pcy - by0));
|
||||||
|
} else {
|
||||||
|
float t = ((pcx - bx0) * lx + (pcy - by0) * ly) / len_sq;
|
||||||
|
if (t < 0) t = 0;
|
||||||
|
if (t > 1) t = 1;
|
||||||
|
float closest_x = bx0 + t * lx;
|
||||||
|
float closest_y = by0 + t * ly;
|
||||||
|
dist_to_beam = sqrtf((pcx - closest_x) * (pcx - closest_x) +
|
||||||
|
(pcy - closest_y) * (pcy - closest_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hit if player center is within half-size of beam */
|
||||||
|
float hit_radius = (pb->size.x + pb->size.y) * 0.25f + beam_half_w;
|
||||||
|
if (dist_to_beam < hit_radius) {
|
||||||
|
player->health -= LASER_DAMAGE;
|
||||||
|
ld->damage_cd = LASER_DAMAGE_CD;
|
||||||
|
particle_emit_spark(vec2(pcx, pcy), (SDL_Color){255, 80, 40, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sparks at beam endpoint during FIRING */
|
||||||
|
if (ld->state == LASER_FIRING) {
|
||||||
|
if (rand() % 4 == 0) {
|
||||||
|
particle_emit_spark(vec2(ld->beam_end_x, ld->beam_end_y),
|
||||||
|
(SDL_Color){255, 100, 50, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Render ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Draw the beam line from turret center to endpoint.
|
||||||
|
* Uses raw SDL calls since renderer_draw_rect only handles
|
||||||
|
* axis-aligned rectangles. */
|
||||||
|
static void draw_beam_line(const Camera *cam,
|
||||||
|
float x0, float y0, float x1, float y1,
|
||||||
|
int thickness, SDL_Color col) {
|
||||||
|
Vec2 s0 = camera_world_to_screen(cam, vec2(x0, y0));
|
||||||
|
Vec2 s1 = camera_world_to_screen(cam, vec2(x1, y1));
|
||||||
|
|
||||||
|
SDL_SetRenderDrawBlendMode(g_engine.renderer, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(g_engine.renderer, col.r, col.g, col.b, col.a);
|
||||||
|
|
||||||
|
/* Draw multiple lines offset perpendicular to simulate thickness. */
|
||||||
|
float dx = s1.x - s0.x;
|
||||||
|
float dy = s1.y - s0.y;
|
||||||
|
float len = sqrtf(dx * dx + dy * dy);
|
||||||
|
if (len < 0.5f) return;
|
||||||
|
|
||||||
|
/* Perpendicular direction */
|
||||||
|
float nx = -dy / len;
|
||||||
|
float ny = dx / len;
|
||||||
|
|
||||||
|
int half = thickness / 2;
|
||||||
|
for (int i = -half; i <= half; i++) {
|
||||||
|
float ox = nx * (float)i;
|
||||||
|
float oy = ny * (float)i;
|
||||||
|
SDL_RenderDrawLine(g_engine.renderer,
|
||||||
|
(int)(s0.x + ox), (int)(s0.y + oy),
|
||||||
|
(int)(s1.x + ox), (int)(s1.y + oy));
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawBlendMode(g_engine.renderer, SDL_BLENDMODE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void laser_turret_render(Entity *self, const Camera *cam) {
|
||||||
|
LaserTurretData *ld = (LaserTurretData *)self->data;
|
||||||
|
if (!ld) return;
|
||||||
|
|
||||||
|
Body *body = &self->body;
|
||||||
|
float cx = body->pos.x + body->size.x * 0.5f;
|
||||||
|
float cy = body->pos.y + body->size.y * 0.5f;
|
||||||
|
|
||||||
|
/* Draw beam based on state */
|
||||||
|
if (ld->state == LASER_CHARGING) {
|
||||||
|
/* Thin flickering indicator line */
|
||||||
|
float progress = 1.0f - (ld->timer / LASER_CHARGE_TIME);
|
||||||
|
uint8_t alpha = (uint8_t)(80 + 100 * progress);
|
||||||
|
/* Flicker effect: modulate alpha with a fast sine */
|
||||||
|
float flicker = sinf(ld->timer * 20.0f) * 0.3f + 0.7f;
|
||||||
|
alpha = (uint8_t)((float)alpha * flicker);
|
||||||
|
|
||||||
|
SDL_Color beam_col = {255, 60, 40, alpha};
|
||||||
|
draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y,
|
||||||
|
1, beam_col);
|
||||||
|
|
||||||
|
} else if (ld->state == LASER_FIRING) {
|
||||||
|
/* Bright thick beam — core + glow */
|
||||||
|
SDL_Color glow = {255, 80, 30, 100};
|
||||||
|
draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y,
|
||||||
|
5, glow);
|
||||||
|
|
||||||
|
SDL_Color core = {255, 200, 150, 230};
|
||||||
|
draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y,
|
||||||
|
2, core);
|
||||||
|
|
||||||
|
SDL_Color center = {255, 255, 220, 255};
|
||||||
|
draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y,
|
||||||
|
0, center);
|
||||||
|
|
||||||
|
} else if (ld->state == LASER_COOLDOWN) {
|
||||||
|
/* Fading beam */
|
||||||
|
float fade = ld->timer / LASER_COOLDOWN_TIME;
|
||||||
|
uint8_t alpha = (uint8_t)(180 * fade);
|
||||||
|
SDL_Color beam_col = {255, 80, 30, alpha};
|
||||||
|
draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y,
|
||||||
|
(int)(3.0f * fade), beam_col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw turret body — colored rect fallback (no sprite yet). */
|
||||||
|
SDL_Color base_col;
|
||||||
|
if (ld->state == LASER_FIRING) {
|
||||||
|
base_col = (SDL_Color){200, 80, 50, 255};
|
||||||
|
} else if (ld->state == LASER_CHARGING) {
|
||||||
|
base_col = (SDL_Color){180, 100, 60, 255};
|
||||||
|
} else {
|
||||||
|
base_col = (SDL_Color){140, 100, 80, 255};
|
||||||
|
}
|
||||||
|
renderer_draw_rect(body->pos, body->size, base_col, LAYER_ENTITIES, cam);
|
||||||
|
|
||||||
|
/* Small dot indicating aim direction */
|
||||||
|
float dot_dist = 10.0f;
|
||||||
|
float dot_x = cx + cosf(ld->aim_angle) * dot_dist - 1.0f;
|
||||||
|
float dot_y = cy + sinf(ld->aim_angle) * dot_dist - 1.0f;
|
||||||
|
SDL_Color dot_col = {255, 50, 30, 255};
|
||||||
|
renderer_draw_rect(vec2(dot_x, dot_y), vec2(2, 2), dot_col,
|
||||||
|
LAYER_ENTITIES, cam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Destroy ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void laser_turret_destroy(Entity *self) {
|
||||||
|
free(self->data);
|
||||||
|
self->data = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Register ────────────────────────────────────── */
|
||||||
|
|
||||||
|
void laser_turret_register(EntityManager *em) {
|
||||||
|
entity_register(em, ENT_LASER_TURRET,
|
||||||
|
laser_turret_update, laser_turret_render,
|
||||||
|
laser_turret_destroy);
|
||||||
|
s_laser_em = em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Spawn ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
static Entity *laser_spawn_internal(EntityManager *em, Vec2 pos, bool tracking) {
|
||||||
|
Entity *e = entity_spawn(em, ENT_LASER_TURRET, pos);
|
||||||
|
if (!e) return NULL;
|
||||||
|
|
||||||
|
e->body.size = vec2(LASER_WIDTH, LASER_HEIGHT);
|
||||||
|
e->body.gravity_scale = 0.0f;
|
||||||
|
e->health = LASER_HEALTH;
|
||||||
|
e->max_health = LASER_HEALTH;
|
||||||
|
e->damage = 0; /* damage comes from the beam, not contact */
|
||||||
|
e->timer = 0.3f;
|
||||||
|
|
||||||
|
LaserTurretData *ld = calloc(1, sizeof(LaserTurretData));
|
||||||
|
if (!ld) { entity_destroy(em, e); return NULL; }
|
||||||
|
ld->state = LASER_IDLE;
|
||||||
|
ld->timer = 0.5f; /* brief idle before first charge */
|
||||||
|
ld->aim_angle = 3.14159f; /* default: aim left */
|
||||||
|
ld->damage_cd = 0;
|
||||||
|
ld->tracking = tracking;
|
||||||
|
e->data = ld;
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *laser_turret_spawn(EntityManager *em, Vec2 pos) {
|
||||||
|
return laser_spawn_internal(em, pos, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity *laser_turret_spawn_tracking(EntityManager *em, Vec2 pos) {
|
||||||
|
return laser_spawn_internal(em, pos, true);
|
||||||
|
}
|
||||||
48
src/game/laser_turret.h
Normal file
48
src/game/laser_turret.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#ifndef JNR_LASER_TURRET_H
|
||||||
|
#define JNR_LASER_TURRET_H
|
||||||
|
|
||||||
|
#include "engine/entity.h"
|
||||||
|
#include "engine/camera.h"
|
||||||
|
#include "engine/tilemap.h"
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* LASER TURRET — Hazard that charges up a visible
|
||||||
|
* indicator beam, then fires a sustained laser for
|
||||||
|
* several seconds. Two variants:
|
||||||
|
* - Fixed direction (placed facing left/right)
|
||||||
|
* - Tracking (slowly rotates toward the player
|
||||||
|
* during idle/charge, locks on fire)
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
#define LASER_WIDTH 14
|
||||||
|
#define LASER_HEIGHT 14
|
||||||
|
#define LASER_CHARGE_TIME 1.5f /* seconds to charge up */
|
||||||
|
#define LASER_FIRE_TIME 2.5f /* seconds beam stays active */
|
||||||
|
#define LASER_COOLDOWN_TIME 2.0f /* seconds before next cycle */
|
||||||
|
#define LASER_TRACK_SPEED 1.5f /* radians/s tracking rate */
|
||||||
|
#define LASER_DAMAGE 1
|
||||||
|
#define LASER_DAMAGE_CD 0.5f /* seconds between damage ticks */
|
||||||
|
#define LASER_HEALTH 3
|
||||||
|
|
||||||
|
typedef enum LaserState {
|
||||||
|
LASER_IDLE,
|
||||||
|
LASER_CHARGING,
|
||||||
|
LASER_FIRING,
|
||||||
|
LASER_COOLDOWN,
|
||||||
|
} LaserState;
|
||||||
|
|
||||||
|
typedef struct LaserTurretData {
|
||||||
|
LaserState state;
|
||||||
|
float timer; /* countdown for current state */
|
||||||
|
float aim_angle; /* current aim angle (radians) */
|
||||||
|
float damage_cd; /* cooldown between damage ticks */
|
||||||
|
bool tracking; /* true = slowly follows player */
|
||||||
|
float beam_end_x; /* computed beam endpoint (world) */
|
||||||
|
float beam_end_y;
|
||||||
|
} LaserTurretData;
|
||||||
|
|
||||||
|
void laser_turret_register(EntityManager *em);
|
||||||
|
Entity *laser_turret_spawn(EntityManager *em, Vec2 pos);
|
||||||
|
Entity *laser_turret_spawn_tracking(EntityManager *em, Vec2 pos);
|
||||||
|
|
||||||
|
#endif /* JNR_LASER_TURRET_H */
|
||||||
@@ -152,9 +152,12 @@ bool level_load_generated(Level *level, Tilemap *gen_map) {
|
|||||||
level->map = *gen_map;
|
level->map = *gen_map;
|
||||||
memset(gen_map, 0, sizeof(Tilemap)); /* prevent double-free */
|
memset(gen_map, 0, sizeof(Tilemap)); /* prevent double-free */
|
||||||
|
|
||||||
/* Load tileset texture (the generator doesn't do this) */
|
/* Load tileset texture. Use the generator's tileset_path if set,
|
||||||
snprintf(level->map.tileset_path, sizeof(level->map.tileset_path),
|
* otherwise fall back to the default tileset. */
|
||||||
"%s", "assets/tiles/tileset.png");
|
if (!level->map.tileset_path[0]) {
|
||||||
|
snprintf(level->map.tileset_path, sizeof(level->map.tileset_path),
|
||||||
|
"%s", "assets/tiles/tileset.png");
|
||||||
|
}
|
||||||
level->map.tileset = assets_get_texture(level->map.tileset_path);
|
level->map.tileset = assets_get_texture(level->map.tileset_path);
|
||||||
if (level->map.tileset) {
|
if (level->map.tileset) {
|
||||||
int tex_w;
|
int tex_w;
|
||||||
|
|||||||
@@ -538,6 +538,24 @@ static SegmentType pick_segment_type(LevelTheme theme, int index, int total) {
|
|||||||
if (r < 0.90f) return SEG_FLAT;
|
if (r < 0.90f) return SEG_FLAT;
|
||||||
return SEG_PIT;
|
return SEG_PIT;
|
||||||
|
|
||||||
|
case THEME_MARS_SURFACE:
|
||||||
|
/* Red dusty exterior: spacey, wide-open, few obstacles.
|
||||||
|
Charger enemies, occasional pits, wind gusts. */
|
||||||
|
if (r < 0.35f) return SEG_FLAT;
|
||||||
|
if (r < 0.55f) return SEG_PIT;
|
||||||
|
if (r < 0.75f) return SEG_ARENA;
|
||||||
|
if (r < 0.90f) return SEG_PLATFORMS;
|
||||||
|
return SEG_SHAFT;
|
||||||
|
|
||||||
|
case THEME_MARS_BASE:
|
||||||
|
/* Indoor Mars facility: very vertical, narrow corridors,
|
||||||
|
90-degree turns, heavy turret/spawner presence. */
|
||||||
|
if (r < 0.30f) return SEG_CORRIDOR;
|
||||||
|
if (r < 0.55f) return SEG_SHAFT;
|
||||||
|
if (r < 0.70f) return SEG_ARENA;
|
||||||
|
if (r < 0.85f) return SEG_PLATFORMS;
|
||||||
|
return SEG_FLAT;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
/* Only pick content types (exclude TRANSITION and CLIMB connectors) */
|
/* Only pick content types (exclude TRANSITION and CLIMB connectors) */
|
||||||
return (SegmentType)rng_range(0, SEG_SHAFT);
|
return (SegmentType)rng_range(0, SEG_SHAFT);
|
||||||
@@ -758,6 +776,8 @@ static float gravity_for_theme(LevelTheme theme) {
|
|||||||
case THEME_PLANET_SURFACE: return 400.0f;
|
case THEME_PLANET_SURFACE: return 400.0f;
|
||||||
case THEME_PLANET_BASE: return 600.0f;
|
case THEME_PLANET_BASE: return 600.0f;
|
||||||
case THEME_SPACE_STATION: return 750.0f;
|
case THEME_SPACE_STATION: return 750.0f;
|
||||||
|
case THEME_MARS_SURFACE: return 370.0f; /* Mars: ~0.38g */
|
||||||
|
case THEME_MARS_BASE: return 700.0f; /* artificial gravity */
|
||||||
default: return 600.0f;
|
default: return 600.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -768,6 +788,8 @@ static SDL_Color bg_color_for_theme(LevelTheme theme) {
|
|||||||
case THEME_PLANET_SURFACE: return (SDL_Color){12, 8, 20, 255};
|
case THEME_PLANET_SURFACE: return (SDL_Color){12, 8, 20, 255};
|
||||||
case THEME_PLANET_BASE: return (SDL_Color){10, 14, 22, 255};
|
case THEME_PLANET_BASE: return (SDL_Color){10, 14, 22, 255};
|
||||||
case THEME_SPACE_STATION: return (SDL_Color){5, 5, 18, 255};
|
case THEME_SPACE_STATION: return (SDL_Color){5, 5, 18, 255};
|
||||||
|
case THEME_MARS_SURFACE: return (SDL_Color){30, 12, 8, 255};
|
||||||
|
case THEME_MARS_BASE: return (SDL_Color){18, 10, 8, 255};
|
||||||
default: return (SDL_Color){15, 15, 30, 255};
|
default: return (SDL_Color){15, 15, 30, 255};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -778,6 +800,8 @@ static ParallaxStyle parallax_style_for_theme(LevelTheme theme) {
|
|||||||
case THEME_PLANET_SURFACE: return PARALLAX_STYLE_ALIEN_SKY;
|
case THEME_PLANET_SURFACE: return PARALLAX_STYLE_ALIEN_SKY;
|
||||||
case THEME_PLANET_BASE: return PARALLAX_STYLE_INTERIOR;
|
case THEME_PLANET_BASE: return PARALLAX_STYLE_INTERIOR;
|
||||||
case THEME_SPACE_STATION: return PARALLAX_STYLE_DEEP_SPACE;
|
case THEME_SPACE_STATION: return PARALLAX_STYLE_DEEP_SPACE;
|
||||||
|
case THEME_MARS_SURFACE: return PARALLAX_STYLE_MARS;
|
||||||
|
case THEME_MARS_BASE: return PARALLAX_STYLE_INTERIOR;
|
||||||
default: return PARALLAX_STYLE_DEFAULT;
|
default: return PARALLAX_STYLE_DEFAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -787,6 +811,8 @@ static const char *theme_label(LevelTheme t) {
|
|||||||
case THEME_PLANET_SURFACE: return "surface";
|
case THEME_PLANET_SURFACE: return "surface";
|
||||||
case THEME_PLANET_BASE: return "base";
|
case THEME_PLANET_BASE: return "base";
|
||||||
case THEME_SPACE_STATION: return "station";
|
case THEME_SPACE_STATION: return "station";
|
||||||
|
case THEME_MARS_SURFACE: return "mars_surf";
|
||||||
|
case THEME_MARS_BASE: return "mars_base";
|
||||||
default: return "?";
|
default: return "?";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,14 +917,18 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
* to give each theme its own vertical zone. Single-theme levels
|
* to give each theme its own vertical zone. Single-theme levels
|
||||||
* stay at the standard 23-tile height. */
|
* stay at the standard 23-tile height. */
|
||||||
bool has_surface = false, has_base = false, has_station = false;
|
bool has_surface = false, has_base = false, has_station = false;
|
||||||
|
bool has_mars_surf = false, has_mars_base = false;
|
||||||
for (int i = 0; i < num_segs; i++) {
|
for (int i = 0; i < num_segs; i++) {
|
||||||
if (seg_themes[i] == THEME_PLANET_SURFACE) has_surface = true;
|
if (seg_themes[i] == THEME_PLANET_SURFACE) has_surface = true;
|
||||||
if (seg_themes[i] == THEME_PLANET_BASE) has_base = true;
|
if (seg_themes[i] == THEME_PLANET_BASE) has_base = true;
|
||||||
if (seg_themes[i] == THEME_SPACE_STATION) has_station = true;
|
if (seg_themes[i] == THEME_SPACE_STATION) has_station = true;
|
||||||
|
if (seg_themes[i] == THEME_MARS_SURFACE) has_mars_surf = true;
|
||||||
|
if (seg_themes[i] == THEME_MARS_BASE) has_mars_base = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Go tall if we have theme variety that benefits from verticality */
|
/* Go tall if we have theme variety that benefits from verticality */
|
||||||
uses_tall = (has_surface && (has_base || has_station)) ||
|
uses_tall = (has_surface && (has_base || has_station)) ||
|
||||||
|
(has_mars_surf && has_mars_base) ||
|
||||||
(has_station && num_segs >= 5);
|
(has_station && num_segs >= 5);
|
||||||
|
|
||||||
if (uses_tall) {
|
if (uses_tall) {
|
||||||
@@ -914,9 +944,11 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
}
|
}
|
||||||
switch (seg_themes[i]) {
|
switch (seg_themes[i]) {
|
||||||
case THEME_PLANET_SURFACE:
|
case THEME_PLANET_SURFACE:
|
||||||
|
case THEME_MARS_SURFACE:
|
||||||
seg_ground[i] = ZONE_LOW_GROUND;
|
seg_ground[i] = ZONE_LOW_GROUND;
|
||||||
break;
|
break;
|
||||||
case THEME_PLANET_BASE:
|
case THEME_PLANET_BASE:
|
||||||
|
case THEME_MARS_BASE:
|
||||||
seg_ground[i] = ZONE_HIGH_GROUND;
|
seg_ground[i] = ZONE_HIGH_GROUND;
|
||||||
break;
|
break;
|
||||||
case THEME_SPACE_STATION:
|
case THEME_SPACE_STATION:
|
||||||
@@ -1092,6 +1124,13 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
map->has_bg_color = true;
|
map->has_bg_color = true;
|
||||||
map->parallax_style = (int)parallax_style_for_theme(primary_theme);
|
map->parallax_style = (int)parallax_style_for_theme(primary_theme);
|
||||||
|
|
||||||
|
/* Select tileset based on theme. Mars themes use a dedicated tileset;
|
||||||
|
* all others use the default. level_load_generated() loads the texture. */
|
||||||
|
if (primary_theme == THEME_MARS_SURFACE || primary_theme == THEME_MARS_BASE) {
|
||||||
|
snprintf(map->tileset_path, sizeof(map->tileset_path),
|
||||||
|
"%s", "assets/tiles/mars_tileset.png");
|
||||||
|
}
|
||||||
|
|
||||||
/* Exit zone at the far-right end of the level.
|
/* Exit zone at the far-right end of the level.
|
||||||
* Placed just inside the right border wall, 2 tiles wide, 3 tiles tall
|
* Placed just inside the right border wall, 2 tiles wide, 3 tiles tall
|
||||||
* sitting on the ground row. Target "generate" chains to another
|
* sitting on the ground row. Target "generate" chains to another
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ typedef enum LevelTheme {
|
|||||||
force fields, turrets, medium gravity */
|
force fields, turrets, medium gravity */
|
||||||
THEME_SPACE_STATION, /* orbital station: shafts, platforms, low
|
THEME_SPACE_STATION, /* orbital station: shafts, platforms, low
|
||||||
gravity, moving platforms, turrets */
|
gravity, moving platforms, turrets */
|
||||||
|
THEME_MARS_SURFACE, /* Mars exterior: red dusty terrain, wind,
|
||||||
|
wide-open with charger enemies */
|
||||||
|
THEME_MARS_BASE, /* Mars base interior: very vertical, narrow
|
||||||
|
corridors, turrets, laser turrets, spawners*/
|
||||||
THEME_COUNT
|
THEME_COUNT
|
||||||
} LevelTheme;
|
} LevelTheme;
|
||||||
|
|
||||||
|
|||||||
156
src/main.c
156
src/main.c
@@ -1,8 +1,10 @@
|
|||||||
#include "engine/core.h"
|
#include "engine/core.h"
|
||||||
#include "engine/input.h"
|
#include "engine/input.h"
|
||||||
|
#include "engine/font.h"
|
||||||
#include "game/level.h"
|
#include "game/level.h"
|
||||||
#include "game/levelgen.h"
|
#include "game/levelgen.h"
|
||||||
#include "game/editor.h"
|
#include "game/editor.h"
|
||||||
|
#include "config.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
typedef enum GameMode {
|
typedef enum GameMode {
|
||||||
MODE_PLAY,
|
MODE_PLAY,
|
||||||
MODE_EDITOR,
|
MODE_EDITOR,
|
||||||
|
MODE_PAUSED,
|
||||||
} GameMode;
|
} GameMode;
|
||||||
|
|
||||||
static Level s_level;
|
static Level s_level;
|
||||||
@@ -38,11 +41,17 @@ static bool s_testing_from_editor = false;
|
|||||||
* Drives escalating difficulty and length. */
|
* Drives escalating difficulty and length. */
|
||||||
static int s_station_depth = 0;
|
static int s_station_depth = 0;
|
||||||
|
|
||||||
|
/* ── Pause menu state ── */
|
||||||
|
#define PAUSE_ITEM_COUNT 3
|
||||||
|
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
||||||
|
|
||||||
static const char *theme_name(LevelTheme t) {
|
static const char *theme_name(LevelTheme t) {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case THEME_PLANET_SURFACE: return "Planet Surface";
|
case THEME_PLANET_SURFACE: return "Planet Surface";
|
||||||
case THEME_PLANET_BASE: return "Planet Base";
|
case THEME_PLANET_BASE: return "Planet Base";
|
||||||
case THEME_SPACE_STATION: return "Space Station";
|
case THEME_SPACE_STATION: return "Space Station";
|
||||||
|
case THEME_MARS_SURFACE: return "Mars Surface";
|
||||||
|
case THEME_MARS_BASE: return "Mars Base";
|
||||||
default: return "Unknown";
|
default: return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,11 +69,17 @@ static void load_generated_level(void) {
|
|||||||
config.num_segments = 6;
|
config.num_segments = 6;
|
||||||
config.difficulty = 0.5f;
|
config.difficulty = 0.5f;
|
||||||
|
|
||||||
|
/* Ensure seed is non-zero so theme selection is deterministic
|
||||||
|
* across restarts. If caller didn't set s_gen_seed (e.g. first
|
||||||
|
* run with -gen but no -seed), snapshot time(NULL) now. */
|
||||||
|
if (s_gen_seed == 0) s_gen_seed = (uint32_t)time(NULL);
|
||||||
|
config.seed = s_gen_seed;
|
||||||
|
|
||||||
/* Build a theme progression — start on surface, move indoors/upward.
|
/* Build a theme progression — start on surface, move indoors/upward.
|
||||||
* Derive from seed so it varies with each regeneration and doesn't
|
* Derive from seed so it varies with each regeneration and doesn't
|
||||||
* depend on rand() state (which parallax generators clobber). */
|
* depend on rand() state (which parallax generators clobber). */
|
||||||
uint32_t seed_for_theme = config.seed ? config.seed : (uint32_t)time(NULL);
|
uint32_t seed_for_theme = config.seed;
|
||||||
int r = (int)(seed_for_theme % 4);
|
int r = (int)(seed_for_theme % 6);
|
||||||
switch (r) {
|
switch (r) {
|
||||||
case 0: /* Surface -> Base */
|
case 0: /* Surface -> Base */
|
||||||
config.themes[0] = THEME_PLANET_SURFACE;
|
config.themes[0] = THEME_PLANET_SURFACE;
|
||||||
@@ -93,9 +108,27 @@ static void load_generated_level(void) {
|
|||||||
config.themes[5] = THEME_SPACE_STATION;
|
config.themes[5] = THEME_SPACE_STATION;
|
||||||
config.theme_count = 6;
|
config.theme_count = 6;
|
||||||
break;
|
break;
|
||||||
case 3: /* Single theme (derived from seed) */
|
case 3: /* Mars Surface -> Mars Base */
|
||||||
|
config.themes[0] = THEME_MARS_SURFACE;
|
||||||
|
config.themes[1] = THEME_MARS_SURFACE;
|
||||||
|
config.themes[2] = THEME_MARS_SURFACE;
|
||||||
|
config.themes[3] = THEME_MARS_BASE;
|
||||||
|
config.themes[4] = THEME_MARS_BASE;
|
||||||
|
config.themes[5] = THEME_MARS_BASE;
|
||||||
|
config.theme_count = 6;
|
||||||
|
break;
|
||||||
|
case 4: /* Mars Surface -> Mars Base -> Station */
|
||||||
|
config.themes[0] = THEME_MARS_SURFACE;
|
||||||
|
config.themes[1] = THEME_MARS_SURFACE;
|
||||||
|
config.themes[2] = THEME_MARS_BASE;
|
||||||
|
config.themes[3] = THEME_MARS_BASE;
|
||||||
|
config.themes[4] = THEME_SPACE_STATION;
|
||||||
|
config.themes[5] = THEME_SPACE_STATION;
|
||||||
|
config.theme_count = 6;
|
||||||
|
break;
|
||||||
|
case 5: /* Single theme (derived from seed) */
|
||||||
default: {
|
default: {
|
||||||
LevelTheme single = (LevelTheme)(seed_for_theme / 4 % THEME_COUNT);
|
LevelTheme single = (LevelTheme)(seed_for_theme / 6 % THEME_COUNT);
|
||||||
config.themes[0] = single;
|
config.themes[0] = single;
|
||||||
config.theme_count = 1;
|
config.theme_count = 1;
|
||||||
break;
|
break;
|
||||||
@@ -200,6 +233,20 @@ static void return_to_editor(void) {
|
|||||||
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
|
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Restart current level (file-based or generated) ── */
|
||||||
|
static void restart_level(void) {
|
||||||
|
level_free(&s_level);
|
||||||
|
if (s_level_path[0]) {
|
||||||
|
if (!load_level_file(s_level_path)) {
|
||||||
|
fprintf(stderr, "Failed to restart level: %s\n", s_level_path);
|
||||||
|
g_engine.running = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Generated level — regenerate with same seed. */
|
||||||
|
load_generated_level();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
* Game callbacks
|
* Game callbacks
|
||||||
* ═══════════════════════════════════════════════════ */
|
* ═══════════════════════════════════════════════════ */
|
||||||
@@ -217,6 +264,48 @@ static void game_init(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Pause menu: handle input and confirm actions ── */
|
||||||
|
static void pause_update(void) {
|
||||||
|
/* Unpause on escape */
|
||||||
|
if (input_pressed(ACTION_PAUSE)) {
|
||||||
|
s_mode = MODE_PLAY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigate menu items */
|
||||||
|
if (input_pressed(ACTION_UP)) {
|
||||||
|
s_pause_selection--;
|
||||||
|
if (s_pause_selection < 0) s_pause_selection = PAUSE_ITEM_COUNT - 1;
|
||||||
|
}
|
||||||
|
if (input_pressed(ACTION_DOWN)) {
|
||||||
|
s_pause_selection++;
|
||||||
|
if (s_pause_selection >= PAUSE_ITEM_COUNT) s_pause_selection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirm selection with jump or enter */
|
||||||
|
bool confirm = input_pressed(ACTION_JUMP)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN2);
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
switch (s_pause_selection) {
|
||||||
|
case 0: /* Resume */
|
||||||
|
s_mode = MODE_PLAY;
|
||||||
|
break;
|
||||||
|
case 1: /* Restart */
|
||||||
|
s_mode = MODE_PLAY;
|
||||||
|
restart_level();
|
||||||
|
break;
|
||||||
|
case 2: /* Quit */
|
||||||
|
if (s_testing_from_editor) {
|
||||||
|
return_to_editor();
|
||||||
|
} else {
|
||||||
|
g_engine.running = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void game_update(float dt) {
|
static void game_update(float dt) {
|
||||||
if (s_mode == MODE_EDITOR) {
|
if (s_mode == MODE_EDITOR) {
|
||||||
editor_update(&s_editor, dt);
|
editor_update(&s_editor, dt);
|
||||||
@@ -230,15 +319,21 @@ static void game_update(float dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_mode == MODE_PAUSED) {
|
||||||
|
pause_update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Play mode ── */
|
/* ── Play mode ── */
|
||||||
|
|
||||||
/* Quit / return to editor on escape */
|
/* Pause on escape (return to editor during test play) */
|
||||||
if (input_pressed(ACTION_PAUSE)) {
|
if (input_pressed(ACTION_PAUSE)) {
|
||||||
if (s_testing_from_editor) {
|
if (s_testing_from_editor) {
|
||||||
return_to_editor();
|
return_to_editor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_engine.running = false;
|
s_pause_selection = 0;
|
||||||
|
s_mode = MODE_PAUSED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,9 +401,54 @@ static void game_update(float dt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Draw the pause menu overlay on top of the frozen game frame ── */
|
||||||
|
static void pause_render(void) {
|
||||||
|
SDL_Renderer *r = g_engine.renderer;
|
||||||
|
|
||||||
|
/* Semi-transparent dark overlay */
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(r, 0, 0, 0, 140);
|
||||||
|
SDL_Rect overlay = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
|
||||||
|
SDL_RenderFillRect(r, &overlay);
|
||||||
|
|
||||||
|
/* Title */
|
||||||
|
SDL_Color col_title = {255, 255, 255, 255};
|
||||||
|
font_draw_text_centered(r, "PAUSED", SCREEN_HEIGHT / 2 - 40,
|
||||||
|
SCREEN_WIDTH, col_title);
|
||||||
|
|
||||||
|
/* Menu items */
|
||||||
|
static const char *items[PAUSE_ITEM_COUNT] = {
|
||||||
|
"RESUME", "RESTART", "QUIT"
|
||||||
|
};
|
||||||
|
SDL_Color col_normal = {160, 160, 170, 255};
|
||||||
|
SDL_Color col_active = {255, 220, 80, 255};
|
||||||
|
|
||||||
|
int item_y = SCREEN_HEIGHT / 2 - 8;
|
||||||
|
for (int i = 0; i < PAUSE_ITEM_COUNT; i++) {
|
||||||
|
SDL_Color c = (i == s_pause_selection) ? col_active : col_normal;
|
||||||
|
|
||||||
|
/* Draw selection indicator */
|
||||||
|
if (i == s_pause_selection) {
|
||||||
|
int tw = font_text_width(items[i]);
|
||||||
|
int tx = (SCREEN_WIDTH - tw) / 2;
|
||||||
|
font_draw_text(r, ">", tx - 8, item_y, col_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
font_draw_text_centered(r, items[i], item_y, SCREEN_WIDTH, c);
|
||||||
|
item_y += 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restore blend mode */
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
static void game_render(float interpolation) {
|
static void game_render(float interpolation) {
|
||||||
if (s_mode == MODE_EDITOR) {
|
if (s_mode == MODE_EDITOR) {
|
||||||
editor_render(&s_editor, interpolation);
|
editor_render(&s_editor, interpolation);
|
||||||
|
} else if (s_mode == MODE_PAUSED) {
|
||||||
|
/* Render frozen game frame, then overlay the pause menu. */
|
||||||
|
level_render(&s_level, interpolation);
|
||||||
|
pause_render();
|
||||||
} else {
|
} else {
|
||||||
level_render(&s_level, interpolation);
|
level_render(&s_level, interpolation);
|
||||||
}
|
}
|
||||||
@@ -318,7 +458,7 @@ static void game_shutdown(void) {
|
|||||||
/* Always free both — editor may have been initialized even if we're
|
/* Always free both — editor may have been initialized even if we're
|
||||||
* currently in play mode (e.g. shutdown during test play). editor_free
|
* currently in play mode (e.g. shutdown during test play). editor_free
|
||||||
* and level_free are safe to call on zeroed/already-freed structs. */
|
* and level_free are safe to call on zeroed/already-freed structs. */
|
||||||
if (s_mode == MODE_PLAY || s_testing_from_editor) {
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED || s_testing_from_editor) {
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
}
|
}
|
||||||
if (s_mode == MODE_EDITOR || s_use_editor) {
|
if (s_mode == MODE_EDITOR || s_use_editor) {
|
||||||
@@ -356,7 +496,7 @@ int main(int argc, char *argv[]) {
|
|||||||
printf("\nIn-game:\n");
|
printf("\nIn-game:\n");
|
||||||
printf(" R Regenerate level with new random seed\n");
|
printf(" R Regenerate level with new random seed\n");
|
||||||
printf(" E Open level editor\n");
|
printf(" E Open level editor\n");
|
||||||
printf(" ESC Quit (or return to editor from test play)\n");
|
printf(" ESC Pause (or return to editor from test play)\n");
|
||||||
printf("\nEditor:\n");
|
printf("\nEditor:\n");
|
||||||
printf(" 1-6 Select tool (Pencil/Eraser/Fill/Entity/Spawn/Exit)\n");
|
printf(" 1-6 Select tool (Pencil/Eraser/Fill/Entity/Spawn/Exit)\n");
|
||||||
printf(" Q/W/E Select layer (Collision/BG/FG)\n");
|
printf(" Q/W/E Select layer (Collision/BG/FG)\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user