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:
Thomas
2026-03-02 19:34:12 +00:00
parent e5e91247fe
commit d0853fb38d
22 changed files with 1519 additions and 147 deletions

View File

@@ -5,6 +5,7 @@
#include "engine/renderer.h"
#include "engine/assets.h"
#include "engine/camera.h"
#include "engine/font.h"
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
@@ -104,124 +105,7 @@ void editor_load_vfs_file(const char *path) {
#endif /* __EMSCRIPTEN__ */
/* ═══════════════════════════════════════════════════
* 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
/* Bitmap font provided by engine/font.h (included above). */
/* ═══════════════════════════════════════════════════
* 8x8 pixel mini-icons for the entity palette
@@ -263,6 +147,14 @@ static const uint64_t s_icon_bitmaps[ICON_COUNT] = {
0x001C3E7F7F3E1C00ULL,
/* ICON_SPACECRAFT: ship */
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,
@@ -1429,7 +1321,7 @@ void editor_render(Editor *ed, float interpolation) {
draw_icon(r, (EditorIcon)reg->icon,
(int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
} 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_Rect sr = {(int)sp.x, (int)sp.y, (int)(zs + 0.5f), (int)(zs + 0.5f)};
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);
}
@@ -1458,9 +1350,9 @@ void editor_render(Editor *ed, float interpolation) {
SDL_RenderFillRect(r, &er);
SDL_SetRenderDrawColor(r, COL_EXIT.r, COL_EXIT.g, COL_EXIT.b, 220);
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]) {
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);
}
@@ -1514,7 +1406,7 @@ void editor_render(Editor *ed, float interpolation) {
for (int i = 0; i < TOOL_COUNT; i++) {
int bx = i * 35 + 2;
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 */
@@ -1527,17 +1419,17 @@ void editor_render(Editor *ed, float interpolation) {
for (int i = 0; i < EDITOR_LAYER_COUNT; i++) {
int bx = layer_start + i * 25;
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 */
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);
/* Tileset switch hint */
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 ── */
@@ -1565,7 +1457,7 @@ void editor_render(Editor *ed, float interpolation) {
{
const char *ts_name = strrchr(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);
}
@@ -1645,10 +1537,10 @@ void editor_render(Editor *ed, float interpolation) {
SDL_Color fc = (flags & TILE_HAZARD) ? (SDL_Color){255, 80, 40, 255} :
(flags & TILE_PLATFORM) ? (SDL_Color){80, 200, 255, 255} :
(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 */
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) ── */
{
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 ent_area_h = py + ph - pal_y_start;
@@ -1685,7 +1577,7 @@ void editor_render(Editor *ed, float interpolation) {
/* Name */
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);
@@ -1712,6 +1604,6 @@ void editor_render(Editor *ed, float interpolation) {
ed->camera.zoom * 100.0f,
ed->has_file ? ed->file_path : "new level",
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);
}
}