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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 ────────────────────────────────── */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user