Add in-game level editor with auto-discovered tile/entity palettes

Implements a full level editor that runs inside the game engine as an
alternative mode, accessible via --edit flag or E key during gameplay.
The editor auto-discovers available tiles from the tileset texture and
entities from a new central registry, so adding new game content
automatically appears in the editor without any editor-specific changes.

Editor features: tile painting (pencil/eraser/flood fill) across 3
layers, entity placement with drag-to-move, player spawn point tool,
camera pan/zoom, grid overlay, .lvl save/load, map resize, and test
play (P to play, ESC to return to editor).

Supporting changes:
- Entity registry centralizes spawn functions (replaces strcmp chain)
- Mouse input + raw keyboard access added to input system
- Camera zoom support for editor overview
- Zoom-aware rendering in tilemap, renderer, and sprite systems
- Powerup and drone sprites/animations wired up (were defined but unused)
- Bitmap font renderer for editor UI (4x6 pixel glyphs, no dependencies)
This commit is contained in:
Thomas
2026-02-28 20:24:43 +00:00
parent c66c12ae68
commit ea6e16358f
30 changed files with 4959 additions and 51 deletions

View File

@@ -88,6 +88,12 @@ void audio_stop_music(void) {
Mix_HaltMusic();
}
void audio_free_music(Music *m) {
if (!m || !m->music) return;
Mix_FreeMusic((Mix_Music *)m->music);
m->music = NULL;
}
void audio_set_music_volume(int volume) {
if (!s_initialized) return;
Mix_VolumeMusic(volume);

View File

@@ -17,6 +17,7 @@ Music audio_load_music(const char *path);
void audio_play_sound(Sound s, int volume); /* 0-128, fire and forget */
void audio_play_music(Music m, bool loop);
void audio_stop_music(void);
void audio_free_music(Music *m);
void audio_set_music_volume(int volume); /* 0-128 */
void audio_shutdown(void);

View File

@@ -10,6 +10,7 @@ void camera_init(Camera *c, float vp_w, float vp_h) {
c->smoothing = 5.0f;
c->deadzone = vec2(30.0f, 20.0f);
c->look_ahead = vec2(40.0f, 0.0f);
c->zoom = 1.0f;
}
void camera_set_bounds(Camera *c, float world_w, float world_h) {
@@ -40,6 +41,10 @@ void camera_follow(Camera *c, Vec2 target, Vec2 velocity, float dt) {
Vec2 camera_world_to_screen(const Camera *c, Vec2 world_pos) {
Vec2 screen = vec2_sub(world_pos, c->pos);
/* Apply zoom */
if (c->zoom != 1.0f && c->zoom > 0.0f) {
screen = vec2_scale(screen, c->zoom);
}
/* Apply shake offset */
screen.x += c->shake_offset.x;
screen.y += c->shake_offset.y;
@@ -47,7 +52,15 @@ Vec2 camera_world_to_screen(const Camera *c, Vec2 world_pos) {
}
Vec2 camera_screen_to_world(const Camera *c, Vec2 screen_pos) {
return vec2_add(screen_pos, c->pos);
Vec2 world = screen_pos;
/* Remove shake */
world.x -= c->shake_offset.x;
world.y -= c->shake_offset.y;
/* Reverse zoom */
if (c->zoom != 1.0f && c->zoom > 0.0f) {
world = vec2_scale(world, 1.0f / c->zoom);
}
return vec2_add(world, c->pos);
}
/* ── Screen shake ────────────────────────────────── */

View File

@@ -12,6 +12,8 @@ typedef struct Camera {
float smoothing; /* higher = slower follow (0=instant) */
Vec2 deadzone; /* half-size of deadzone rect */
Vec2 look_ahead; /* pixels to lead in move direction */
/* Zoom (used by editor; gameplay keeps 1.0) */
float zoom; /* 1.0 = normal, <1 = zoomed out */
/* Screen shake */
float shake_timer; /* remaining shake time */
float shake_intensity; /* current max pixel offset */

View File

@@ -20,6 +20,12 @@ typedef enum EntityType {
ENT_PROJECTILE,
ENT_PICKUP,
ENT_PARTICLE,
ENT_TURRET,
ENT_MOVING_PLATFORM,
ENT_FLAME_VENT,
ENT_FORCE_FIELD,
ENT_POWERUP,
ENT_DRONE,
ENT_TYPE_COUNT
} EntityType;

View File

@@ -14,6 +14,19 @@ static bool s_latched_released[ACTION_COUNT];
static bool s_quit_requested;
/* ── Mouse state ──────────────────────────────────── */
static int s_mouse_x, s_mouse_y;
static bool s_mouse_current[MOUSE_BUTTON_COUNT];
static bool s_mouse_previous[MOUSE_BUTTON_COUNT];
static bool s_mouse_latched_pressed[MOUSE_BUTTON_COUNT];
static bool s_mouse_latched_released[MOUSE_BUTTON_COUNT];
static int s_mouse_scroll;
/* ── Raw keyboard state ───────────────────────────── */
static const Uint8 *s_key_state = NULL;
static Uint8 s_prev_keys[SDL_NUM_SCANCODES];
static Uint8 s_latched_keys[SDL_NUM_SCANCODES];
/* Default key bindings (primary + alternate) */
static SDL_Scancode s_bindings[ACTION_COUNT] = {
[ACTION_LEFT] = SDL_SCANCODE_LEFT,
@@ -36,13 +49,28 @@ void input_init(void) {
memset(s_previous, 0, sizeof(s_previous));
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
memset(s_latched_released, 0, sizeof(s_latched_released));
memset(s_mouse_current, 0, sizeof(s_mouse_current));
memset(s_mouse_previous, 0, sizeof(s_mouse_previous));
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
memset(s_prev_keys, 0, sizeof(s_prev_keys));
memset(s_latched_keys, 0, sizeof(s_latched_keys));
s_mouse_x = s_mouse_y = 0;
s_mouse_scroll = 0;
s_quit_requested = false;
}
void input_poll(void) {
/* Save previous state */
memcpy(s_previous, s_current, sizeof(s_current));
memcpy(s_mouse_previous, s_mouse_current, sizeof(s_mouse_current));
s_quit_requested = false;
s_mouse_scroll = 0;
/* Save previous raw key state */
if (s_key_state) {
memcpy(s_prev_keys, s_key_state, SDL_NUM_SCANCODES);
}
/* Process SDL events */
SDL_Event event;
@@ -51,14 +79,17 @@ void input_poll(void) {
case SDL_QUIT:
s_quit_requested = true;
break;
case SDL_MOUSEWHEEL:
s_mouse_scroll += event.wheel.y;
break;
}
}
/* Read keyboard state */
const Uint8 *keys = SDL_GetKeyboardState(NULL);
s_key_state = SDL_GetKeyboardState(NULL);
for (int i = 0; i < ACTION_COUNT; i++) {
s_current[i] = keys[s_bindings[i]];
if (s_alt_bindings[i] && keys[s_alt_bindings[i]]) {
s_current[i] = s_key_state[s_bindings[i]];
if (s_alt_bindings[i] && s_key_state[s_alt_bindings[i]]) {
s_current[i] = true;
}
@@ -70,12 +101,50 @@ void input_poll(void) {
s_latched_released[i] = true;
}
}
/* Latch raw key edges */
if (s_key_state) {
for (int i = 0; i < SDL_NUM_SCANCODES; i++) {
if (s_key_state[i] && !s_prev_keys[i]) {
s_latched_keys[i] = 1;
}
}
}
/* Read mouse state */
Uint32 buttons = SDL_GetMouseState(&s_mouse_x, &s_mouse_y);
/* Convert window coords to logical coords (SDL handles this via
SDL_RenderSetLogicalSize, but GetMouseState returns window coords) */
SDL_Renderer *r = SDL_GetRenderer(SDL_GetMouseFocus());
if (r) {
float lx, ly;
SDL_RenderWindowToLogical(r, s_mouse_x, s_mouse_y, &lx, &ly);
s_mouse_x = (int)lx;
s_mouse_y = (int)ly;
}
s_mouse_current[MOUSE_LEFT] = (buttons & SDL_BUTTON_LMASK) != 0;
s_mouse_current[MOUSE_MIDDLE] = (buttons & SDL_BUTTON_MMASK) != 0;
s_mouse_current[MOUSE_RIGHT] = (buttons & SDL_BUTTON_RMASK) != 0;
for (int i = 0; i < MOUSE_BUTTON_COUNT; i++) {
if (s_mouse_current[i] && !s_mouse_previous[i]) {
s_mouse_latched_pressed[i] = true;
}
if (!s_mouse_current[i] && s_mouse_previous[i]) {
s_mouse_latched_released[i] = true;
}
}
}
void input_consume(void) {
/* Clear latched states after an update tick has read them */
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
memset(s_latched_released, 0, sizeof(s_latched_released));
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
memset(s_latched_keys, 0, sizeof(s_latched_keys));
}
bool input_pressed(Action a) {
@@ -94,6 +163,44 @@ bool input_quit_requested(void) {
return s_quit_requested;
}
/* ── Mouse queries ────────────────────────────────── */
void input_mouse_pos(int *x, int *y) {
if (x) *x = s_mouse_x;
if (y) *y = s_mouse_y;
}
bool input_mouse_pressed(MouseButton btn) {
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
return s_mouse_latched_pressed[btn];
}
bool input_mouse_held(MouseButton btn) {
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
return s_mouse_current[btn];
}
bool input_mouse_released(MouseButton btn) {
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
return s_mouse_latched_released[btn];
}
int input_mouse_scroll(void) {
return s_mouse_scroll;
}
/* ── Raw keyboard queries ─────────────────────────── */
bool input_key_pressed(SDL_Scancode key) {
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
return s_latched_keys[key] != 0;
}
bool input_key_held(SDL_Scancode key) {
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
return s_key_state && s_key_state[key];
}
void input_shutdown(void) {
/* Nothing to clean up */
}

View File

@@ -27,4 +27,27 @@ void input_shutdown(void);
/* Returns true if SDL_QUIT was received */
bool input_quit_requested(void);
/* ── Mouse state ────────────────────────────────── */
typedef enum MouseButton {
MOUSE_LEFT = 0,
MOUSE_MIDDLE = 1,
MOUSE_RIGHT = 2,
MOUSE_BUTTON_COUNT
} MouseButton;
/* Mouse position in logical (game) coordinates */
void input_mouse_pos(int *x, int *y);
/* Mouse button queries (same semantics as keyboard) */
bool input_mouse_pressed(MouseButton btn);
bool input_mouse_held(MouseButton btn);
bool input_mouse_released(MouseButton btn);
/* Scroll wheel delta since last poll (positive = up) */
int input_mouse_scroll(void);
/* Raw keyboard access for text-like input in editor */
bool input_key_pressed(SDL_Scancode key);
bool input_key_held(SDL_Scancode key);
#endif /* JNR_INPUT_H */

View File

@@ -231,6 +231,491 @@ void parallax_generate_nebula(Parallax *p, SDL_Renderer *renderer) {
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 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_DEFAULT:
default:
parallax_generate_stars(p, renderer);
parallax_generate_nebula(p, renderer);
break;
}
}
/* ── Render ──────────────────────────────────────────── */
static void render_layer(const ParallaxLayer *layer, const Camera *cam,

View File

@@ -37,6 +37,17 @@ void parallax_generate_stars(Parallax *p, SDL_Renderer *renderer);
/* Generate procedural nebula/dust texture (near layer) */
void parallax_generate_nebula(Parallax *p, SDL_Renderer *renderer);
/* Themed parallax generation styles */
typedef enum ParallaxStyle {
PARALLAX_STYLE_DEFAULT, /* generic space (same as stars+nebula) */
PARALLAX_STYLE_ALIEN_SKY, /* alien planet surface: dusty, hazy */
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
} ParallaxStyle;
/* Generate both layers with a unified style */
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style);
/* Render both layers (call before tile/entity rendering) */
void parallax_render(const Parallax *p, const Camera *cam, SDL_Renderer *renderer);

View File

@@ -43,11 +43,18 @@ void renderer_flush(const Camera *cam) {
screen_pos = camera_world_to_screen(cam, s->pos);
}
float zw = s->size.x;
float zh = s->size.y;
if (layer != LAYER_HUD && cam && cam->zoom != 1.0f && cam->zoom > 0.0f) {
zw *= cam->zoom;
zh *= cam->zoom;
}
SDL_Rect dst = {
(int)screen_pos.x,
(int)screen_pos.y,
(int)s->size.x,
(int)s->size.y
(int)(zw + 0.5f),
(int)(zh + 0.5f)
};
SDL_RendererFlip flip = SDL_FLIP_NONE;
@@ -75,15 +82,20 @@ void renderer_present(void) {
void renderer_draw_rect(Vec2 pos, Vec2 size, SDL_Color color,
DrawLayer layer, const Camera *cam) {
Vec2 screen_pos = pos;
float zw = size.x, zh = size.y;
if (layer != LAYER_HUD && cam) {
screen_pos = camera_world_to_screen(cam, pos);
if (cam->zoom != 1.0f && cam->zoom > 0.0f) {
zw *= cam->zoom;
zh *= cam->zoom;
}
}
SDL_Rect rect = {
(int)screen_pos.x,
(int)screen_pos.y,
(int)size.x,
(int)size.y
(int)(zw + 0.5f),
(int)(zh + 0.5f)
};
SDL_SetRenderDrawColor(s_renderer, color.r, color.g, color.b, color.a);

View File

@@ -129,10 +129,11 @@ void tilemap_render_layer(const Tilemap *map, const uint16_t *layer,
int end_x = map->width, end_y = map->height;
if (cam) {
float inv_zoom = (cam->zoom > 0.0f) ? (1.0f / cam->zoom) : 1.0f;
start_x = (int)(cam->pos.x / TILE_SIZE) - 1;
start_y = (int)(cam->pos.y / TILE_SIZE) - 1;
end_x = start_x + (int)(cam->viewport.x / TILE_SIZE) + 3;
end_y = start_y + (int)(cam->viewport.y / TILE_SIZE) + 3;
end_x = start_x + (int)(cam->viewport.x * inv_zoom / TILE_SIZE) + 3;
end_y = start_y + (int)(cam->viewport.y * inv_zoom / TILE_SIZE) + 3;
if (start_x < 0) start_x = 0;
if (start_y < 0) start_y = 0;
@@ -163,11 +164,16 @@ void tilemap_render_layer(const Tilemap *map, const uint16_t *layer,
Vec2 world_pos = vec2(tile_to_world(x), tile_to_world(y));
Vec2 screen_pos = cam ? camera_world_to_screen(cam, world_pos) : world_pos;
float tile_draw_size = TILE_SIZE;
if (cam && cam->zoom != 1.0f && cam->zoom > 0.0f) {
tile_draw_size = TILE_SIZE * cam->zoom;
}
SDL_Rect dst = {
(int)screen_pos.x,
(int)screen_pos.y,
TILE_SIZE,
TILE_SIZE
(int)(tile_draw_size + 0.5f),
(int)(tile_draw_size + 0.5f)
};
SDL_RenderCopy(renderer, map->tileset, &src, &dst);

View File

@@ -46,6 +46,7 @@ typedef struct Tilemap {
bool has_bg_color; /* true if BG_COLOR was set */
char parallax_far_path[ASSET_PATH_MAX]; /* far bg image path */
char parallax_near_path[ASSET_PATH_MAX]; /* near bg image path */
int parallax_style; /* procedural bg style (0=default) */
EntitySpawn entity_spawns[MAX_ENTITY_SPAWNS];
int entity_spawn_count;
} Tilemap;