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:
@@ -28,6 +28,9 @@ typedef enum EntityType {
|
||||
ENT_DRONE,
|
||||
ENT_ASTEROID,
|
||||
ENT_SPACECRAFT,
|
||||
ENT_LASER_TURRET,
|
||||
ENT_ENEMY_CHARGER,
|
||||
ENT_SPAWNER,
|
||||
ENT_TYPE_COUNT
|
||||
} 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;
|
||||
}
|
||||
|
||||
/* ── 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 ─────────────────────── */
|
||||
|
||||
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_near(p, renderer);
|
||||
break;
|
||||
case PARALLAX_STYLE_MARS:
|
||||
generate_mars_far(p, renderer);
|
||||
generate_mars_near(p, renderer);
|
||||
break;
|
||||
case PARALLAX_STYLE_DEFAULT:
|
||||
default:
|
||||
parallax_generate_stars(p, renderer);
|
||||
|
||||
@@ -55,6 +55,7 @@ typedef enum ParallaxStyle {
|
||||
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
|
||||
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
|
||||
PARALLAX_STYLE_MOON, /* moon surface: craters, grey terrain */
|
||||
PARALLAX_STYLE_MARS, /* Mars: salmon sky, red mesas, dust */
|
||||
} ParallaxStyle;
|
||||
|
||||
/* Generate both layers with a unified style */
|
||||
|
||||
Reference in New Issue
Block a user