#include "engine/parallax.h" #include "engine/assets.h" #include "engine/camera.h" #include #include #include #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; }