forked from tas/major_tom
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.
1168 lines
43 KiB
C
1168 lines
43 KiB
C
#include "engine/parallax.h"
|
|
#include "engine/assets.h"
|
|
#include "engine/camera.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
/* ── Helpers ─────────────────────────────────────────── */
|
|
|
|
static float randf(void) {
|
|
return (float)rand() / (float)RAND_MAX;
|
|
}
|
|
|
|
static uint8_t clamp_u8(int v) {
|
|
if (v < 0) return 0;
|
|
if (v > 255) return 255;
|
|
return (uint8_t)v;
|
|
}
|
|
|
|
/* ── Init / Free ─────────────────────────────────────── */
|
|
|
|
void parallax_init(Parallax *p) {
|
|
memset(p, 0, sizeof(Parallax));
|
|
}
|
|
|
|
void parallax_set_far(Parallax *p, SDL_Texture *tex, float scroll_x, float scroll_y) {
|
|
p->far_layer.texture = tex;
|
|
p->far_layer.scroll_x = scroll_x;
|
|
p->far_layer.scroll_y = scroll_y;
|
|
p->far_layer.active = true;
|
|
p->far_layer.owns_texture = false; /* asset manager owns it */
|
|
SDL_QueryTexture(tex, NULL, NULL, &p->far_layer.tex_w, &p->far_layer.tex_h);
|
|
}
|
|
|
|
void parallax_set_near(Parallax *p, SDL_Texture *tex, float scroll_x, float scroll_y) {
|
|
p->near_layer.texture = tex;
|
|
p->near_layer.scroll_x = scroll_x;
|
|
p->near_layer.scroll_y = scroll_y;
|
|
p->near_layer.active = true;
|
|
p->near_layer.owns_texture = false; /* asset manager owns it */
|
|
SDL_QueryTexture(tex, NULL, NULL, &p->near_layer.tex_w, &p->near_layer.tex_h);
|
|
}
|
|
|
|
/* ── Procedural star generation ──────────────────────── */
|
|
|
|
void parallax_generate_stars(Parallax *p, SDL_Renderer *renderer) {
|
|
/* Create a 640x360 texture (screen-sized, tiles seamlessly) */
|
|
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);
|
|
|
|
/* Clear to transparent (bg_color will show through) */
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
|
SDL_RenderClear(renderer);
|
|
|
|
/* Seed for reproducible star patterns */
|
|
unsigned int saved_seed = (unsigned int)rand();
|
|
srand(42);
|
|
|
|
/* Small dim stars (distant) — lots of them */
|
|
for (int i = 0; i < 120; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
uint8_t brightness = (uint8_t)(100 + (int)(randf() * 80));
|
|
/* Slight color tints: bluish, yellowish, or white */
|
|
uint8_t r = brightness, g = brightness, b = brightness;
|
|
float tint = randf();
|
|
if (tint < 0.2f) {
|
|
b = clamp_u8(brightness + 40); /* blue tint */
|
|
} else if (tint < 0.35f) {
|
|
r = clamp_u8(brightness + 30);
|
|
g = clamp_u8(brightness + 20); /* warm tint */
|
|
}
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, (uint8_t)(150 + (int)(randf() * 105)));
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
/* Medium stars */
|
|
for (int i = 0; i < 30; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
uint8_t brightness = (uint8_t)(180 + (int)(randf() * 75));
|
|
uint8_t r = brightness, g = brightness, b = brightness;
|
|
float tint = randf();
|
|
if (tint < 0.25f) {
|
|
b = 255; r = clamp_u8(brightness - 20); /* blue star */
|
|
} else if (tint < 0.4f) {
|
|
r = 255; g = clamp_u8(brightness - 10); /* warm star */
|
|
}
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 255);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
/* Cross-halo for slightly brighter stars */
|
|
if (randf() < 0.4f) {
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 80);
|
|
SDL_Rect h1 = {x - 1, y, 3, 1};
|
|
SDL_Rect h2 = {x, y - 1, 1, 3};
|
|
SDL_RenderFillRect(renderer, &h1);
|
|
SDL_RenderFillRect(renderer, &h2);
|
|
}
|
|
}
|
|
|
|
/* Bright feature stars (few, prominent) */
|
|
for (int i = 0; i < 6; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
/* Core pixel */
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_Rect core = {x, y, 2, 2};
|
|
SDL_RenderFillRect(renderer, &core);
|
|
/* Glow cross */
|
|
uint8_t glow_a = (uint8_t)(120 + (int)(randf() * 80));
|
|
float tint = randf();
|
|
uint8_t gr = 200, gg = 210, gb = 255; /* default blue-white */
|
|
if (tint < 0.3f) { gr = 255; gg = 220; gb = 180; } /* warm */
|
|
SDL_SetRenderDrawColor(renderer, gr, gg, gb, glow_a);
|
|
SDL_Rect cross_h = {x - 1, y, 4, 2};
|
|
SDL_Rect cross_v = {x, y - 1, 2, 4};
|
|
SDL_RenderFillRect(renderer, &cross_h);
|
|
SDL_RenderFillRect(renderer, &cross_v);
|
|
}
|
|
|
|
srand(saved_seed); /* restore randomness */
|
|
|
|
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.05f;
|
|
p->far_layer.scroll_y = 0.05f;
|
|
p->far_layer.active = true;
|
|
p->far_layer.owns_texture = true;
|
|
}
|
|
|
|
/* ── Procedural nebula generation ────────────────────── */
|
|
|
|
void parallax_generate_nebula(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);
|
|
|
|
/* Clear to transparent */
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
|
SDL_RenderClear(renderer);
|
|
|
|
unsigned int saved_seed = (unsigned int)rand();
|
|
srand(137);
|
|
|
|
/* Paint soft nebula blobs — clusters of semi-transparent rects
|
|
* that overlap to create soft, cloudy shapes */
|
|
|
|
/* Nebula color palette (space purples, blues, teals) */
|
|
typedef struct { uint8_t r, g, b; } NebColor;
|
|
NebColor palette[] = {
|
|
{ 60, 20, 100}, /* deep purple */
|
|
{ 30, 40, 120}, /* dark blue */
|
|
{ 20, 60, 90}, /* teal */
|
|
{ 80, 15, 60}, /* magenta */
|
|
{ 40, 50, 100}, /* slate blue */
|
|
};
|
|
int palette_count = sizeof(palette) / sizeof(palette[0]);
|
|
|
|
/* Paint 4-5 nebula clouds */
|
|
for (int cloud = 0; cloud < 5; cloud++) {
|
|
float cx = randf() * w;
|
|
float cy = randf() * h;
|
|
NebColor col = palette[cloud % palette_count];
|
|
|
|
/* Each cloud is ~30-50 overlapping soft rects */
|
|
int blobs = 30 + (int)(randf() * 20);
|
|
for (int b = 0; b < blobs; b++) {
|
|
float angle = randf() * (float)(2.0 * M_PI);
|
|
float dist = randf() * 80.0f + randf() * 40.0f;
|
|
int bx = (int)(cx + cosf(angle) * dist);
|
|
int by = (int)(cy + sinf(angle) * dist);
|
|
int bw = 8 + (int)(randf() * 20);
|
|
int bh = 8 + (int)(randf() * 16);
|
|
|
|
/* Vary color slightly per blob */
|
|
uint8_t br = clamp_u8(col.r + (int)(randf() * 30 - 15));
|
|
uint8_t bg = clamp_u8(col.g + (int)(randf() * 30 - 15));
|
|
uint8_t bb = clamp_u8(col.b + (int)(randf() * 30 - 15));
|
|
uint8_t ba = (uint8_t)(8 + (int)(randf() * 18)); /* very subtle */
|
|
|
|
SDL_SetRenderDrawColor(renderer, br, bg, bb, ba);
|
|
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
}
|
|
}
|
|
|
|
/* Scattered dust particles (tiny dim dots between clouds) */
|
|
for (int i = 0; i < 60; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
NebColor col = palette[(int)(randf() * palette_count)];
|
|
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
|
(uint8_t)(30 + (int)(randf() * 40)));
|
|
SDL_Rect dot = {x, y, 2, 2};
|
|
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.15f;
|
|
p->near_layer.scroll_y = 0.10f;
|
|
p->near_layer.active = true;
|
|
p->near_layer.owns_texture = true;
|
|
}
|
|
|
|
/* ── Themed: Alien Sky (planet surface) ──────────────── */
|
|
|
|
static void generate_alien_sky_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(55);
|
|
|
|
/* Fewer stars than deep space — atmosphere scatters light */
|
|
for (int i = 0; i < 60; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h * 0.6f); /* stars only in upper portion */
|
|
uint8_t brightness = (uint8_t)(60 + (int)(randf() * 80));
|
|
/* Warm-tinted: alien atmosphere filters starlight */
|
|
uint8_t r = clamp_u8(brightness + 20);
|
|
uint8_t g = clamp_u8(brightness - 10);
|
|
uint8_t b = clamp_u8(brightness - 30);
|
|
uint8_t a = (uint8_t)(100 + (int)(randf() * 80));
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
/* A few brighter stars peeking through */
|
|
for (int i = 0; i < 8; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h * 0.4f);
|
|
SDL_SetRenderDrawColor(renderer, 255, 220, 180, 200);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
/* Distant mountain/terrain silhouette at bottom */
|
|
int terrain_base = (int)(h * 0.70f);
|
|
for (int x = 0; x < w; x++) {
|
|
/* Jagged terrain profile using multiple sine waves */
|
|
float t = (float)x / (float)w;
|
|
float h1 = sinf(t * 6.28f * 2.0f) * 15.0f;
|
|
float h2 = sinf(t * 6.28f * 5.3f + 1.2f) * 8.0f;
|
|
float h3 = sinf(t * 6.28f * 11.7f + 3.1f) * 4.0f;
|
|
int peak = terrain_base - (int)(h1 + h2 + h3);
|
|
if (peak < terrain_base - 30) peak = terrain_base - 30;
|
|
|
|
/* Dark terrain silhouette */
|
|
for (int y = peak; y < h; y++) {
|
|
/* Gradient: darker at bottom, slightly lighter at peaks */
|
|
int depth = y - peak;
|
|
uint8_t r = clamp_u8(8 - depth / 8);
|
|
uint8_t g = clamp_u8(5 - depth / 10);
|
|
uint8_t b = clamp_u8(12 - depth / 6);
|
|
uint8_t a = (uint8_t)(depth < 3 ? 120 : 180);
|
|
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; /* very slow — distant landscape */
|
|
p->far_layer.scroll_y = 0.03f;
|
|
p->far_layer.active = true;
|
|
p->far_layer.owns_texture = true;
|
|
}
|
|
|
|
static void generate_alien_sky_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(188);
|
|
|
|
/* Atmospheric haze / dust clouds — warm oranges and reds */
|
|
typedef struct { uint8_t r, g, b; } HazeColor;
|
|
HazeColor haze_palette[] = {
|
|
{ 80, 30, 15}, /* rusty orange */
|
|
{ 60, 20, 25}, /* dark red */
|
|
{ 50, 35, 20}, /* brown dust */
|
|
{ 70, 25, 40}, /* murky purple */
|
|
{ 45, 40, 30}, /* sandy */
|
|
};
|
|
int haze_count = sizeof(haze_palette) / sizeof(haze_palette[0]);
|
|
|
|
/* Haze bands across the sky */
|
|
for (int band = 0; band < 4; band++) {
|
|
float cy = randf() * h * 0.7f + h * 0.15f;
|
|
HazeColor col = haze_palette[band % haze_count];
|
|
int blobs = 20 + (int)(randf() * 15);
|
|
for (int b = 0; b < blobs; b++) {
|
|
float angle = randf() * (float)(2.0 * M_PI);
|
|
float dist = randf() * 100.0f;
|
|
int bx = (int)(randf() * w);
|
|
int by = (int)(cy + sinf(angle) * dist * 0.3f);
|
|
int bw = 15 + (int)(randf() * 30);
|
|
int bh = 5 + (int)(randf() * 10);
|
|
uint8_t br = clamp_u8(col.r + (int)(randf() * 20 - 10));
|
|
uint8_t bg = clamp_u8(col.g + (int)(randf() * 20 - 10));
|
|
uint8_t bb = clamp_u8(col.b + (int)(randf() * 20 - 10));
|
|
SDL_SetRenderDrawColor(renderer, br, bg, bb, (uint8_t)(6 + (int)(randf() * 12)));
|
|
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
}
|
|
}
|
|
|
|
/* Floating dust particles */
|
|
for (int i = 0; i < 40; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
HazeColor col = haze_palette[(int)(randf() * haze_count)];
|
|
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
|
(uint8_t)(25 + (int)(randf() * 35)));
|
|
SDL_Rect dot = {x, y, 1, 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: Interior (planet base) ─────────────────── */
|
|
|
|
static void generate_interior_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(73);
|
|
|
|
/* Wall panel grid — subtle horizontal and vertical lines */
|
|
/* Horizontal beams */
|
|
for (int i = 0; i < 6; i++) {
|
|
int y = (int)(randf() * h);
|
|
uint8_t a = (uint8_t)(15 + (int)(randf() * 20));
|
|
SDL_SetRenderDrawColor(renderer, 25, 30, 40, a);
|
|
SDL_Rect beam = {0, y, w, 2};
|
|
SDL_RenderFillRect(renderer, &beam);
|
|
}
|
|
|
|
/* Vertical structural columns */
|
|
for (int i = 0; i < 8; i++) {
|
|
int x = (int)(randf() * w);
|
|
int col_w = 2 + (int)(randf() * 3);
|
|
uint8_t a = (uint8_t)(12 + (int)(randf() * 18));
|
|
SDL_SetRenderDrawColor(renderer, 20, 25, 35, a);
|
|
SDL_Rect col_rect = {x, 0, col_w, h};
|
|
SDL_RenderFillRect(renderer, &col_rect);
|
|
}
|
|
|
|
/* Recessed wall panels (darker rectangles) */
|
|
for (int i = 0; i < 12; i++) {
|
|
int px = (int)(randf() * w);
|
|
int py = (int)(randf() * h);
|
|
int pw = 20 + (int)(randf() * 40);
|
|
int ph = 15 + (int)(randf() * 30);
|
|
SDL_SetRenderDrawColor(renderer, 8, 10, 18, (uint8_t)(20 + (int)(randf() * 15)));
|
|
SDL_Rect panel = {px, py, pw, ph};
|
|
SDL_RenderFillRect(renderer, &panel);
|
|
/* Panel border highlight (top/left edge) */
|
|
SDL_SetRenderDrawColor(renderer, 30, 35, 50, 20);
|
|
SDL_Rect edge_h = {px, py, pw, 1};
|
|
SDL_Rect edge_v = {px, py, 1, ph};
|
|
SDL_RenderFillRect(renderer, &edge_h);
|
|
SDL_RenderFillRect(renderer, &edge_v);
|
|
}
|
|
|
|
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.02f; /* very slow — background wall */
|
|
p->far_layer.scroll_y = 0.02f;
|
|
p->far_layer.active = true;
|
|
p->far_layer.owns_texture = true;
|
|
}
|
|
|
|
static void generate_interior_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(209);
|
|
|
|
/* Pipes running along ceiling/floor areas */
|
|
for (int i = 0; i < 5; i++) {
|
|
int py = (int)(randf() * h);
|
|
int pipe_h = 2 + (int)(randf() * 2);
|
|
/* Pipe body */
|
|
uint8_t r = (uint8_t)(20 + (int)(randf() * 15));
|
|
uint8_t g = (uint8_t)(25 + (int)(randf() * 15));
|
|
uint8_t b = (uint8_t)(35 + (int)(randf() * 15));
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 25);
|
|
SDL_Rect pipe = {0, py, w, pipe_h};
|
|
SDL_RenderFillRect(renderer, &pipe);
|
|
/* Pipe highlight (top edge) */
|
|
SDL_SetRenderDrawColor(renderer, r + 15, g + 15, b + 15, 18);
|
|
SDL_Rect highlight = {0, py, w, 1};
|
|
SDL_RenderFillRect(renderer, &highlight);
|
|
}
|
|
|
|
/* Small indicator lights / LEDs scattered on walls */
|
|
for (int i = 0; i < 15; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
float r_chance = randf();
|
|
if (r_chance < 0.4f) {
|
|
/* Green status light */
|
|
SDL_SetRenderDrawColor(renderer, 30, 180, 60, 60);
|
|
} else if (r_chance < 0.65f) {
|
|
/* Amber warning */
|
|
SDL_SetRenderDrawColor(renderer, 200, 150, 30, 50);
|
|
} else if (r_chance < 0.80f) {
|
|
/* Red alert */
|
|
SDL_SetRenderDrawColor(renderer, 200, 40, 30, 45);
|
|
} else {
|
|
/* Blue data */
|
|
SDL_SetRenderDrawColor(renderer, 40, 100, 200, 50);
|
|
}
|
|
SDL_Rect led = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &led);
|
|
/* Tiny glow around the LED */
|
|
SDL_SetRenderDrawColor(renderer, 40, 60, 80, 8);
|
|
SDL_Rect glow = {x - 1, y - 1, 3, 3};
|
|
SDL_RenderFillRect(renderer, &glow);
|
|
}
|
|
|
|
/* Ventilation grate patterns */
|
|
for (int i = 0; i < 4; i++) {
|
|
int gx = (int)(randf() * w);
|
|
int gy = (int)(randf() * h);
|
|
int gw = 8 + (int)(randf() * 12);
|
|
int gh = 4 + (int)(randf() * 6);
|
|
/* Grate slots (horizontal lines within a rectangle) */
|
|
for (int s = 0; s < gh; s += 2) {
|
|
SDL_SetRenderDrawColor(renderer, 15, 18, 28, 25);
|
|
SDL_Rect slot = {gx, gy + s, gw, 1};
|
|
SDL_RenderFillRect(renderer, &slot);
|
|
}
|
|
}
|
|
|
|
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.08f;
|
|
p->near_layer.scroll_y = 0.05f;
|
|
p->near_layer.active = true;
|
|
p->near_layer.owns_texture = true;
|
|
}
|
|
|
|
/* ── Themed: Deep Space (space station viewports) ───── */
|
|
|
|
static void generate_deep_space_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(99);
|
|
|
|
/* Dense starfield — station viewports show deep space clearly */
|
|
/* Many small stars */
|
|
for (int i = 0; i < 180; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
uint8_t brightness = (uint8_t)(120 + (int)(randf() * 100));
|
|
uint8_t r = brightness, g = brightness, b = brightness;
|
|
float tint = randf();
|
|
if (tint < 0.3f) {
|
|
b = clamp_u8(brightness + 50);
|
|
g = clamp_u8(brightness + 10); /* cool blue-white */
|
|
} else if (tint < 0.45f) {
|
|
r = clamp_u8(brightness + 30); /* warm */
|
|
}
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, (uint8_t)(180 + (int)(randf() * 75)));
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
/* Medium stars with halos — more prominent than default */
|
|
for (int i = 0; i < 40; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
uint8_t brightness = (uint8_t)(200 + (int)(randf() * 55));
|
|
uint8_t r = brightness, g = brightness, b = 255;
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 255);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
if (randf() < 0.5f) {
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, 90);
|
|
SDL_Rect hx = {x - 1, y, 3, 1};
|
|
SDL_Rect hy = {x, y - 1, 1, 3};
|
|
SDL_RenderFillRect(renderer, &hx);
|
|
SDL_RenderFillRect(renderer, &hy);
|
|
}
|
|
}
|
|
|
|
/* Bright feature stars */
|
|
for (int i = 0; i < 10; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_Rect core = {x, y, 2, 2};
|
|
SDL_RenderFillRect(renderer, &core);
|
|
/* Cyan-white glow */
|
|
SDL_SetRenderDrawColor(renderer, 150, 220, 255, (uint8_t)(100 + (int)(randf() * 80)));
|
|
SDL_Rect ch = {x - 1, y, 4, 2};
|
|
SDL_Rect cv = {x, y - 1, 2, 4};
|
|
SDL_RenderFillRect(renderer, &ch);
|
|
SDL_RenderFillRect(renderer, &cv);
|
|
}
|
|
|
|
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.05f;
|
|
p->far_layer.scroll_y = 0.05f;
|
|
p->far_layer.active = true;
|
|
p->far_layer.owns_texture = true;
|
|
}
|
|
|
|
static void generate_deep_space_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(251);
|
|
|
|
/* Vivid nebula clouds — cyan, electric blue, violet */
|
|
typedef struct { uint8_t r, g, b; } SpaceColor;
|
|
SpaceColor palette[] = {
|
|
{ 20, 60, 140}, /* electric blue */
|
|
{ 30, 90, 130}, /* cyan-blue */
|
|
{ 50, 30, 120}, /* deep violet */
|
|
{ 15, 80, 100}, /* teal */
|
|
{ 70, 20, 100}, /* purple */
|
|
};
|
|
int palette_count = sizeof(palette) / sizeof(palette[0]);
|
|
|
|
for (int cloud = 0; cloud < 6; cloud++) {
|
|
float cx = randf() * w;
|
|
float cy = randf() * h;
|
|
SpaceColor col = palette[cloud % palette_count];
|
|
int blobs = 35 + (int)(randf() * 25);
|
|
for (int b = 0; b < blobs; b++) {
|
|
float angle = randf() * (float)(2.0 * M_PI);
|
|
float dist = randf() * 90.0f + randf() * 50.0f;
|
|
int bx = (int)(cx + cosf(angle) * dist);
|
|
int by = (int)(cy + sinf(angle) * dist);
|
|
int bw = 10 + (int)(randf() * 24);
|
|
int bh = 8 + (int)(randf() * 18);
|
|
uint8_t br = clamp_u8(col.r + (int)(randf() * 30 - 15));
|
|
uint8_t bg = clamp_u8(col.g + (int)(randf() * 30 - 15));
|
|
uint8_t bb = clamp_u8(col.b + (int)(randf() * 30 - 15));
|
|
SDL_SetRenderDrawColor(renderer, br, bg, bb, (uint8_t)(10 + (int)(randf() * 20)));
|
|
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
}
|
|
}
|
|
|
|
/* Scattered bright dust */
|
|
for (int i = 0; i < 50; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h);
|
|
SpaceColor col = palette[(int)(randf() * palette_count)];
|
|
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
|
(uint8_t)(35 + (int)(randf() * 45)));
|
|
SDL_Rect dot = {x, y, 2, 2};
|
|
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.15f;
|
|
p->near_layer.scroll_y = 0.10f;
|
|
p->near_layer.active = true;
|
|
p->near_layer.owns_texture = true;
|
|
}
|
|
|
|
/* ── Themed: Moon surface ───────────────────────────── */
|
|
|
|
static void generate_moon_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(77);
|
|
|
|
/* Dim, cold stars — no atmosphere, but sparse */
|
|
for (int i = 0; i < 100; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h * 0.55f);
|
|
uint8_t brightness = (uint8_t)(80 + (int)(randf() * 120));
|
|
/* Cool-white tint for airless sky */
|
|
uint8_t r = clamp_u8(brightness - 5);
|
|
uint8_t g = clamp_u8(brightness);
|
|
uint8_t b = clamp_u8(brightness + 10);
|
|
uint8_t a = (uint8_t)(120 + (int)(randf() * 100));
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
}
|
|
|
|
/* A few bright feature stars */
|
|
for (int i = 0; i < 12; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(randf() * h * 0.45f);
|
|
SDL_SetRenderDrawColor(renderer, 240, 240, 255, 220);
|
|
SDL_Rect dot = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &dot);
|
|
/* Cross glow on some */
|
|
if (i < 4) {
|
|
SDL_SetRenderDrawColor(renderer, 200, 200, 230, 80);
|
|
SDL_Rect halo[] = {
|
|
{x - 1, y, 3, 1},
|
|
{x, y - 1, 1, 3},
|
|
};
|
|
SDL_RenderFillRect(renderer, &halo[0]);
|
|
SDL_RenderFillRect(renderer, &halo[1]);
|
|
}
|
|
}
|
|
|
|
/* Distant crater-pocked terrain silhouette */
|
|
int terrain_base = (int)(h * 0.65f);
|
|
for (int x = 0; x < w; x++) {
|
|
float t = (float)x / (float)w;
|
|
/* Gentle rolling hills with crater dips */
|
|
float h1 = sinf(t * 6.28f * 1.5f) * 12.0f;
|
|
float h2 = sinf(t * 6.28f * 3.7f + 0.8f) * 8.0f;
|
|
float h3 = sinf(t * 6.28f * 8.2f + 2.5f) * 4.0f;
|
|
|
|
/* Crater depressions — sharp V-shaped dips */
|
|
float crater = 0.0f;
|
|
float c_positions[] = {0.15f, 0.35f, 0.55f, 0.78f, 0.92f};
|
|
float c_widths[] = {0.06f, 0.08f, 0.05f, 0.10f, 0.04f};
|
|
float c_depths[] = {18.0f, 25.0f, 15.0f, 30.0f, 12.0f};
|
|
for (int c = 0; c < 5; c++) {
|
|
float dist = (t - c_positions[c]) / c_widths[c];
|
|
if (dist > -1.0f && dist < 1.0f) {
|
|
float d = 1.0f - dist * dist; /* parabolic bowl */
|
|
crater += c_depths[c] * d;
|
|
}
|
|
}
|
|
|
|
int peak = terrain_base - (int)(h1 + h2 + h3) + (int)(crater);
|
|
if (peak < terrain_base - 35) peak = terrain_base - 35;
|
|
if (peak > h - 5) peak = h - 5;
|
|
|
|
for (int y = peak; y < h; y++) {
|
|
int depth = y - peak;
|
|
/* Grey regolith tones — lighter at ridgeline, darker below */
|
|
uint8_t base = clamp_u8(30 - depth / 4);
|
|
uint8_t r = clamp_u8(base + 5);
|
|
uint8_t g = clamp_u8(base + 3);
|
|
uint8_t b = clamp_u8(base);
|
|
uint8_t a = (uint8_t)(depth < 3 ? 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;
|
|
|
|
/* Earth in the sky — non-tiling feature sprite */
|
|
SDL_Texture *earth_tex = assets_get_texture("assets/sprites/earth.png");
|
|
if (earth_tex) {
|
|
int ew, eh;
|
|
SDL_QueryTexture(earth_tex, NULL, NULL, &ew, &eh);
|
|
p->feature.texture = earth_tex;
|
|
p->feature.tex_w = ew;
|
|
p->feature.tex_h = eh;
|
|
p->feature.x = (float)(w / 4);
|
|
p->feature.y = (float)(h * 0.08f);
|
|
p->feature.scroll_x = 0.03f; /* same as far layer */
|
|
p->feature.scroll_y = 0.03f;
|
|
p->feature.active = true;
|
|
}
|
|
}
|
|
|
|
static void generate_moon_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(166);
|
|
|
|
/* Closer crater-rim terrain — taller, more detail */
|
|
int terrain_base = (int)(h * 0.72f);
|
|
for (int x = 0; x < w; x++) {
|
|
float t = (float)x / (float)w;
|
|
float h1 = sinf(t * 6.28f * 2.3f + 1.0f) * 18.0f;
|
|
float h2 = sinf(t * 6.28f * 5.1f + 3.5f) * 10.0f;
|
|
float h3 = sinf(t * 6.28f * 13.0f + 0.7f) * 3.0f;
|
|
|
|
/* Near craters — fewer but bolder */
|
|
float crater = 0.0f;
|
|
float c_positions[] = {0.25f, 0.60f, 0.85f};
|
|
float c_widths[] = {0.10f, 0.12f, 0.07f};
|
|
float c_depths[] = {22.0f, 35.0f, 18.0f};
|
|
for (int c = 0; c < 3; c++) {
|
|
float dist = (t - c_positions[c]) / c_widths[c];
|
|
if (dist > -1.0f && dist < 1.0f) {
|
|
float d = 1.0f - dist * dist;
|
|
crater += c_depths[c] * d;
|
|
}
|
|
}
|
|
|
|
int peak = terrain_base - (int)(h1 + h2 + h3) + (int)(crater);
|
|
if (peak < terrain_base - 45) peak = terrain_base - 45;
|
|
if (peak > h - 5) peak = h - 5;
|
|
|
|
for (int y = peak; y < h; y++) {
|
|
int depth = y - peak;
|
|
/* Slightly brighter grey than far layer */
|
|
uint8_t base = clamp_u8(40 - depth / 3);
|
|
uint8_t r = clamp_u8(base + 8);
|
|
uint8_t g = clamp_u8(base + 5);
|
|
uint8_t b = clamp_u8(base + 2);
|
|
uint8_t a = (uint8_t)(depth < 2 ? 80 : 140);
|
|
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
|
SDL_Rect px = {x, y, 1, 1};
|
|
SDL_RenderFillRect(renderer, &px);
|
|
}
|
|
|
|
/* Rim highlight — bright edge at the very top of terrain */
|
|
if (peak < h - 5) {
|
|
uint8_t rim_a = (uint8_t)(40 + (int)(randf() * 30));
|
|
SDL_SetRenderDrawColor(renderer, 80, 75, 65, rim_a);
|
|
SDL_Rect px = {x, peak, 1, 1};
|
|
SDL_RenderFillRect(renderer, &px);
|
|
}
|
|
}
|
|
|
|
/* Scattered regolith dust particles */
|
|
for (int i = 0; i < 30; i++) {
|
|
int x = (int)(randf() * w);
|
|
int y = (int)(h * 0.5f + randf() * h * 0.4f);
|
|
uint8_t grey = (uint8_t)(30 + (int)(randf() * 30));
|
|
SDL_SetRenderDrawColor(renderer, grey + 5, grey + 3, grey,
|
|
(uint8_t)(20 + (int)(randf() * 25)));
|
|
SDL_Rect dot = {x, y, 1, 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;
|
|
}
|
|
|
|
/* ── 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) {
|
|
switch (style) {
|
|
case PARALLAX_STYLE_ALIEN_SKY:
|
|
generate_alien_sky_far(p, renderer);
|
|
generate_alien_sky_near(p, renderer);
|
|
break;
|
|
case PARALLAX_STYLE_INTERIOR:
|
|
generate_interior_far(p, renderer);
|
|
generate_interior_near(p, renderer);
|
|
break;
|
|
case PARALLAX_STYLE_DEEP_SPACE:
|
|
generate_deep_space_far(p, renderer);
|
|
generate_deep_space_near(p, renderer);
|
|
break;
|
|
case PARALLAX_STYLE_MOON:
|
|
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);
|
|
parallax_generate_nebula(p, renderer);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ── Render ──────────────────────────────────────────── */
|
|
|
|
static void render_layer(const ParallaxLayer *layer, const Camera *cam,
|
|
SDL_Renderer *renderer) {
|
|
if (!layer->active || !layer->texture) return;
|
|
|
|
int tw = layer->tex_w;
|
|
int th = layer->tex_h;
|
|
if (tw <= 0 || th <= 0) return;
|
|
|
|
/* Calculate scroll offset from camera position */
|
|
float offset_x = cam->pos.x * layer->scroll_x;
|
|
float offset_y = cam->pos.y * layer->scroll_y;
|
|
|
|
/* Wrap to texture bounds (fmod that handles negatives) */
|
|
int ix = (int)offset_x % tw;
|
|
int iy = (int)offset_y % th;
|
|
if (ix < 0) ix += tw;
|
|
if (iy < 0) iy += th;
|
|
|
|
/* Tile the texture across the screen.
|
|
* We need to cover SCREEN_WIDTH x SCREEN_HEIGHT, starting
|
|
* from the wrapped offset. Draw a 2x2 grid of tiles to
|
|
* ensure full coverage regardless of offset. */
|
|
for (int ty = -1; ty <= 1; ty++) {
|
|
for (int tx = -1; tx <= 1; tx++) {
|
|
SDL_Rect dst = {
|
|
tx * tw - ix,
|
|
ty * th - iy,
|
|
tw,
|
|
th
|
|
};
|
|
/* Skip tiles entirely off-screen */
|
|
if (dst.x + dst.w < 0 || dst.x >= SCREEN_WIDTH) continue;
|
|
if (dst.y + dst.h < 0 || dst.y >= SCREEN_HEIGHT) continue;
|
|
|
|
SDL_RenderCopy(renderer, layer->texture, NULL, &dst);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void render_feature(const ParallaxFeature *f, const Camera *cam,
|
|
SDL_Renderer *renderer) {
|
|
if (!f->active || !f->texture) return;
|
|
|
|
/* Scroll with camera but do not tile */
|
|
int draw_x = (int)(f->x - cam->pos.x * f->scroll_x);
|
|
int draw_y = (int)(f->y - cam->pos.y * f->scroll_y);
|
|
|
|
SDL_Rect dst = {draw_x, draw_y, f->tex_w, f->tex_h};
|
|
if (dst.x + dst.w < 0 || dst.x >= SCREEN_WIDTH) return;
|
|
if (dst.y + dst.h < 0 || dst.y >= SCREEN_HEIGHT) return;
|
|
|
|
SDL_RenderCopy(renderer, f->texture, NULL, &dst);
|
|
}
|
|
|
|
void parallax_render(const Parallax *p, const Camera *cam, SDL_Renderer *renderer) {
|
|
if (!p || !cam) return;
|
|
render_layer(&p->far_layer, cam, renderer);
|
|
render_feature(&p->feature, cam, renderer);
|
|
render_layer(&p->near_layer, cam, renderer);
|
|
}
|
|
|
|
/* ── Free ────────────────────────────────────────────── */
|
|
|
|
void parallax_free(Parallax *p) {
|
|
/* Only free textures we generated — asset manager owns file-loaded ones */
|
|
if (p->far_layer.texture && p->far_layer.owns_texture) {
|
|
SDL_DestroyTexture(p->far_layer.texture);
|
|
}
|
|
p->far_layer.texture = NULL;
|
|
p->far_layer.active = false;
|
|
|
|
if (p->near_layer.texture && p->near_layer.owns_texture) {
|
|
SDL_DestroyTexture(p->near_layer.texture);
|
|
}
|
|
p->near_layer.texture = NULL;
|
|
p->near_layer.active = false;
|
|
|
|
/* Feature sprite is asset-manager-owned — just clear the reference */
|
|
p->feature.texture = NULL;
|
|
p->feature.active = false;
|
|
}
|