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

@@ -16,6 +16,13 @@ ENTITY grunt 9 16
ENTITY flyer 20 10
ENTITY flyer 30 8
# Hazards
ENTITY turret 33 4
ENTITY flame_vent 18 19
ENTITY force_field 35 17
ENTITY platform 12 11
ENTITY platform_v 27 12
# Tile definitions: ID TEX_X TEX_Y FLAGS
# Flag 1 = SOLID, Flag 2 = PLATFORM
TILEDEF 1 0 0 1

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;

217
src/game/drone.c Normal file
View File

@@ -0,0 +1,217 @@
#include "game/drone.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include "engine/physics.h"
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ── Stashed entity manager ────────────────────────── */
static EntityManager *s_em = NULL;
/* ── Helpers ───────────────────────────────────────── */
/* Find the active player entity */
static Entity *find_player(void) {
if (!s_em) return NULL;
for (int i = 0; i < s_em->count; i++) {
Entity *e = &s_em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
/* Find nearest enemy within range */
static Entity *find_nearest_enemy(Vec2 from, float range) {
if (!s_em) return NULL;
Entity *best = NULL;
float best_dist = range * range;
for (int i = 0; i < s_em->count; i++) {
Entity *e = &s_em->entities[i];
if (!e->active || e->health <= 0) continue;
if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER &&
e->type != ENT_TURRET) continue;
Vec2 epos = vec2(e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f);
float dx = epos.x - from.x;
float dy = epos.y - from.y;
float dist2 = dx * dx + dy * dy;
if (dist2 < best_dist) {
best_dist = dist2;
best = e;
}
}
return best;
}
/* ── Callbacks ─────────────────────────────────────── */
static void drone_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
DroneData *dd = (DroneData *)self->data;
if (!dd) return;
/* Countdown lifetime */
dd->lifetime -= dt;
if (dd->lifetime <= 0) {
/* Expiry: death particles and destroy */
Vec2 center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
particle_emit_spark(center, (SDL_Color){50, 200, 255, 255});
self->flags |= ENTITY_DEAD;
return;
}
/* Follow player — orbit around player center */
Entity *player = find_player();
if (!player) return;
Vec2 pcenter = vec2(
player->body.pos.x + player->body.size.x * 0.5f,
player->body.pos.y + player->body.size.y * 0.5f
);
dd->orbit_angle += DRONE_ORBIT_SPEED * dt;
if (dd->orbit_angle > 2.0f * (float)M_PI)
dd->orbit_angle -= 2.0f * (float)M_PI;
float target_x = pcenter.x + cosf(dd->orbit_angle) * DRONE_ORBIT_RADIUS;
float target_y = pcenter.y + sinf(dd->orbit_angle) * DRONE_ORBIT_RADIUS;
/* Smooth movement toward orbit position */
float lerp = 8.0f * dt;
if (lerp > 1.0f) lerp = 1.0f;
self->body.pos.x += (target_x - self->body.pos.x - self->body.size.x * 0.5f) * lerp;
self->body.pos.y += (target_y - self->body.pos.y - self->body.size.y * 0.5f) * lerp;
/* Shooting */
dd->shoot_cooldown -= dt;
if (dd->shoot_cooldown <= 0) {
Vec2 drone_center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
Entity *target = find_nearest_enemy(drone_center, DRONE_SHOOT_RANGE);
if (target) {
Vec2 tcenter = vec2(
target->body.pos.x + target->body.size.x * 0.5f,
target->body.pos.y + target->body.size.y * 0.5f
);
Vec2 dir = vec2(tcenter.x - drone_center.x, tcenter.y - drone_center.y);
/* Spawn projectile from drone, counted as player bullet */
projectile_spawn_def(s_em, &WEAPON_PLASMA,
drone_center, dir, true);
dd->shoot_cooldown = DRONE_SHOOT_CD;
/* Muzzle flash */
particle_emit_muzzle_flash(drone_center, dir);
}
}
/* Trail particles — subtle engine glow */
if ((int)(dd->lifetime * 10.0f) % 3 == 0) {
Vec2 center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
ParticleBurst b = {
.origin = center,
.count = 1,
.speed_min = 5.0f,
.speed_max = 15.0f,
.life_min = 0.1f,
.life_max = 0.3f,
.size_min = 1.0f,
.size_max = 1.5f,
.spread = (float)M_PI * 2.0f,
.direction = 0,
.drag = 3.0f,
.gravity_scale = 0.0f,
.color = (SDL_Color){50, 180, 255, 150},
.color_vary = true,
};
particle_emit(&b);
}
/* Blink when about to expire (last 3 seconds) */
animation_update(&self->anim, dt);
}
static void drone_render(Entity *self, const Camera *cam) {
(void)cam;
DroneData *dd = (DroneData *)self->data;
if (!dd) return;
/* Blink when about to expire */
if (dd->lifetime < 3.0f) {
int blink = (int)(dd->lifetime * 8.0f);
if (blink % 2 == 0) return; /* skip rendering every other frame */
}
if (!g_spritesheet || !self->anim.def) return;
SDL_Rect src = animation_current_rect(&self->anim);
Body *body = &self->body;
/* Center sprite on hitbox */
float draw_x = body->pos.x + body->size.x * 0.5f - SPRITE_CELL * 0.5f;
float draw_y = body->pos.y + body->size.y * 0.5f - SPRITE_CELL * 0.5f;
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = vec2(draw_x, draw_y),
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
}
static void drone_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
/* ── Public API ────────────────────────────────────── */
void drone_register(EntityManager *em) {
entity_register(em, ENT_DRONE, drone_update, drone_render, drone_destroy);
s_em = em;
}
Entity *drone_spawn(EntityManager *em, Vec2 player_pos) {
Entity *e = entity_spawn(em, ENT_DRONE, player_pos);
if (!e) return NULL;
e->body.size = vec2(10.0f, 10.0f);
e->body.gravity_scale = 0.0f; /* floats freely */
e->health = 99; /* practically invulnerable */
e->max_health = 99;
e->damage = 0;
e->flags |= ENTITY_INVINCIBLE;
DroneData *dd = calloc(1, sizeof(DroneData));
dd->orbit_angle = 0.0f;
dd->shoot_cooldown = 0.5f; /* short initial delay */
dd->lifetime = DRONE_DURATION;
e->data = dd;
animation_set(&e->anim, &anim_drone);
/* Spawn burst */
particle_emit_spark(player_pos, (SDL_Color){50, 200, 255, 255});
printf("Drone deployed! (%0.0fs)\n", DRONE_DURATION);
return e;
}

33
src/game/drone.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef JNR_DRONE_H
#define JNR_DRONE_H
#include "engine/entity.h"
/* ═══════════════════════════════════════════════════
* Combat Drone
*
* A small companion that orbits the player and
* periodically fires at nearby enemies. Spawned
* by the drone powerup. One drone at a time.
* Duration: 15 seconds, then self-destructs.
* ═══════════════════════════════════════════════════ */
#define DRONE_DURATION 15.0f /* seconds before expiry */
#define DRONE_ORBIT_RADIUS 28.0f /* orbit distance from player */
#define DRONE_ORBIT_SPEED 3.0f /* radians per second */
#define DRONE_SHOOT_RANGE 120.0f /* detection range for enemies */
#define DRONE_SHOOT_CD 1.2f /* seconds between shots */
typedef struct DroneData {
float orbit_angle; /* current angle around player */
float shoot_cooldown; /* time until next shot */
float lifetime; /* remaining lifetime */
} DroneData;
/* Register the drone entity type */
void drone_register(EntityManager *em);
/* Spawn a drone attached to the player */
Entity *drone_spawn(EntityManager *em, Vec2 player_pos);
#endif /* JNR_DRONE_H */

1118
src/game/editor.c Normal file

File diff suppressed because it is too large Load Diff

98
src/game/editor.h Normal file
View File

@@ -0,0 +1,98 @@
#ifndef JNR_EDITOR_H
#define JNR_EDITOR_H
#include "engine/tilemap.h"
#include "engine/camera.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
/* ═══════════════════════════════════════════════════
* Level Editor
*
* An in-engine level editor that runs as an
* alternative game mode. Uses the same renderer,
* camera, and tilemap systems as gameplay.
*
* Tile palette is auto-discovered from the tileset
* texture. Entity palette is auto-discovered from
* the entity registry. Adding new tiles or entities
* to the game makes them appear in the editor
* automatically.
* ═══════════════════════════════════════════════════ */
typedef enum EditorTool {
TOOL_PENCIL, /* paint tiles */
TOOL_ERASER, /* clear tiles (paint tile 0) */
TOOL_FILL, /* flood fill area */
TOOL_ENTITY, /* place/select entities */
TOOL_SPAWN, /* set player spawn point */
TOOL_COUNT
} EditorTool;
typedef enum EditorLayer {
EDITOR_LAYER_COLLISION,
EDITOR_LAYER_BG,
EDITOR_LAYER_FG,
EDITOR_LAYER_COUNT
} EditorLayer;
/* UI panel regions */
#define EDITOR_TOOLBAR_H 14 /* top bar height */
#define EDITOR_PALETTE_W 80 /* right panel width */
#define EDITOR_STATUS_H 10 /* bottom status bar height */
typedef struct Editor {
/* ── Map data ──────────────────────────── */
Tilemap map;
char file_path[256];
bool has_file; /* true if loaded from / saved to file */
bool dirty; /* unsaved changes */
/* ── Camera ────────────────────────────── */
Camera camera;
/* ── Tool state ────────────────────────── */
EditorTool tool;
EditorLayer active_layer;
uint16_t selected_tile; /* currently selected tile ID */
int selected_entity; /* index into entity registry */
bool show_grid;
bool show_all_layers; /* dim inactive layers */
/* ── Entity editing ────────────────────── */
int dragging_entity; /* index in entity_spawns, or -1 */
float drag_offset_x; /* offset from entity origin to mouse */
float drag_offset_y;
/* ── Palette scroll ────────────────────── */
int tile_palette_scroll;
int entity_palette_scroll;
/* ── Tileset info (auto-discovered) ────── */
int tileset_cols;
int tileset_rows;
int tileset_total; /* total tiles in tileset */
/* ── UI state ──────────────────────────── */
bool active; /* editor is running */
} Editor;
/* Lifecycle */
void editor_init(Editor *ed);
void editor_new_level(Editor *ed, int width, int height);
bool editor_load(Editor *ed, const char *path);
bool editor_save(Editor *ed);
bool editor_save_as(Editor *ed, const char *path);
void editor_free(Editor *ed);
/* Frame callbacks (plug into engine) */
void editor_update(Editor *ed, float dt);
void editor_render(Editor *ed, float interpolation);
/* Returns true if the user pressed the test-play key */
bool editor_wants_test_play(Editor *ed);
/* Returns true if the editor wants to quit */
bool editor_wants_quit(Editor *ed);
#endif /* JNR_EDITOR_H */

108
src/game/entity_registry.c Normal file
View File

@@ -0,0 +1,108 @@
#include "game/entity_registry.h"
#include "game/player.h"
#include "game/enemy.h"
#include "game/projectile.h"
#include "game/hazards.h"
#include "game/powerup.h"
#include "game/drone.h"
#include <string.h>
#include <stdio.h>
EntityRegistry g_entity_registry = {0};
/* ── Wrapper spawn functions ──────────────────────
* Some spawn functions need extra args beyond (em, pos).
* We wrap them to match the EntitySpawnFn signature.
* ──────────────────────────────────────────────────── */
static Entity *spawn_platform_v(EntityManager *em, Vec2 pos) {
return mplat_spawn_dir(em, pos, vec2(0.0f, 1.0f));
}
static Entity *spawn_powerup_health(EntityManager *em, Vec2 pos) {
return powerup_spawn_health(em, pos);
}
static Entity *spawn_powerup_jetpack(EntityManager *em, Vec2 pos) {
return powerup_spawn_jetpack(em, pos);
}
static Entity *spawn_powerup_drone(EntityManager *em, Vec2 pos) {
return powerup_spawn_drone(em, pos);
}
/* ── Registry population ─────────────────────────── */
static void reg_add(const char *name, const char *display,
EntitySpawnFn fn, SDL_Color color, int w, int h) {
EntityRegistry *r = &g_entity_registry;
if (r->count >= MAX_REGISTRY_ENTRIES) return;
r->entries[r->count++] = (EntityRegEntry){
.name = name,
.display = display,
.spawn_fn = fn,
.color = color,
.width = w,
.height = h,
};
}
void entity_registry_init(EntityManager *em) {
g_entity_registry.count = 0;
/* Register all entity behaviors with the entity manager */
player_register(em);
player_set_entity_manager(em);
grunt_register(em);
flyer_register(em);
projectile_register(em);
turret_register(em);
mplat_register(em);
flame_vent_register(em);
force_field_register(em);
powerup_register(em);
drone_register(em);
/* ════════════════════════════════════════════
* REGISTRY TABLE
*
* To add a new entity type to the game:
* 1. Create its .h/.c files
* 2. Register its behaviors above
* 3. Add one reg_add() line below
*
* The editor will pick it up automatically.
* ════════════════════════════════════════════ */
/* .lvl name display name spawn fn color (editor) w h */
reg_add("grunt", "Grunt", grunt_spawn, (SDL_Color){200, 60, 60, 255}, GRUNT_WIDTH, GRUNT_HEIGHT);
reg_add("flyer", "Flyer", flyer_spawn, (SDL_Color){140, 80, 200, 255}, FLYER_WIDTH, FLYER_HEIGHT);
reg_add("turret", "Turret", turret_spawn, (SDL_Color){160, 160, 160, 255}, TURRET_WIDTH, TURRET_HEIGHT);
reg_add("platform", "Platform (H)", mplat_spawn, (SDL_Color){80, 180, 80, 255}, MPLAT_WIDTH, MPLAT_HEIGHT);
reg_add("platform_v", "Platform (V)", spawn_platform_v, (SDL_Color){80, 160, 100, 255}, MPLAT_WIDTH, MPLAT_HEIGHT);
reg_add("flame_vent", "Flame Vent", flame_vent_spawn, (SDL_Color){255, 120, 40, 255}, FLAME_WIDTH, FLAME_HEIGHT);
reg_add("force_field", "Force Field", force_field_spawn, (SDL_Color){60, 140, 255, 255}, FFIELD_WIDTH, FFIELD_HEIGHT);
reg_add("powerup_hp", "Health Pickup", spawn_powerup_health, (SDL_Color){255, 80, 80, 255}, 12, 12);
reg_add("powerup_jet", "Jetpack Refill", spawn_powerup_jetpack,(SDL_Color){255, 200, 50, 255}, 12, 12);
reg_add("powerup_drone", "Drone Pickup", spawn_powerup_drone, (SDL_Color){80, 200, 255, 255}, 12, 12);
printf("Entity registry: %d types registered\n", g_entity_registry.count);
}
const EntityRegEntry *entity_registry_find(const char *name) {
for (int i = 0; i < g_entity_registry.count; i++) {
if (strcmp(g_entity_registry.entries[i].name, name) == 0) {
return &g_entity_registry.entries[i];
}
}
return NULL;
}
Entity *entity_registry_spawn(EntityManager *em, const char *name, Vec2 pos) {
const EntityRegEntry *entry = entity_registry_find(name);
if (!entry) {
fprintf(stderr, "entity_registry_spawn: unknown type '%s'\n", name);
return NULL;
}
return entry->spawn_fn(em, pos);
}

View File

@@ -0,0 +1,52 @@
#ifndef JNR_ENTITY_REGISTRY_H
#define JNR_ENTITY_REGISTRY_H
#include "engine/entity.h"
#include <SDL2/SDL.h>
/* ═══════════════════════════════════════════════════
* Entity Registry
*
* Central table that maps entity type name strings
* to their spawn functions and metadata. Used by:
* - Level loader (to spawn entities from .lvl files)
* - Level editor (to discover available entity types)
*
* When adding a new entity type to the game, add one
* entry here and it will appear in the editor palette
* automatically.
* ═══════════════════════════════════════════════════ */
#define MAX_REGISTRY_ENTRIES 32
/* Spawn function: creates an entity at position, returns it */
typedef Entity *(*EntitySpawnFn)(EntityManager *em, Vec2 pos);
typedef struct EntityRegEntry {
const char *name; /* .lvl file name, e.g. "grunt" */
const char *display; /* human-readable, e.g. "Grunt" */
EntitySpawnFn spawn_fn; /* function that creates the entity */
SDL_Color color; /* editor palette color for preview */
int width; /* hitbox width (for editor preview) */
int height; /* hitbox height (for editor preview) */
} EntityRegEntry;
typedef struct EntityRegistry {
EntityRegEntry entries[MAX_REGISTRY_ENTRIES];
int count;
} EntityRegistry;
/* Global registry instance */
extern EntityRegistry g_entity_registry;
/* Call once at startup to populate the registry.
* Also registers entity behaviors with the given EntityManager. */
void entity_registry_init(EntityManager *em);
/* Look up a registry entry by name (returns NULL if not found) */
const EntityRegEntry *entity_registry_find(const char *name);
/* Spawn an entity by registry name. Returns NULL if name unknown. */
Entity *entity_registry_spawn(EntityManager *em, const char *name, Vec2 pos);
#endif /* JNR_ENTITY_REGISTRY_H */

599
src/game/hazards.c Normal file
View File

@@ -0,0 +1,599 @@
#include "game/hazards.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include <stdlib.h>
#include <math.h>
/* ════════════════════════════════════════════════════
* Shared helpers
* ════════════════════════════════════════════════════ */
/* Find the player entity in the manager */
static Entity *find_player(EntityManager *em) {
for (int i = 0; i < em->count; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
/* ════════════════════════════════════════════════════
* TURRET — floor/wall-mounted gun, shoots at player
* ════════════════════════════════════════════════════ */
static EntityManager *s_turret_em = NULL;
static void turret_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
TurretData *td = (TurretData *)self->data;
if (!td) return;
/* Death: turrets can be destroyed */
if (self->flags & ENTITY_DEAD) {
self->timer -= dt;
if (self->timer <= 0) {
entity_destroy(s_turret_em, self);
}
return;
}
/* Flash timer for muzzle flash visual */
if (td->fire_flash > 0) {
td->fire_flash -= dt;
}
/* Look for player */
Entity *player = find_player(s_turret_em);
if (!player || !player->active || (player->flags & ENTITY_DEAD)) {
td->shoot_timer = TURRET_SHOOT_CD;
return;
}
/* Distance check */
float px = player->body.pos.x + player->body.size.x * 0.5f;
float py = player->body.pos.y + player->body.size.y * 0.5f;
float tx = self->body.pos.x + self->body.size.x * 0.5f;
float ty = self->body.pos.y + self->body.size.y * 0.5f;
float dx = px - tx;
float dy = py - ty;
float dist = sqrtf(dx * dx + dy * dy);
/* Face toward player */
if (dx < 0) self->flags |= ENTITY_FACING_LEFT;
else self->flags &= ~ENTITY_FACING_LEFT;
if (dist < TURRET_DETECT_RANGE) {
td->shoot_timer -= dt;
if (td->shoot_timer <= 0 && s_turret_em) {
td->shoot_timer = TURRET_SHOOT_CD;
td->fire_flash = 0.15f;
/* Aim direction: normalize vector to player */
Vec2 dir;
if (dist > 0.01f) {
dir = vec2(dx / dist, dy / dist);
} else {
dir = vec2(1.0f, 0.0f);
}
/* Spawn projectile from turret barrel */
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
float bx = facing_left ?
self->body.pos.x - 4.0f :
self->body.pos.x + self->body.size.x;
float by = self->body.pos.y + self->body.size.y * 0.3f;
projectile_spawn_dir(s_turret_em, vec2(bx, by), dir, false);
}
} else {
/* Reset cooldown when player leaves range */
if (td->shoot_timer < TURRET_SHOOT_CD * 0.5f)
td->shoot_timer = TURRET_SHOOT_CD * 0.5f;
}
/* Animation */
if (td->fire_flash > 0) {
animation_set(&self->anim, &anim_turret_fire);
} else {
animation_set(&self->anim, &anim_turret_idle);
}
animation_update(&self->anim, dt);
}
static void turret_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Vec2 render_pos = vec2(
body->pos.x - 1.0f,
body->pos.y
);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {130, 130, 130, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void turret_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void turret_register(EntityManager *em) {
entity_register(em, ENT_TURRET, turret_update, turret_render, turret_destroy);
s_turret_em = em;
}
Entity *turret_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_TURRET, pos);
if (!e) return NULL;
e->body.size = vec2(TURRET_WIDTH, TURRET_HEIGHT);
e->body.gravity_scale = 0.0f; /* turrets are stationary */
e->health = TURRET_HEALTH;
e->max_health = TURRET_HEALTH;
e->damage = 1; /* light contact damage — turret is a hostile machine */
TurretData *td = calloc(1, sizeof(TurretData));
td->shoot_timer = TURRET_SHOOT_CD;
td->fire_flash = 0.0f;
e->data = td;
/* Start with death timer if needed */
e->timer = 0.3f;
animation_set(&e->anim, &anim_turret_idle);
return e;
}
/* ════════════════════════════════════════════════════
* MOVING PLATFORM — travels back and forth,
* player rides on top
* ════════════════════════════════════════════════════ */
static EntityManager *s_mplat_em = NULL;
static void mplat_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
MovingPlatData *md = (MovingPlatData *)self->data;
if (!md) return;
/* Advance phase (sine oscillation) */
float angular_speed = (MPLAT_SPEED / md->range) * 1.0f;
md->phase += angular_speed * dt;
if (md->phase > 2.0f * 3.14159265f)
md->phase -= 2.0f * 3.14159265f;
/* Calculate new position */
float offset = sinf(md->phase) * md->range;
Vec2 new_pos = vec2(
md->origin.x + md->direction.x * offset,
md->origin.y + md->direction.y * offset
);
/* Store velocity for player riding (carry velocity) */
self->body.vel.x = (new_pos.x - self->body.pos.x) / dt;
self->body.vel.y = (new_pos.y - self->body.pos.y) / dt;
self->body.pos = new_pos;
/* Check if player is standing on the platform */
Entity *player = find_player(s_mplat_em);
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
Body *pb = &player->body;
Body *mb = &self->body;
/* Player's feet must be at the platform top (within 3px) */
float player_bottom = pb->pos.y + pb->size.y;
float plat_top = mb->pos.y;
bool horizontally_on = (pb->pos.x + pb->size.x > mb->pos.x + 1.0f) &&
(pb->pos.x < mb->pos.x + mb->size.x - 1.0f);
bool vertically_close = (player_bottom >= plat_top - 3.0f) &&
(player_bottom <= plat_top + 4.0f);
bool falling_or_standing = pb->vel.y >= -1.0f;
if (horizontally_on && vertically_close && falling_or_standing) {
/* Carry the player */
pb->pos.x += self->body.vel.x * dt;
pb->pos.y = plat_top - pb->size.y;
pb->on_ground = true;
if (pb->vel.y > 0) pb->vel.y = 0;
}
}
}
static void mplat_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet) {
SDL_Rect src = animation_current_rect(&self->anim);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = body->pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {60, 110, 165, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void mplat_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void mplat_register(EntityManager *em) {
entity_register(em, ENT_MOVING_PLATFORM, mplat_update, mplat_render, mplat_destroy);
s_mplat_em = em;
}
Entity *mplat_spawn(EntityManager *em, Vec2 pos) {
return mplat_spawn_dir(em, pos, vec2(1.0f, 0.0f)); /* default horizontal */
}
Entity *mplat_spawn_dir(EntityManager *em, Vec2 pos, Vec2 dir) {
Entity *e = entity_spawn(em, ENT_MOVING_PLATFORM, pos);
if (!e) return NULL;
e->body.size = vec2(MPLAT_WIDTH, MPLAT_HEIGHT);
e->body.gravity_scale = 0.0f;
e->health = 9999; /* indestructible */
e->max_health = 9999;
e->flags |= ENTITY_INVINCIBLE;
e->damage = 0;
MovingPlatData *md = calloc(1, sizeof(MovingPlatData));
md->origin = pos;
md->direction = dir;
md->range = MPLAT_RANGE;
md->phase = 0.0f;
e->data = md;
animation_set(&e->anim, &anim_platform);
return e;
}
/* ════════════════════════════════════════════════════
* FLAME VENT — floor grate with timed blue flames
* ════════════════════════════════════════════════════ */
static EntityManager *s_flame_em = NULL;
/* Damage cooldown tracking: use entity timer field */
#define FLAME_DAMAGE_CD 0.5f
static void flame_vent_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
FlameVentData *fd = (FlameVentData *)self->data;
if (!fd) return;
/* Tick the phase timer */
fd->timer -= dt;
if (fd->timer <= 0) {
fd->active = !fd->active;
fd->timer = fd->active ? FLAME_ON_TIME : FLAME_OFF_TIME;
}
/* Warning flicker: 0.3s before flames ignite */
fd->warn_time = 0.0f;
if (!fd->active && fd->timer < 0.3f) {
fd->warn_time = fd->timer;
}
/* Animation */
if (fd->active) {
animation_set(&self->anim, &anim_flame_vent_active);
} else {
animation_set(&self->anim, &anim_flame_vent_idle);
}
animation_update(&self->anim, dt);
/* Damage player when flames are active */
if (fd->active) {
/* Damage cooldown */
if (self->timer > 0) {
self->timer -= dt;
}
Entity *player = find_player(s_flame_em);
if (player && player->active &&
!(player->flags & ENTITY_DEAD) &&
!(player->flags & ENTITY_INVINCIBLE) &&
self->timer <= 0) {
/* Check overlap with flame area (the full sprite, not just base) */
if (physics_overlap(&self->body, &player->body)) {
player->health -= FLAME_DAMAGE;
if (player->health <= 0)
player->flags |= ENTITY_DEAD;
self->timer = FLAME_DAMAGE_CD;
/* Blue flame hit particles */
Vec2 hit_center = vec2(
player->body.pos.x + player->body.size.x * 0.5f,
player->body.pos.y + player->body.size.y * 0.5f
);
SDL_Color flame_col = {68, 136, 255, 255};
particle_emit_spark(hit_center, flame_col);
}
}
/* Emit ambient flame particles */
if ((int)(fd->timer * 10.0f) % 3 == 0) {
Vec2 origin = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + 2.0f
);
ParticleBurst burst = {
.origin = origin,
.count = 2,
.speed_min = 20.0f,
.speed_max = 50.0f,
.life_min = 0.1f,
.life_max = 0.3f,
.size_min = 1.0f,
.size_max = 2.5f,
.spread = 0.4f,
.direction = -1.5708f, /* upward */
.drag = 0.0f,
.gravity_scale = -0.3f, /* float upward */
.color = {100, 150, 255, 255},
.color_vary = true,
};
particle_emit(&burst);
}
}
}
static void flame_vent_render(Entity *self, const Camera *cam) {
(void)cam;
FlameVentData *fd = (FlameVentData *)self->data;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = body->pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
/* Warning flicker: blink the sprite when about to ignite */
if (fd && fd->warn_time > 0) {
int blink = (int)(fd->warn_time * 20.0f) % 2;
spr.alpha = blink ? 180 : 255;
}
renderer_submit(&spr);
} else {
SDL_Color color = fd && fd->active ?
(SDL_Color){68, 136, 255, 255} :
(SDL_Color){80, 80, 80, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void flame_vent_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void flame_vent_register(EntityManager *em) {
entity_register(em, ENT_FLAME_VENT, flame_vent_update, flame_vent_render, flame_vent_destroy);
s_flame_em = em;
}
Entity *flame_vent_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_FLAME_VENT, pos);
if (!e) return NULL;
e->body.size = vec2(FLAME_WIDTH, FLAME_HEIGHT);
e->body.gravity_scale = 0.0f;
e->health = 9999;
e->max_health = 9999;
e->flags |= ENTITY_INVINCIBLE;
e->damage = FLAME_DAMAGE;
FlameVentData *fd = calloc(1, sizeof(FlameVentData));
fd->active = false;
fd->timer = FLAME_OFF_TIME;
fd->warn_time = 0.0f;
e->data = fd;
e->timer = 0.0f; /* damage cooldown */
animation_set(&e->anim, &anim_flame_vent_idle);
return e;
}
/* ════════════════════════════════════════════════════
* FORCE FIELD — energy barrier that toggles on/off
* ════════════════════════════════════════════════════ */
static EntityManager *s_ffield_em = NULL;
static void force_field_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
ForceFieldData *fd = (ForceFieldData *)self->data;
if (!fd) return;
/* Tick the phase timer */
fd->timer -= dt;
if (fd->timer <= 0) {
fd->active = !fd->active;
fd->timer = fd->active ? FFIELD_ON_TIME : FFIELD_OFF_TIME;
}
/* Animation */
if (fd->active) {
animation_set(&self->anim, &anim_force_field_on);
} else {
animation_set(&self->anim, &anim_force_field_off);
}
animation_update(&self->anim, dt);
/* When active: block player and deal damage */
if (fd->active) {
Entity *player = find_player(s_ffield_em);
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
Body *pb = &player->body;
Body *fb = &self->body;
if (physics_overlap(pb, fb)) {
/* Push player out of the force field */
float player_cx = pb->pos.x + pb->size.x * 0.5f;
float field_cx = fb->pos.x + fb->size.x * 0.5f;
if (player_cx < field_cx) {
/* Push left */
pb->pos.x = fb->pos.x - pb->size.x;
if (pb->vel.x > 0) pb->vel.x = 0;
} else {
/* Push right */
pb->pos.x = fb->pos.x + fb->size.x;
if (pb->vel.x < 0) pb->vel.x = 0;
}
/* Damage with cooldown */
if (!(player->flags & ENTITY_INVINCIBLE) && self->timer <= 0) {
player->health -= FFIELD_DAMAGE;
if (player->health <= 0)
player->flags |= ENTITY_DEAD;
self->timer = 0.8f; /* damage cooldown */
/* Electric zap particles */
Vec2 zap_pos = vec2(
fb->pos.x + fb->size.x * 0.5f,
player->body.pos.y + player->body.size.y * 0.5f
);
SDL_Color zap_col = {136, 170, 255, 255};
particle_emit_spark(zap_pos, zap_col);
}
}
}
/* Emit shimmer particles */
if ((int)(fd->timer * 8.0f) % 2 == 0) {
float ry = self->body.pos.y + (float)(rand() % (int)self->body.size.y);
Vec2 origin = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
ry
);
ParticleBurst burst = {
.origin = origin,
.count = 1,
.speed_min = 5.0f,
.speed_max = 15.0f,
.life_min = 0.1f,
.life_max = 0.25f,
.size_min = 0.5f,
.size_max = 1.5f,
.spread = 3.14159f,
.direction = 0.0f,
.drag = 0.5f,
.gravity_scale = 0.0f,
.color = {136, 170, 255, 200},
.color_vary = true,
};
particle_emit(&burst);
}
}
/* Damage cooldown */
if (self->timer > 0) {
self->timer -= dt;
}
}
static void force_field_render(Entity *self, const Camera *cam) {
(void)cam;
ForceFieldData *fd = (ForceFieldData *)self->data;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = body->pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = fd && fd->active ? 220 : 100,
};
renderer_submit(&spr);
} else {
SDL_Color color = fd && fd->active ?
(SDL_Color){100, 130, 255, 220} :
(SDL_Color){40, 50, 80, 100};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void force_field_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void force_field_register(EntityManager *em) {
entity_register(em, ENT_FORCE_FIELD, force_field_update, force_field_render, force_field_destroy);
s_ffield_em = em;
}
Entity *force_field_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_FORCE_FIELD, pos);
if (!e) return NULL;
e->body.size = vec2(FFIELD_WIDTH, FFIELD_HEIGHT);
e->body.gravity_scale = 0.0f;
e->health = 9999;
e->max_health = 9999;
e->flags |= ENTITY_INVINCIBLE;
e->damage = FFIELD_DAMAGE;
ForceFieldData *fd = calloc(1, sizeof(ForceFieldData));
fd->active = true;
fd->timer = FFIELD_ON_TIME;
e->data = fd;
e->timer = 0.0f; /* damage cooldown */
animation_set(&e->anim, &anim_force_field_on);
return e;
}

90
src/game/hazards.h Normal file
View File

@@ -0,0 +1,90 @@
#ifndef JNR_HAZARDS_H
#define JNR_HAZARDS_H
#include "engine/entity.h"
#include "engine/camera.h"
#include "engine/tilemap.h"
/* ═══════════════════════════════════════════════════
* TURRET — Wall/floor-mounted gun that shoots at
* the player when in range, on a cooldown timer.
* ═══════════════════════════════════════════════════ */
#define TURRET_WIDTH 14
#define TURRET_HEIGHT 16
#define TURRET_DETECT_RANGE 160.0f /* detection radius in px */
#define TURRET_SHOOT_CD 2.0f /* seconds between shots */
#define TURRET_HEALTH 3
typedef struct TurretData {
float shoot_timer; /* countdown to next shot */
float fire_flash; /* visual: time remaining on flash */
} TurretData;
void turret_register(EntityManager *em);
Entity *turret_spawn(EntityManager *em, Vec2 pos);
/* ═══════════════════════════════════════════════════
* MOVING PLATFORM — Travels back and forth along a
* horizontal or vertical path. Player rides on top.
* ═══════════════════════════════════════════════════ */
#define MPLAT_WIDTH 16
#define MPLAT_HEIGHT 16
#define MPLAT_SPEED 40.0f /* pixels per second */
#define MPLAT_RANGE 80.0f /* total travel distance in px */
typedef struct MovingPlatData {
Vec2 origin; /* center of travel path */
Vec2 direction; /* (1,0) horizontal / (0,1) vert*/
float range; /* half-distance of travel */
float phase; /* current sine phase (radians) */
} MovingPlatData;
void mplat_register(EntityManager *em);
Entity *mplat_spawn(EntityManager *em, Vec2 pos);
/* Spawn with explicit direction: dir (1,0) = horizontal, (0,1) = vertical */
Entity *mplat_spawn_dir(EntityManager *em, Vec2 pos, Vec2 dir);
/* ═══════════════════════════════════════════════════
* FLAME VENT — Floor-mounted grate that periodically
* bursts blue flames upward. Damages on contact
* when flames are active.
* ═══════════════════════════════════════════════════ */
#define FLAME_WIDTH 16
#define FLAME_HEIGHT 16
#define FLAME_ON_TIME 1.5f /* seconds flames are active */
#define FLAME_OFF_TIME 2.0f /* seconds between bursts */
#define FLAME_DAMAGE 1
typedef struct FlameVentData {
float timer; /* counts down in current phase */
bool active; /* true = flames on */
float warn_time; /* brief flicker before igniting*/
} FlameVentData;
void flame_vent_register(EntityManager *em);
Entity *flame_vent_spawn(EntityManager *em, Vec2 pos);
/* ═══════════════════════════════════════════════════
* FORCE FIELD — Energy barrier that toggles on/off
* on a timer. Blocks movement and damages when
* active, passable when off.
* ═══════════════════════════════════════════════════ */
#define FFIELD_WIDTH 16
#define FFIELD_HEIGHT 16
#define FFIELD_ON_TIME 3.0f /* seconds barrier is active */
#define FFIELD_OFF_TIME 2.0f /* seconds barrier is off */
#define FFIELD_DAMAGE 1
typedef struct ForceFieldData {
float timer; /* counts down in current phase */
bool active; /* true = barrier is on */
} ForceFieldData;
void force_field_register(EntityManager *em);
Entity *force_field_spawn(EntityManager *em, Vec2 pos);
#endif /* JNR_HAZARDS_H */

View File

@@ -2,7 +2,9 @@
#include "game/player.h"
#include "game/enemy.h"
#include "game/projectile.h"
#include "game/hazards.h"
#include "game/sprites.h"
#include "game/entity_registry.h"
#include "engine/core.h"
#include "engine/renderer.h"
#include "engine/physics.h"
@@ -19,9 +21,8 @@ static Sound s_sfx_hit;
static Sound s_sfx_enemy_death;
static bool s_sfx_loaded = false;
bool level_load(Level *level, const char *path) {
memset(level, 0, sizeof(Level));
/* ── Shared level setup (after tilemap is ready) ─── */
static bool level_setup(Level *level) {
/* Initialize subsystems */
entity_manager_init(&level->entities);
camera_init(&level->camera, SCREEN_WIDTH, SCREEN_HEIGHT);
@@ -40,17 +41,8 @@ bool level_load(Level *level, const char *path) {
fprintf(stderr, "Warning: failed to generate spritesheet\n");
}
/* Register entity types */
player_register(&level->entities);
player_set_entity_manager(&level->entities);
grunt_register(&level->entities);
flyer_register(&level->entities);
projectile_register(&level->entities);
/* Load tilemap */
if (!tilemap_load(&level->map, path, g_engine.renderer)) {
return false;
}
/* Register all entity types via the central registry */
entity_registry_init(&level->entities);
/* Apply level gravity (0 = use default) */
if (level->map.gravity > 0) {
@@ -75,11 +67,19 @@ bool level_load(Level *level, const char *path) {
if (near_tex) parallax_set_near(&level->parallax, near_tex, 0.15f, 0.10f);
}
/* Generate procedural backgrounds for any layers not loaded from file */
if (!level->parallax.far_layer.active) {
parallax_generate_stars(&level->parallax, g_engine.renderer);
}
if (!level->parallax.near_layer.active) {
parallax_generate_nebula(&level->parallax, g_engine.renderer);
if (!level->parallax.far_layer.active && !level->parallax.near_layer.active
&& level->map.parallax_style != 0) {
/* Use themed parallax when a style is specified */
parallax_generate_themed(&level->parallax, g_engine.renderer,
(ParallaxStyle)level->map.parallax_style);
} else {
/* Default: generic stars + nebula */
if (!level->parallax.far_layer.active) {
parallax_generate_stars(&level->parallax, g_engine.renderer);
}
if (!level->parallax.near_layer.active) {
parallax_generate_nebula(&level->parallax, g_engine.renderer);
}
}
/* Set camera bounds to level size */
@@ -94,18 +94,11 @@ bool level_load(Level *level, const char *path) {
return false;
}
/* Spawn entities from level data */
/* Spawn entities from level data (via registry) */
for (int i = 0; i < level->map.entity_spawn_count; i++) {
EntitySpawn *es = &level->map.entity_spawns[i];
Vec2 pos = vec2(es->x, es->y);
if (strcmp(es->type_name, "grunt") == 0) {
grunt_spawn(&level->entities, pos);
} else if (strcmp(es->type_name, "flyer") == 0) {
flyer_spawn(&level->entities, pos);
} else {
fprintf(stderr, "Unknown entity type: %s\n", es->type_name);
}
entity_registry_spawn(&level->entities, es->type_name, pos);
}
/* Load level music (playback deferred to first update —
@@ -119,6 +112,35 @@ bool level_load(Level *level, const char *path) {
return true;
}
bool level_load(Level *level, const char *path) {
memset(level, 0, sizeof(Level));
/* Load tilemap from file */
if (!tilemap_load(&level->map, path, g_engine.renderer)) {
return false;
}
return level_setup(level);
}
bool level_load_generated(Level *level, Tilemap *gen_map) {
memset(level, 0, sizeof(Level));
/* Take ownership of the generated tilemap */
level->map = *gen_map;
memset(gen_map, 0, sizeof(Tilemap)); /* prevent double-free */
/* Load tileset texture (the generator doesn't do this) */
level->map.tileset = assets_get_texture("assets/tiles/tileset.png");
if (level->map.tileset) {
int tex_w;
SDL_QueryTexture(level->map.tileset, NULL, NULL, &tex_w, NULL);
level->map.tileset_cols = tex_w / TILE_SIZE;
}
return level_setup(level);
}
/* ── Collision handling ──────────────────────────── */
/* Forward declaration for shake access */
@@ -139,6 +161,8 @@ static void damage_entity(Entity *target, int damage) {
death_color = (SDL_Color){200, 60, 60, 255}; /* red debris */
} else if (target->type == ENT_ENEMY_FLYER) {
death_color = (SDL_Color){140, 80, 200, 255}; /* purple puff */
} else if (target->type == ENT_TURRET) {
death_color = (SDL_Color){160, 160, 160, 255}; /* metal scraps */
} else {
death_color = (SDL_Color){200, 200, 200, 255}; /* grey */
}
@@ -179,7 +203,8 @@ static void damage_player(Entity *player, int damage, Entity *source) {
}
static bool is_enemy(const Entity *e) {
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER;
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER ||
e->type == ENT_TURRET;
}
static void handle_collisions(EntityManager *em) {
@@ -392,6 +417,11 @@ void level_render(Level *level, float interpolation) {
void level_free(Level *level) {
audio_stop_music();
/* Free music handle (prevent leak on reload) */
audio_free_music(&level->music);
level->music_started = false;
entity_manager_clear(&level->entities);
particle_clear();
parallax_free(&level->parallax);

View File

@@ -17,6 +17,7 @@ typedef struct Level {
} Level;
bool level_load(Level *level, const char *path);
bool level_load_generated(Level *level, Tilemap *gen_map);
void level_update(Level *level, float dt);
void level_render(Level *level, float interpolation);
void level_free(Level *level);

903
src/game/levelgen.c Normal file
View File

@@ -0,0 +1,903 @@
#include "game/levelgen.h"
#include "engine/parallax.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ═══════════════════════════════════════════════════
* RNG — simple xorshift32 for deterministic generation
* ═══════════════════════════════════════════════════ */
static uint32_t s_rng_state;
static void rng_seed(uint32_t seed) {
s_rng_state = seed ? seed : (uint32_t)time(NULL);
if (s_rng_state == 0) s_rng_state = 1;
}
static uint32_t rng_next(void) {
uint32_t x = s_rng_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
s_rng_state = x;
return x;
}
/* Random int in [min, max] inclusive */
static int rng_range(int min, int max) {
if (min >= max) return min;
return min + (int)(rng_next() % (uint32_t)(max - min + 1));
}
/* Random float in [0, 1) */
static float rng_float(void) {
return (float)(rng_next() & 0xFFFF) / 65536.0f;
}
/* ═══════════════════════════════════════════════════
* Tile IDs used in generation
* (must match TILEDEF entries we set up)
* ═══════════════════════════════════════════════════ */
#define TILE_EMPTY 0
#define TILE_SOLID_1 1 /* main ground/wall tile */
#define TILE_SOLID_2 2 /* variant solid tile */
#define TILE_SOLID_3 3 /* variant solid tile */
#define TILE_PLAT 4 /* one-way platform */
/* ═══════════════════════════════════════════════════
* Segment types and templates
*
* Each segment is a rectangular section of tiles
* with a fixed height (the full level height) and
* a variable width. Segments are stitched left-to-right.
*
* The level has a standard height of 23 tiles
* (matching level01 — fits ~1.0 screen vertically).
* Ground is at row 20-22 (3 tiles thick).
* ═══════════════════════════════════════════════════ */
#define SEG_HEIGHT 23
#define GROUND_ROW 20 /* first ground row */
#define FLOOR_ROWS 3 /* rows 20, 21, 22 */
typedef enum SegmentType {
SEG_FLAT, /* flat ground, maybe some platforms above */
SEG_PIT, /* gap in the ground — must jump across */
SEG_PLATFORMS, /* vertical platforming section */
SEG_CORRIDOR, /* walled corridor with ceiling */
SEG_ARENA, /* open area, wider, good for combat */
SEG_SHAFT, /* vertical shaft with platforms inside */
SEG_TRANSITION, /* doorway/airlock between themes */
SEG_TYPE_COUNT
} SegmentType;
/* ═══════════════════════════════════════════════════
* Tile placement helpers
* ═══════════════════════════════════════════════════ */
static void set_tile(uint16_t *layer, int map_w, int x, int y, uint16_t id) {
if (x >= 0 && x < map_w && y >= 0 && y < SEG_HEIGHT) {
layer[y * map_w + x] = id;
}
}
static void fill_rect(uint16_t *layer, int map_w,
int x0, int y0, int x1, int y1, uint16_t id) {
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
set_tile(layer, map_w, x, y, id);
}
}
}
static void fill_ground(uint16_t *layer, int map_w, int x0, int x1) {
fill_rect(layer, map_w, x0, GROUND_ROW, x1, SEG_HEIGHT - 1, TILE_SOLID_1);
}
/* Add a random solid variant tile to break up visual monotony */
static uint16_t random_solid(void) {
int r = rng_range(0, 5);
if (r == 0) return TILE_SOLID_2;
if (r == 1) return TILE_SOLID_3;
return TILE_SOLID_1;
}
/* ═══════════════════════════════════════════════════
* Entity spawn helpers
* ═══════════════════════════════════════════════════ */
static void add_entity(Tilemap *map, const char *type, int tile_x, int tile_y) {
if (map->entity_spawn_count >= MAX_ENTITY_SPAWNS) return;
EntitySpawn *es = &map->entity_spawns[map->entity_spawn_count];
strncpy(es->type_name, type, 31);
es->type_name[31] = '\0';
es->x = (float)(tile_x * TILE_SIZE);
es->y = (float)(tile_y * TILE_SIZE);
map->entity_spawn_count++;
}
/* ═══════════════════════════════════════════════════
* Segment generators
*
* Each writes tiles into the collision layer starting
* at column offset `x0` for `width` columns.
* Returns the effective ground connectivity at the
* right edge (row of the topmost ground tile, or -1
* for no ground at the edge).
* ═══════════════════════════════════════════════════ */
/* SEG_FLAT: solid ground, optionally with platforms and enemies */
static void gen_flat(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
(void)theme;
uint16_t *col = map->collision_layer;
int mw = map->width;
fill_ground(col, mw, x0, x0 + w - 1);
/* Random platforms above */
int num_plats = rng_range(0, 2);
for (int i = 0; i < num_plats; i++) {
int px = x0 + rng_range(1, w - 5);
int py = rng_range(13, 17);
int pw = rng_range(3, 5);
for (int j = 0; j < pw && px + j < x0 + w; j++) {
set_tile(col, mw, px + j, py, TILE_PLAT);
}
/* Maybe a grunt on the platform */
if (difficulty > 0.3f && rng_float() < difficulty * 0.5f) {
add_entity(map, "grunt", px + pw / 2, py - 1);
}
}
/* Ground enemies */
if (rng_float() < 0.4f + difficulty * 0.4f) {
add_entity(map, "grunt", x0 + rng_range(2, w - 3), GROUND_ROW - 1);
}
}
/* SEG_PIT: gap in the ground the player must jump */
static void gen_pit(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
int pit_start = rng_range(3, 5);
int pit_width = rng_range(3, 5 + (int)(difficulty * 3));
if (pit_width > w - 4) pit_width = w - 4;
int pit_end = pit_start + pit_width;
/* Ground before pit */
fill_ground(col, mw, x0, x0 + pit_start - 1);
/* Ground after pit */
if (x0 + pit_end < x0 + w) {
fill_ground(col, mw, x0 + pit_end, x0 + w - 1);
}
/* Hazard at pit bottom — theme dependent */
if (difficulty > 0.2f && rng_float() < 0.5f) {
int fx = x0 + pit_start + pit_width / 2;
if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) {
/* Flame vents: natural hazard (surface) or industrial (base) */
add_entity(map, "flame_vent", fx, GROUND_ROW);
fill_rect(col, mw, fx - 1, SEG_HEIGHT - 2, fx + 1, SEG_HEIGHT - 1, TILE_SOLID_1);
} else {
/* Space station: force field across the pit */
add_entity(map, "force_field", fx, GROUND_ROW - 2);
}
}
/* Stepping stones in wider pits */
if (pit_width >= 5) {
int sx = x0 + pit_start + pit_width / 2;
if (theme == THEME_SPACE_STATION && rng_float() < 0.6f) {
/* Moving platform instead of static stepping stones */
add_entity(map, "platform", sx, GROUND_ROW - 2);
} else {
set_tile(col, mw, sx, GROUND_ROW - 1, TILE_PLAT);
set_tile(col, mw, sx + 1, GROUND_ROW - 1, TILE_PLAT);
}
}
}
/* SEG_PLATFORMS: vertical platforming section */
static void gen_platforms(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
/* Ground at bottom */
fill_ground(col, mw, x0, x0 + w - 1);
/* Stack of platforms ascending */
int num_plats = rng_range(3, 5);
/* Space station: more platforms, low gravity makes climbing easier */
if (theme == THEME_SPACE_STATION) num_plats += 1;
for (int i = 0; i < num_plats; i++) {
int py = GROUND_ROW - 3 - i * 3;
if (py < 3) break;
int px = x0 + rng_range(1, w - 4);
int pw = rng_range(2, 4);
for (int j = 0; j < pw && px + j < x0 + w; j++) {
set_tile(col, mw, px + j, py, TILE_PLAT);
}
}
/* Moving platform — more common on stations, less on surface */
float plat_chance = (theme == THEME_SPACE_STATION) ? 0.8f :
(theme == THEME_PLANET_SURFACE) ? 0.3f : 0.5f;
if (rng_float() < plat_chance) {
int py = rng_range(10, 15);
bool vertical = (theme == THEME_SPACE_STATION && rng_float() < 0.5f);
add_entity(map, vertical ? "platform_v" : "platform", x0 + w / 2, py);
}
/* Aerial threat */
if (difficulty > 0.3f && rng_float() < difficulty) {
if (theme == THEME_PLANET_SURFACE) {
/* Surface: flyers are alien wildlife */
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
} else if (theme == THEME_SPACE_STATION) {
/* Station: turret mounted high up or flyer drone */
if (rng_float() < 0.5f) {
add_entity(map, "turret", x0 + rng_range(2, w - 3), rng_range(4, 8));
} else {
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
}
} else {
/* Base: mix */
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
}
}
}
/* SEG_CORRIDOR: walled section with ceiling */
static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
int ceil_row = rng_range(12, 15);
/* Ground */
fill_ground(col, mw, x0, x0 + w - 1);
/* Ceiling */
fill_rect(col, mw, x0, ceil_row - 2, x0 + w - 1, ceil_row, TILE_SOLID_1);
/* Walls at edges (short, just to frame) */
fill_rect(col, mw, x0, ceil_row, x0, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, x0 + w - 1, ceil_row, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1);
/* Opening in left wall (1 tile above ground to enter) */
set_tile(col, mw, x0, GROUND_ROW - 1, TILE_EMPTY);
set_tile(col, mw, x0, GROUND_ROW - 2, TILE_EMPTY);
set_tile(col, mw, x0, GROUND_ROW - 3, TILE_EMPTY);
/* Opening in right wall */
set_tile(col, mw, x0 + w - 1, GROUND_ROW - 1, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, GROUND_ROW - 2, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, GROUND_ROW - 3, TILE_EMPTY);
/* Theme-dependent corridor hazards */
if (theme == THEME_PLANET_BASE || theme == THEME_SPACE_STATION) {
/* Tech environments: turrets and force fields */
if (difficulty > 0.2f && rng_float() < 0.7f) {
add_entity(map, "turret", x0 + w / 2, ceil_row + 1);
}
if (difficulty > 0.4f && rng_float() < 0.5f) {
add_entity(map, "force_field", x0 + w / 2, GROUND_ROW - 1);
}
} else {
/* Planet surface: flame vents leak through the floor */
if (difficulty > 0.3f && rng_float() < 0.5f) {
add_entity(map, "flame_vent", x0 + rng_range(2, w - 3), GROUND_ROW - 1);
}
/* Rare turret even on surface (crashed tech, scavenged) */
if (difficulty > 0.5f && rng_float() < 0.3f) {
add_entity(map, "turret", x0 + w / 2, ceil_row + 1);
}
}
/* Grunt patrol inside (all themes) */
if (rng_float() < 0.5f + difficulty * 0.3f) {
add_entity(map, "grunt", x0 + rng_range(2, w - 3), GROUND_ROW - 1);
}
}
/* SEG_ARENA: wide open area, multiple enemies */
static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
fill_ground(col, mw, x0, x0 + w - 1);
/* Raised platforms on sides */
int plat_h = rng_range(16, 18);
fill_rect(col, mw, x0, plat_h, x0 + 2, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, x0 + w - 3, plat_h, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1);
/* Central platform */
int cp_y = rng_range(14, 16);
int cp_x = x0 + w / 2 - 2;
for (int j = 0; j < 4; j++) {
set_tile(col, mw, cp_x + j, cp_y, TILE_PLAT);
}
/* Multiple enemies — composition depends on theme */
int num_enemies = 1 + (int)(difficulty * 3);
for (int i = 0; i < num_enemies; i++) {
float r = rng_float();
if (theme == THEME_PLANET_SURFACE) {
/* Alien wildlife: more grunts, some flyers */
if (r < 0.65f)
add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1);
else
add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(8, 14));
} else if (theme == THEME_SPACE_STATION) {
/* Station security: more flyers (drones), some grunts */
if (r < 0.35f)
add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1);
else
add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(8, 14));
} else {
/* Base: balanced mix */
if (r < 0.5f)
add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1);
else
add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(8, 14));
}
}
/* Theme-dependent arena hazard */
if (difficulty > 0.5f && rng_float() < 0.7f) {
int side = rng_range(0, 1);
int tx = side ? x0 + w - 2 : x0 + 1;
if (theme == THEME_PLANET_SURFACE) {
/* Surface: flame vents on the arena floor */
add_entity(map, "flame_vent", x0 + w / 2, GROUND_ROW - 1);
} else {
/* Base/Station: turret on elevated ledge */
add_entity(map, "turret", tx, plat_h - 1);
}
}
}
/* SEG_SHAFT: vertical shaft with platforms to climb */
static void gen_shaft(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
/* Ground at bottom */
fill_ground(col, mw, x0, x0 + w - 1);
/* Walls on both sides forming a shaft */
int shaft_left = x0 + 1;
int shaft_right = x0 + w - 2;
fill_rect(col, mw, x0, 3, x0, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, x0 + w - 1, 3, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1);
/* Opening at top */
set_tile(col, mw, x0, 3, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, 3, TILE_EMPTY);
/* Openings at bottom to enter */
for (int r = GROUND_ROW - 3; r < GROUND_ROW; r++) {
set_tile(col, mw, x0, r, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, r, TILE_EMPTY);
}
/* Alternating platforms up the shaft */
int inner_w = shaft_right - shaft_left + 1;
for (int i = 0; i < 5; i++) {
int py = GROUND_ROW - 3 - i * 3;
if (py < 4) break;
bool left_side = (i % 2 == 0);
int px = left_side ? shaft_left : shaft_right - 2;
int pw = (inner_w > 4) ? 3 : 2;
for (int j = 0; j < pw; j++) {
set_tile(col, mw, px + j, py, TILE_PLAT);
}
}
/* Vertical moving platform — very common in stations */
float vplat_chance = (theme == THEME_SPACE_STATION) ? 0.8f : 0.4f;
if (rng_float() < vplat_chance) {
add_entity(map, "platform_v", x0 + w / 2, 10);
}
/* Bottom hazard — theme dependent */
if (difficulty > 0.3f && rng_float() < 0.5f) {
if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) {
add_entity(map, "flame_vent", x0 + w / 2, GROUND_ROW - 1);
} else {
/* Station: force field at bottom of shaft */
add_entity(map, "force_field", x0 + w / 2, GROUND_ROW - 2);
}
}
/* Aerial threat in shaft */
if (difficulty > 0.4f && rng_float() < difficulty) {
if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) {
/* Station: turret on shaft wall */
int side = rng_range(0, 1);
int turret_x = side ? x0 + 1 : x0 + w - 2;
add_entity(map, "turret", turret_x, rng_range(8, 14));
} else {
add_entity(map, "flyer", x0 + w / 2, rng_range(6, 12));
}
}
}
/* ═══════════════════════════════════════════════════
* Segment type selection based on theme
* ═══════════════════════════════════════════════════ */
static SegmentType pick_segment_type(LevelTheme theme, int index, int total) {
/* First segment is always flat (safe start) */
if (index == 0) return SEG_FLAT;
/* Last segment tends to be an arena (climactic) */
if (index == total - 1 && rng_float() < 0.6f) return SEG_ARENA;
/* Theme biases */
float r = rng_float();
switch (theme) {
case THEME_PLANET_SURFACE:
/* Open terrain: lots of flat ground, pits, arenas.
Harsh alien landscape — wide and horizontal. */
if (r < 0.25f) return SEG_FLAT;
if (r < 0.50f) return SEG_PIT;
if (r < 0.70f) return SEG_ARENA;
if (r < 0.85f) return SEG_PLATFORMS;
return SEG_SHAFT;
case THEME_PLANET_BASE:
/* Research outpost: corridors, shafts, structured.
Indoor military feel — walled and enclosed. */
if (r < 0.30f) return SEG_CORRIDOR;
if (r < 0.50f) return SEG_SHAFT;
if (r < 0.65f) return SEG_ARENA;
if (r < 0.80f) return SEG_FLAT;
if (r < 0.90f) return SEG_PLATFORMS;
return SEG_PIT;
case THEME_SPACE_STATION:
/* Orbital station: shafts, platforms, corridors.
Vertical design, low gravity — lots of verticality. */
if (r < 0.25f) return SEG_SHAFT;
if (r < 0.45f) return SEG_PLATFORMS;
if (r < 0.60f) return SEG_CORRIDOR;
if (r < 0.75f) return SEG_ARENA;
if (r < 0.90f) return SEG_FLAT;
return SEG_PIT;
default:
return (SegmentType)rng_range(0, SEG_TYPE_COUNT - 1);
}
}
/* Width for each segment type */
static int segment_width(SegmentType type) {
switch (type) {
case SEG_FLAT: return rng_range(8, 14);
case SEG_PIT: return rng_range(10, 14);
case SEG_PLATFORMS: return rng_range(10, 14);
case SEG_CORRIDOR: return rng_range(10, 16);
case SEG_ARENA: return rng_range(14, 20);
case SEG_SHAFT: return rng_range(6, 10);
case SEG_TRANSITION: return 6; /* fixed: doorway/airlock */
default: return 10;
}
}
/* ═══════════════════════════════════════════════════
* SEG_TRANSITION — doorway/airlock between themes
*
* A narrow walled passage with a doorway frame.
* Visually signals the environment change.
* ═══════════════════════════════════════════════════ */
static void gen_transition(Tilemap *map, int x0, int w,
float difficulty, LevelTheme from, LevelTheme to) {
(void)difficulty;
uint16_t *col = map->collision_layer;
int mw = map->width;
/* ── Ground: solid floor across the whole transition ── */
fill_ground(col, mw, x0, x0 + w - 1);
/* ── Airlock structure ──
* Layout (6 tiles wide):
* col 0: outer wall (left bulkhead)
* col 1: inner left frame
* col 2-3: airlock chamber (open space)
* col 4: inner right frame
* col 5: outer wall (right bulkhead)
*
* Rows 4-6: thick ceiling / airlock header
* Rows 7-8: door frame top (inner columns only)
* Rows 9-18: open passageway
* Row 19: floor level (ground starts at 20)
*/
int left = x0;
int right = x0 + w - 1;
/* Outer bulkhead walls — full height from top to ground */
fill_rect(col, mw, left, 0, left, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, right, 0, right, GROUND_ROW - 1, TILE_SOLID_1);
/* Thick ceiling header (airlock hull) */
fill_rect(col, mw, left, 4, right, 7, TILE_SOLID_2);
/* Inner frame columns — pillars flanking the doorway */
fill_rect(col, mw, left + 1, 8, left + 1, GROUND_ROW - 1, TILE_SOLID_2);
fill_rect(col, mw, right - 1, 8, right - 1, GROUND_ROW - 1, TILE_SOLID_2);
/* Clear the inner chamber (passable area between pillars) */
for (int y = 8; y < GROUND_ROW; y++) {
for (int x = left + 2; x <= right - 2; x++) {
set_tile(col, mw, x, y, TILE_EMPTY);
}
}
/* Door openings in the outer walls (player entry/exit) */
for (int y = GROUND_ROW - 4; y < GROUND_ROW; y++) {
set_tile(col, mw, left, y, TILE_EMPTY);
set_tile(col, mw, right, y, TILE_EMPTY);
}
/* Floor plating inside the airlock — one-way platform */
for (int x = left + 2; x <= right - 2; x++) {
set_tile(col, mw, x, GROUND_ROW - 1, TILE_PLAT);
}
/* ── Hazards inside the airlock ── */
/* Force field barrier in the doorway when entering a tech zone */
if ((to == THEME_PLANET_BASE || to == THEME_SPACE_STATION) &&
from == THEME_PLANET_SURFACE) {
add_entity(map, "force_field", x0 + w / 2, 14);
}
/* Force field when transitioning between base and station too */
if ((from == THEME_PLANET_BASE && to == THEME_SPACE_STATION) ||
(from == THEME_SPACE_STATION && to == THEME_PLANET_BASE)) {
add_entity(map, "force_field", x0 + w / 2, 13);
}
}
/* ═══════════════════════════════════════════════════
* Theme sequence helpers
* ═══════════════════════════════════════════════════ */
/* Get the theme for a given segment index from the config */
static LevelTheme theme_for_segment(const LevelGenConfig *config, int seg_idx) {
if (config->theme_count <= 0) return config->themes[0];
if (seg_idx >= config->theme_count) return config->themes[config->theme_count - 1];
return config->themes[seg_idx];
}
/* Get gravity for a theme.
* Surface: low gravity alien world — floaty jumps.
* Base: normal Earth-like artificial gravity.
* Station: heavy centrifugal / mag-lock gravity. */
static float gravity_for_theme(LevelTheme theme) {
switch (theme) {
case THEME_PLANET_SURFACE: return 400.0f;
case THEME_PLANET_BASE: return 600.0f;
case THEME_SPACE_STATION: return 750.0f;
default: return 600.0f;
}
}
/* Get background color for a theme */
static SDL_Color bg_color_for_theme(LevelTheme theme) {
switch (theme) {
case THEME_PLANET_SURFACE: return (SDL_Color){12, 8, 20, 255};
case THEME_PLANET_BASE: return (SDL_Color){10, 14, 22, 255};
case THEME_SPACE_STATION: return (SDL_Color){5, 5, 18, 255};
default: return (SDL_Color){15, 15, 30, 255};
}
}
/* Map theme to parallax visual style */
static ParallaxStyle parallax_style_for_theme(LevelTheme theme) {
switch (theme) {
case THEME_PLANET_SURFACE: return PARALLAX_STYLE_ALIEN_SKY;
case THEME_PLANET_BASE: return PARALLAX_STYLE_INTERIOR;
case THEME_SPACE_STATION: return PARALLAX_STYLE_DEEP_SPACE;
default: return PARALLAX_STYLE_DEFAULT;
}
}
static const char *theme_label(LevelTheme t) {
switch (t) {
case THEME_PLANET_SURFACE: return "surface";
case THEME_PLANET_BASE: return "base";
case THEME_SPACE_STATION: return "station";
default: return "?";
}
}
/* ═══════════════════════════════════════════════════
* Background decoration (bg_layer)
* ═══════════════════════════════════════════════════ */
static void gen_bg_decoration(Tilemap *map) {
/* Scatter some background tiles for visual depth */
uint16_t *bg = map->bg_layer;
int mw = map->width;
for (int y = 0; y < SEG_HEIGHT - FLOOR_ROWS; y++) {
for (int x = 0; x < mw; x++) {
/* Only place bg tiles where collision is empty */
if (map->collision_layer[y * mw + x] != TILE_EMPTY) continue;
/* Sparse random decoration */
if (rng_float() < 0.02f) {
bg[y * mw + x] = (uint16_t)rng_range(2, 3);
}
}
}
}
/* ═══════════════════════════════════════════════════
* Main generator
* ═══════════════════════════════════════════════════ */
LevelGenConfig levelgen_default_config(void) {
LevelGenConfig config = {
.seed = 0,
.num_segments = 5,
.difficulty = 0.5f,
.gravity = 0, /* 0 = let theme decide */
.theme_count = 1,
};
config.themes[0] = THEME_SPACE_STATION;
return config;
}
bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
if (!map || !config) return false;
rng_seed(config->seed);
int num_segs = config->num_segments;
if (num_segs < 2) num_segs = 2;
if (num_segs > 10) num_segs = 10;
/* ── Phase 1: decide segment types and widths ── */
/* We may insert transition segments between theme boundaries,
* so the final count can be larger than num_segs.
* Max: num_segs content + (num_segs-1) transitions = 2*num_segs-1 */
#define MAX_FINAL_SEGS 20
SegmentType seg_types[MAX_FINAL_SEGS];
int seg_widths[MAX_FINAL_SEGS];
LevelTheme seg_themes[MAX_FINAL_SEGS]; /* per-segment theme */
LevelTheme seg_from[MAX_FINAL_SEGS]; /* for transitions: source theme */
int final_seg_count = 0;
int total_width = 0;
for (int i = 0; i < num_segs; i++) {
LevelTheme t = theme_for_segment(config, i);
/* Insert a transition segment at theme boundaries */
if (i > 0) {
LevelTheme prev_t = theme_for_segment(config, i - 1);
if (t != prev_t && final_seg_count < MAX_FINAL_SEGS) {
seg_types[final_seg_count] = SEG_TRANSITION;
seg_widths[final_seg_count] = segment_width(SEG_TRANSITION);
seg_themes[final_seg_count] = t; /* entering new theme */
seg_from[final_seg_count] = prev_t; /* leaving old theme */
total_width += seg_widths[final_seg_count];
final_seg_count++;
}
}
if (final_seg_count >= MAX_FINAL_SEGS) break;
seg_types[final_seg_count] = pick_segment_type(t, i, num_segs);
seg_widths[final_seg_count] = segment_width(seg_types[final_seg_count]);
seg_themes[final_seg_count] = t;
seg_from[final_seg_count] = t; /* not a transition */
total_width += seg_widths[final_seg_count];
final_seg_count++;
}
num_segs = final_seg_count;
/* Add 2-tile buffer on each side */
total_width += 4;
/* ── Phase 2: allocate tilemap ── */
memset(map, 0, sizeof(Tilemap));
map->width = total_width;
map->height = SEG_HEIGHT;
int total_tiles = map->width * map->height;
map->collision_layer = calloc(total_tiles, sizeof(uint16_t));
map->bg_layer = calloc(total_tiles, sizeof(uint16_t));
map->fg_layer = calloc(total_tiles, sizeof(uint16_t));
if (!map->collision_layer || !map->bg_layer || !map->fg_layer) {
fprintf(stderr, "levelgen: failed to allocate layers\n");
return false;
}
/* ── Phase 3: set tile definitions ── */
map->tile_defs[1] = (TileDef){0, 0, TILE_SOLID}; /* main solid */
map->tile_defs[2] = (TileDef){1, 0, TILE_SOLID}; /* solid variant */
map->tile_defs[3] = (TileDef){2, 0, TILE_SOLID}; /* solid variant */
map->tile_defs[4] = (TileDef){0, 1, TILE_PLATFORM}; /* one-way */
map->tile_def_count = 5;
/* ── Phase 4: generate segments ── */
int cursor = 2; /* start after left buffer */
/* Left border wall */
fill_rect(map->collision_layer, map->width, 0, 0, 1, SEG_HEIGHT - 1, TILE_SOLID_1);
for (int i = 0; i < num_segs; i++) {
int w = seg_widths[i];
LevelTheme theme = seg_themes[i];
switch (seg_types[i]) {
case SEG_FLAT: gen_flat(map, cursor, w, config->difficulty, theme); break;
case SEG_PIT: gen_pit(map, cursor, w, config->difficulty, theme); break;
case SEG_PLATFORMS: gen_platforms(map, cursor, w, config->difficulty, theme); break;
case SEG_CORRIDOR: gen_corridor(map, cursor, w, config->difficulty, theme); break;
case SEG_ARENA: gen_arena(map, cursor, w, config->difficulty, theme); break;
case SEG_SHAFT: gen_shaft(map, cursor, w, config->difficulty, theme); break;
case SEG_TRANSITION:
gen_transition(map, cursor, w, config->difficulty, seg_from[i], theme);
break;
default: gen_flat(map, cursor, w, config->difficulty, theme); break;
}
cursor += w;
}
/* Right border wall */
fill_rect(map->collision_layer, map->width,
map->width - 2, 0, map->width - 1, SEG_HEIGHT - 1, TILE_SOLID_1);
/* ── Phase 5: add visual variety to solid tiles ── */
for (int y = 0; y < map->height; y++) {
for (int x = 0; x < map->width; x++) {
int idx = y * map->width + x;
if (map->collision_layer[idx] == TILE_SOLID_1) {
/* Only vary interior tiles (not edge tiles) */
bool has_air_neighbor = false;
if (y > 0 && map->collision_layer[(y-1)*map->width+x] == 0) has_air_neighbor = true;
if (!has_air_neighbor) {
map->collision_layer[idx] = random_solid();
}
}
}
}
/* ── Phase 6: background decoration ── */
gen_bg_decoration(map);
/* ── Phase 7: metadata ── */
map->player_spawn = vec2(3.0f * TILE_SIZE, (GROUND_ROW - 2) * TILE_SIZE);
/* Theme-based gravity and atmosphere (uses first theme in sequence) */
LevelTheme primary_theme = config->themes[0];
map->gravity = config->gravity > 0 ? config->gravity : gravity_for_theme(primary_theme);
map->bg_color = bg_color_for_theme(primary_theme);
map->has_bg_color = true;
map->parallax_style = (int)parallax_style_for_theme(primary_theme);
/* Music */
strncpy(map->music_path, "assets/sounds/algardalgar.ogg", ASSET_PATH_MAX - 1);
/* Tileset */
/* NOTE: tileset texture will be loaded by level_load_generated */
/* Segment type names for debug output */
static const char *seg_names[] = {
"flat", "pit", "plat", "corr", "arena", "shaft", "trans"
};
printf("levelgen: generated %dx%d level (%d segments, seed=%u)\n",
map->width, map->height, num_segs, s_rng_state);
printf(" segments:");
for (int i = 0; i < num_segs; i++) {
printf(" %s[%s]", seg_names[seg_types[i]], theme_label(seg_themes[i]));
}
printf("\n");
return true;
}
/* ═══════════════════════════════════════════════════
* Dump to .lvl file format
* ═══════════════════════════════════════════════════ */
bool levelgen_dump_lvl(const Tilemap *map, const char *path) {
if (!map || !path) return false;
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "levelgen: failed to open %s for writing\n", path);
return false;
}
fprintf(f, "# Procedurally generated level\n");
fprintf(f, "# Seed state: %u\n\n", s_rng_state);
fprintf(f, "TILESET assets/tiles/tileset.png\n");
fprintf(f, "SIZE %d %d\n", map->width, map->height);
/* Player spawn in tile coords */
int spawn_tx = (int)(map->player_spawn.x / TILE_SIZE);
int spawn_ty = (int)(map->player_spawn.y / TILE_SIZE);
fprintf(f, "SPAWN %d %d\n", spawn_tx, spawn_ty);
if (map->gravity > 0) {
fprintf(f, "GRAVITY %.0f\n", map->gravity);
}
if (map->has_bg_color) {
fprintf(f, "BG_COLOR %d %d %d\n",
map->bg_color.r, map->bg_color.g, map->bg_color.b);
}
if (map->music_path[0]) {
fprintf(f, "MUSIC %s\n", map->music_path);
}
fprintf(f, "\n");
/* Entity spawns */
for (int i = 0; i < map->entity_spawn_count; i++) {
const EntitySpawn *es = &map->entity_spawns[i];
int tx = (int)(es->x / TILE_SIZE);
int ty = (int)(es->y / TILE_SIZE);
fprintf(f, "ENTITY %s %d %d\n", es->type_name, tx, ty);
}
fprintf(f, "\n");
/* Tile definitions */
for (int id = 1; id < map->tile_def_count; id++) {
const TileDef *td = &map->tile_defs[id];
if (td->flags != 0 || td->tex_x != 0 || td->tex_y != 0) {
fprintf(f, "TILEDEF %d %d %d %u\n", id, td->tex_x, td->tex_y, td->flags);
}
}
fprintf(f, "\n");
/* Collision layer */
fprintf(f, "LAYER collision\n");
for (int y = 0; y < map->height; y++) {
for (int x = 0; x < map->width; x++) {
if (x > 0) fprintf(f, " ");
fprintf(f, "%d", map->collision_layer[y * map->width + x]);
}
fprintf(f, "\n");
}
/* Background layer */
bool has_bg = false;
for (int i = 0; i < map->width * map->height; i++) {
if (map->bg_layer[i] != 0) { has_bg = true; break; }
}
if (has_bg) {
fprintf(f, "\nLAYER bg\n");
for (int y = 0; y < map->height; y++) {
for (int x = 0; x < map->width; x++) {
if (x > 0) fprintf(f, " ");
fprintf(f, "%d", map->bg_layer[y * map->width + x]);
}
fprintf(f, "\n");
}
}
fclose(f);
printf("levelgen: dumped level to %s\n", path);
return true;
}

61
src/game/levelgen.h Normal file
View File

@@ -0,0 +1,61 @@
#ifndef JNR_LEVELGEN_H
#define JNR_LEVELGEN_H
#include "engine/tilemap.h"
#include <stdint.h>
/* ═══════════════════════════════════════════════════
* Procedural Level Generator
*
* Generates a Tilemap directly in memory by stitching
* together segment templates. Supports:
* - Mixed layouts (horizontal, vertical, arena)
* - Enemy and hazard placement
* - Difficulty scaling via seed
* - Dumping to .lvl format for inspection
* ═══════════════════════════════════════════════════ */
/* Level theme — affects layout, hazards, gravity, atmosphere */
typedef enum LevelTheme {
THEME_PLANET_SURFACE, /* alien planet exterior: open terrain, pits,
flame vents, harsh conditions, high gravity */
THEME_PLANET_BASE, /* research outpost on a planet: corridors,
force fields, turrets, medium gravity */
THEME_SPACE_STATION, /* orbital station: shafts, platforms, low
gravity, moving platforms, turrets */
THEME_COUNT
} LevelTheme;
#define MAX_THEME_SEQUENCE 10
/* Generator configuration */
typedef struct LevelGenConfig {
uint32_t seed; /* RNG seed (0 = random from time) */
int num_segments; /* how many segments to stitch (2-10) */
float difficulty; /* 0.0 = easy, 1.0 = hard */
float gravity; /* level gravity override (0 = auto) */
/* Theme sequence: one theme per segment.
* If theme_count == 0, all segments use themes[0].
* If theme_count > 0, segment i uses themes[min(i, theme_count-1)].
* Transition segments are auto-inserted at theme boundaries. */
LevelTheme themes[MAX_THEME_SEQUENCE];
int theme_count; /* number of entries in themes[] */
} LevelGenConfig;
/* Default config for quick generation */
LevelGenConfig levelgen_default_config(void);
/* Generate a level directly into a Tilemap struct.
* The Tilemap will have layers allocated, tile defs set,
* entity spawns populated, and metadata configured.
* Caller must eventually call tilemap_free() on it.
* Returns true on success. */
bool levelgen_generate(Tilemap *map, const LevelGenConfig *config);
/* Dump a generated (or any) Tilemap to a .lvl file.
* Useful for inspecting/editing procedural output.
* Returns true on success. */
bool levelgen_dump_lvl(const Tilemap *map, const char *path);
#endif /* JNR_LEVELGEN_H */

142
src/game/powerup.c Normal file
View File

@@ -0,0 +1,142 @@
#include "game/powerup.h"
#include "game/sprites.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ── Stashed entity manager ────────────────────────── */
static EntityManager *s_em = NULL;
/* ── Constants ─────────────────────────────────────── */
#define BOB_SPEED 3.0f /* sine cycles per second */
#define BOB_HEIGHT 3.0f /* pixels of vertical bob */
#define PICKUP_SIZE 12 /* hitbox size (centered in 16x16) */
/* ── Callbacks ─────────────────────────────────────── */
static void powerup_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
PowerupData *pd = (PowerupData *)self->data;
if (!pd) return;
/* Sine bob animation */
pd->bob_timer += dt;
float bob = sinf(pd->bob_timer * BOB_SPEED * 2.0f * (float)M_PI) * BOB_HEIGHT;
self->body.pos.y = pd->base_pos.y + bob;
/* Gentle sparkle particles every ~0.4s */
if ((int)(pd->bob_timer * 10.0f) % 4 == 0) {
SDL_Color color;
switch (pd->kind) {
case POWERUP_HEALTH: color = (SDL_Color){220, 50, 50, 200}; break;
case POWERUP_JETPACK: color = (SDL_Color){255, 180, 50, 200}; break;
case POWERUP_DRONE: color = (SDL_Color){50, 200, 255, 200}; break;
default: color = (SDL_Color){255, 255, 255, 200}; break;
}
Vec2 center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
ParticleBurst b = {
.origin = center,
.count = 1,
.speed_min = 5.0f,
.speed_max = 15.0f,
.life_min = 0.3f,
.life_max = 0.6f,
.size_min = 1.0f,
.size_max = 2.0f,
.spread = (float)M_PI,
.direction = -(float)M_PI / 2.0f, /* upward */
.drag = 1.0f,
.gravity_scale = 0.0f,
.color = color,
.color_vary = true,
};
particle_emit(&b);
}
/* Animation cycling */
animation_update(&self->anim, dt);
}
static void powerup_render(Entity *self, const Camera *cam) {
(void)cam;
if (!g_spritesheet || !self->anim.def) return;
SDL_Rect src = animation_current_rect(&self->anim);
Body *body = &self->body;
/* Center the 16x16 sprite on the smaller hitbox */
float draw_x = body->pos.x + body->size.x * 0.5f - SPRITE_CELL * 0.5f;
float draw_y = body->pos.y + body->size.y * 0.5f - SPRITE_CELL * 0.5f;
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = vec2(draw_x, draw_y),
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
}
static void powerup_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
/* ── Public API ────────────────────────────────────── */
void powerup_register(EntityManager *em) {
entity_register(em, ENT_POWERUP, powerup_update, powerup_render, powerup_destroy);
s_em = em;
}
Entity *powerup_spawn(EntityManager *em, Vec2 pos, PowerupKind kind) {
Entity *e = entity_spawn(em, ENT_POWERUP, pos);
if (!e) return NULL;
e->body.size = vec2(PICKUP_SIZE, PICKUP_SIZE);
e->body.gravity_scale = 0.0f; /* floats in place */
e->health = 1;
e->max_health = 1;
e->damage = 0;
e->flags |= ENTITY_INVINCIBLE; /* can't be shot */
PowerupData *pd = calloc(1, sizeof(PowerupData));
pd->kind = kind;
pd->bob_timer = 0.0f;
pd->base_pos = pos;
e->data = pd;
/* Set animation based on kind */
switch (kind) {
case POWERUP_HEALTH: animation_set(&e->anim, &anim_powerup_health); break;
case POWERUP_JETPACK: animation_set(&e->anim, &anim_powerup_jetpack); break;
case POWERUP_DRONE: animation_set(&e->anim, &anim_powerup_drone); break;
default: animation_set(&e->anim, &anim_powerup_health); break;
}
return e;
}
Entity *powerup_spawn_health(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_HEALTH);
}
Entity *powerup_spawn_jetpack(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_JETPACK);
}
Entity *powerup_spawn_drone(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_DRONE);
}

40
src/game/powerup.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef JNR_POWERUP_H
#define JNR_POWERUP_H
#include "engine/entity.h"
/* ═══════════════════════════════════════════════════
* Powerup Pickups
*
* Small collectible items that grant the player
* an instant benefit on contact:
* - Health: restores 1 HP
* - Jetpack: instantly refills all dash charges
* - Drone: spawns an orbiting combat drone
* ═══════════════════════════════════════════════════ */
typedef enum PowerupKind {
POWERUP_HEALTH,
POWERUP_JETPACK,
POWERUP_DRONE,
POWERUP_KIND_COUNT
} PowerupKind;
typedef struct PowerupData {
PowerupKind kind;
float bob_timer; /* sine bob animation */
Vec2 base_pos; /* original spawn position */
} PowerupData;
/* Register the powerup entity type */
void powerup_register(EntityManager *em);
/* Spawn a specific powerup at a position */
Entity *powerup_spawn(EntityManager *em, Vec2 pos, PowerupKind kind);
/* Convenience spawners for level/levelgen */
Entity *powerup_spawn_health(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_jetpack(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_drone(EntityManager *em, Vec2 pos);
#endif /* JNR_POWERUP_H */

View File

@@ -505,6 +505,361 @@ static const uint32_t enemy_bullet2[16*16] = {
};
/* ── Hazard sprites ─────────────────────────────────── */
/* Turret idle - metallic gun turret mounted on wall/floor */
static const uint32_t turret_idle1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, T, T, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GYD, T, T,
T, T, T, T, GYD, GRY, GYL, GRY, GRY, GYL, GRY, GRY, GRY, GYD, T, T,
T, T, T, GYD, GRY, GRY, GRY, RED, RED, GRY, GRY, GRY, GRY, GRY, GYD, T,
T, T, T, GYD, GRY, GRY, GRY, RDD, RDD, GRY, GRY, GRY, GRY, GRY, GYD, T,
T, T, T, T, GYD, GRY, GYL, GRY, GRY, GYL, GRY, GRY, GRY, GYD, T, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GYD, T, T,
T, T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, T, T, T,
T, T, T, T, T, T, GYD, GRY, GRY, GYD, T, T, T, T, T, T,
T, T, T, T, T, GYD, GRY, GRY, GRY, GRY, GYD, T, T, T, T, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GYD, T, T, T, T,
T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, T, T, T, T,
};
/* Turret fire - barrel flash */
static const uint32_t turret_fire1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, ORG, T, T,
T, T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, YLW, ORG, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GRY, WHT, YLW, T,
T, T, T, T, GYD, GRY, GYL, GRY, GRY, GYL, GRY, GRY, GRY, WHT, YLW, T,
T, T, T, GYD, GRY, GRY, GRY, RED, RED, GRY, GRY, GRY, GRY, YLW, ORG, T,
T, T, T, GYD, GRY, GRY, GRY, RDD, RDD, GRY, GRY, GRY, GRY, YLW, ORG, T,
T, T, T, T, GYD, GRY, GYL, GRY, GRY, GYL, GRY, GRY, GRY, WHT, YLW, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GRY, GRY, WHT, YLW, T,
T, T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, YLW, ORG, T,
T, T, T, T, T, T, GYD, GRY, GRY, GYD, T, T, T, ORG, T, T,
T, T, T, T, T, GYD, GRY, GRY, GRY, GRY, GYD, T, T, T, T, T,
T, T, T, T, GYD, GRY, GRY, GRY, GRY, GRY, GRY, GYD, T, T, T, T,
T, T, T, T, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, T, T, T, T,
};
/* Moving platform - metallic platform with glowing edges */
#define PLT 0x3a6ea5FF /* platform blue */
#define PLD 0x2a5080FF /* platform blue dark */
#define PLL 0x5a9ed5FF /* platform blue light */
static const uint32_t platform1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN, CYN,
PLL, PLL, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLT, PLL, PLL,
PLT, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLT,
PLT, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLD, PLT, PLT, PLT,
PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD, PLD,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Flame vent base - metallic grate on floor */
#define FLB 0x2244aaFF /* flame blue */
#define FLD 0x1a3388FF /* flame blue dark */
#define FLL 0x4488ffFF /* flame blue light */
static const uint32_t flame_vent_idle1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FLB, FLB, T, T, T, T, T, T, T,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
};
/* Flame vent active - blue flames shooting up from grate */
static const uint32_t flame_vent_active1[16*16] = {
T, T, T, T, T, T, FLD, T, T, FLD, T, T, T, T, T, T,
T, T, T, T, T, FLD, FLB, FLD, FLD, FLB, FLD, T, T, T, T, T,
T, T, T, T, FLD, FLB, FLL, FLB, FLB, FLL, FLB, FLD, T, T, T, T,
T, T, T, FLD, FLB, FLL, WHT, FLL, FLL, WHT, FLL, FLB, FLD, T, T, T,
T, T, T, FLB, FLL, WHT, WHT, FLL, FLL, WHT, WHT, FLL, FLB, T, T, T,
T, T, FLD, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, FLD, T, T,
T, T, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, T, T,
T, T, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, T, T,
T, T, FLD, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, FLD, T, T,
T, T, T, FLB, FLL, FLL, WHT, WHT, WHT, WHT, FLL, FLL, FLB, T, T, T,
T, T, T, T, FLB, FLL, FLL, FLB, FLB, FLL, FLL, FLB, T, T, T, T,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
};
/* Flame vent active frame 2 - flame flicker */
static const uint32_t flame_vent_active2[16*16] = {
T, T, T, T, T, T, T, FLD, FLD, T, T, T, T, T, T, T,
T, T, T, T, T, T, FLD, FLB, FLB, FLD, T, T, T, T, T, T,
T, T, T, T, T, FLD, FLB, FLL, FLL, FLB, FLD, T, T, T, T, T,
T, T, T, T, FLD, FLB, FLL, WHT, WHT, FLL, FLB, FLD, T, T, T, T,
T, T, T, FLD, FLB, FLL, WHT, WHT, WHT, WHT, FLL, FLB, FLD, T, T, T,
T, T, T, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, T, T, T,
T, T, FLD, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, FLD, T, T,
T, T, FLB, FLL, WHT, WHT, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLB, T, T,
T, T, FLB, FLL, FLL, WHT, WHT, WHT, WHT, WHT, WHT, FLL, FLL, FLB, T, T,
T, T, T, FLB, FLL, FLL, WHT, WHT, WHT, WHT, FLL, FLL, FLB, T, T, T,
T, T, T, T, FLB, FLB, FLL, FLB, FLB, FLL, FLB, FLB, T, T, T, T,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY,
GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD, GRY, GYD,
GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD, GYD,
};
/* Force field on - vertical energy barrier */
#define FFB 0x4466ffFF /* force field blue */
#define FFD 0x2244ccFF /* force field dark */
#define FFL 0x88aaFFFF /* force field light */
static const uint32_t force_field_on1[16*16] = {
T, T, T, FFD, T, FFD, FFB, FFL, FFL, FFB, FFD, T, FFD, T, T, T,
T, T, FFD, FFB, FFD, FFB, FFL, WHT, WHT, FFL, FFB, FFD, FFB, FFD, T, T,
T, T, T, FFB, FFB, FFL, WHT, WHT, WHT, WHT, FFL, FFB, FFB, T, T, T,
T, T, FFD, FFB, FFL, WHT, WHT, FFL, FFL, WHT, WHT, FFL, FFB, FFD, T, T,
T, T, T, FFB, FFL, WHT, FFL, FFB, FFB, FFL, WHT, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, FFB, FFD, FFD, FFB, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, FFB, FFD, FFB, FFB, FFD, FFB, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFD, FFB, FFL, FFL, FFB, FFD, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFD, FFB, FFL, WHT, WHT, FFL, FFB, FFD, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, WHT, WHT, WHT, WHT, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, WHT, WHT, FFL, FFL, WHT, WHT, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, FFB, FFD, FFD, FFB, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, FFB, FFD, FFB, FFB, FFD, FFB, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFD, FFB, FFL, FFL, FFL, FFL, FFB, FFD, FFB, FFD, T, T,
T, T, T, FFD, T, FFD, FFB, FFL, FFL, FFB, FFD, T, FFD, T, T, T,
T, T, T, T, T, T, FFD, FFD, FFD, FFD, T, T, T, T, T, T,
};
/* Force field on frame 2 - shimmer */
static const uint32_t force_field_on2[16*16] = {
T, T, T, T, T, FFD, FFB, FFB, FFB, FFB, FFD, T, T, T, T, T,
T, T, T, FFD, FFD, FFB, FFL, FFL, FFL, FFL, FFB, FFD, FFD, T, T, T,
T, T, FFD, FFB, FFB, FFL, WHT, FFL, FFL, WHT, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, WHT, FFL, FFB, FFB, FFL, WHT, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, FFB, FFD, FFD, FFB, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, FFB, FFD, FFB, FFB, FFD, FFB, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFD, FFB, FFL, FFL, FFB, FFD, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFD, FFB, FFL, WHT, WHT, FFL, FFB, FFD, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, WHT, WHT, WHT, WHT, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, WHT, FFL, FFB, FFB, FFL, WHT, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFB, FFL, FFB, FFD, FFD, FFB, FFL, FFB, FFB, FFD, T, T,
T, T, T, FFB, FFL, FFB, FFD, FFB, FFB, FFD, FFB, FFL, FFB, T, T, T,
T, T, FFD, FFB, FFD, FFB, FFL, FFL, FFL, FFL, FFB, FFD, FFB, FFD, T, T,
T, T, T, FFD, T, FFD, FFB, FFL, FFL, FFB, FFD, T, FFD, T, T, T,
T, T, T, T, T, T, FFD, FFB, FFB, FFD, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
};
/* Force field off - just dim posts/emitters */
static const uint32_t force_field_off1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, FFD, FFD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Powerup sprites ────────────────────────────────── */
/* Health powerup — red cross / heart icon */
static const uint32_t powerup_health1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, RDL, RDL, T, T, RDL, RDL, T, T, T, T, T,
T, T, T, T, RDL, RED, RED, RDL, RDL, RED, RED, RDL, T, T, T, T,
T, T, T, RDL, RED, RED, RED, RED, RED, RED, RED, RED, RDL, T, T, T,
T, T, T, RDL, RED, RED, WHT, RED, RED, WHT, RED, RED, RDL, T, T, T,
T, T, T, RDL, RED, RED, RED, WHT, WHT, RED, RED, RED, RDL, T, T, T,
T, T, T, RDL, RED, RED, RED, RED, RED, RED, RED, RED, RDL, T, T, T,
T, T, T, T, RDL, RED, RED, RED, RED, RED, RED, RDL, T, T, T, T,
T, T, T, T, T, RDL, RED, RED, RED, RED, RDL, T, T, T, T, T,
T, T, T, T, T, T, RDL, RED, RED, RDL, T, T, T, T, T, T,
T, T, T, T, T, T, T, RDD, RDD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Health powerup frame 2 — slight glow/pulse */
static const uint32_t powerup_health2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, RDL, RDL, T, T, RDL, RDL, T, T, T, T, T,
T, T, T, T, RDL, RED, RED, RDL, RDL, RED, RED, RDL, T, T, T, T,
T, T, T, RDL, RED, RED, RED, RED, RED, RED, RED, RED, RDL, T, T, T,
T, T, RDL, RED, RED, RED, RED, RED, RED, RED, RED, RED, RED, RDL, T, T,
T, T, RDL, RED, RED, RED, WHT, WHT, WHT, WHT, RED, RED, RED, RDL, T, T,
T, T, RDL, RED, RED, WHT, WHT, WHT, WHT, WHT, WHT, RED, RED, RDL, T, T,
T, T, RDL, RED, RED, RED, WHT, WHT, WHT, WHT, RED, RED, RED, RDL, T, T,
T, T, T, RDL, RED, RED, RED, RED, RED, RED, RED, RED, RDL, T, T, T,
T, T, T, T, RDL, RED, RED, RED, RED, RED, RED, RDL, T, T, T, T,
T, T, T, T, T, RDL, RED, RED, RED, RED, RDL, T, T, T, T, T,
T, T, T, T, T, T, RDD, RDD, RDD, RDD, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Jetpack recharge powerup — orange lightning bolt */
static const uint32_t powerup_jetpack1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, ORG, ORG, ORG, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, ORG, YLW, YLW, ORG, T, T, T, T, T, T, T, T,
T, T, T, ORG, YLW, WHT, YLW, ORG, ORG, ORG, ORG, T, T, T, T, T,
T, T, T, ORG, YLW, WHT, WHT, YLW, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, ORG, ORG, YLW, WHT, YLW, ORG, T, T, T, T, T,
T, T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T, T,
T, T, T, T, T, ORG, ORG, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Jetpack powerup frame 2 — brighter glow */
static const uint32_t powerup_jetpack2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, YLW, YLW, YLW, T, T, T, T, T, T,
T, T, T, T, T, T, YLW, WHT, WHT, YLW, T, T, T, T, T, T,
T, T, T, T, T, YLW, WHT, WHT, YLW, T, T, T, T, T, T, T,
T, T, T, T, YLW, WHT, WHT, YLW, T, T, T, T, T, T, T, T,
T, T, T, YLW, WHT, WHT, WHT, YLW, YLW, YLW, YLW, T, T, T, T, T,
T, T, T, YLW, WHT, WHT, WHT, WHT, WHT, WHT, YLW, T, T, T, T, T,
T, T, T, T, YLW, YLW, YLW, WHT, WHT, WHT, YLW, T, T, T, T, T,
T, T, T, T, T, T, T, YLW, WHT, YLW, T, T, T, T, T, T,
T, T, T, T, T, T, YLW, WHT, YLW, T, T, T, T, T, T, T,
T, T, T, T, T, YLW, WHT, YLW, T, T, T, T, T, T, T, T,
T, T, T, T, T, YLW, YLW, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Drone powerup — cyan spinning gear/orb */
static const uint32_t powerup_drone1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, CYD, CYN, CYN, CYN, CYN, CYD, T, T, T, T, T,
T, T, T, T, CYD, CYN, CYN, WHT, WHT, CYN, CYN, CYD, T, T, T, T,
T, T, T, CYD, CYN, CYN, WHT, WHT, WHT, WHT, CYN, CYN, CYD, T, T, T,
T, T, T, CYN, CYN, WHT, WHT, CYN, CYN, WHT, WHT, CYN, CYN, T, T, T,
T, T, CYD, CYN, WHT, WHT, CYN, CYD, CYD, CYN, WHT, WHT, CYN, CYD, T, T,
T, T, CYD, CYN, WHT, WHT, CYN, CYD, CYD, CYN, WHT, WHT, CYN, CYD, T, T,
T, T, T, CYN, CYN, WHT, WHT, CYN, CYN, WHT, WHT, CYN, CYN, T, T, T,
T, T, T, CYD, CYN, CYN, WHT, WHT, WHT, WHT, CYN, CYN, CYD, T, T, T,
T, T, T, T, CYD, CYN, CYN, WHT, WHT, CYN, CYN, CYD, T, T, T, T,
T, T, T, T, T, CYD, CYN, CYN, CYN, CYN, CYD, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Drone powerup frame 2 — rotated highlight */
static const uint32_t powerup_drone2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, CYD, CYN, CYN, CYN, CYN, CYD, T, T, T, T, T,
T, T, T, T, CYD, CYN, WHT, WHT, CYN, CYN, CYN, CYD, T, T, T, T,
T, T, T, CYD, CYN, WHT, WHT, CYN, CYN, CYN, CYN, CYN, CYD, T, T, T,
T, T, T, CYN, WHT, WHT, CYN, CYD, CYD, CYN, CYN, CYN, CYN, T, T, T,
T, T, CYD, CYN, WHT, CYN, CYD, CYD, CYD, CYD, CYN, WHT, CYN, CYD, T, T,
T, T, CYD, CYN, WHT, CYN, CYD, CYD, CYD, CYD, CYN, WHT, CYN, CYD, T, T,
T, T, T, CYN, CYN, CYN, CYN, CYD, CYD, CYN, WHT, WHT, CYN, T, T, T,
T, T, T, CYD, CYN, CYN, CYN, CYN, CYN, WHT, WHT, CYN, CYD, T, T, T,
T, T, T, T, CYD, CYN, CYN, CYN, WHT, WHT, CYN, CYD, T, T, T, T,
T, T, T, T, T, CYD, CYN, CYN, CYN, CYN, CYD, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Drone companion sprite ────────────────────────── */
/* Small hovering drone — metallic with cyan thruster */
static const uint32_t drone_frame1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, GYL, GYL, GYL, GYL, T, T, T, T, T, T,
T, T, T, T, T, GYL, GRY, GRY, GRY, GRY, GYL, T, T, T, T, T,
T, T, T, T, GYL, GRY, GRY, CYN, CYN, GRY, GRY, GYL, T, T, T, T,
T, T, T, T, GYD, GRY, CYN, WHT, WHT, CYN, GRY, GYD, T, T, T, T,
T, T, T, T, GYD, GRY, CYN, WHT, WHT, CYN, GRY, GYD, T, T, T, T,
T, T, T, T, GYL, GRY, GRY, CYN, CYN, GRY, GRY, GYL, T, T, T, T,
T, T, T, T, T, GYD, GRY, GRY, GRY, GRY, GYD, T, T, T, T, T,
T, T, T, T, T, T, GYD, CYD, CYD, GYD, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYN, CYN, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Drone frame 2 — thruster flicker */
static const uint32_t drone_frame2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, GYL, GYL, GYL, GYL, T, T, T, T, T, T,
T, T, T, T, T, GYL, GRY, GRY, GRY, GRY, GYL, T, T, T, T, T,
T, T, T, T, GYL, GRY, GRY, CYN, CYN, GRY, GRY, GYL, T, T, T, T,
T, T, T, T, GYD, GRY, CYN, WHT, WHT, CYN, GRY, GYD, T, T, T, T,
T, T, T, T, GYD, GRY, CYN, WHT, WHT, CYN, GRY, GYD, T, T, T, T,
T, T, T, T, GYL, GRY, GRY, CYN, CYN, GRY, GRY, GYL, T, T, T, T,
T, T, T, T, T, GYD, GRY, GRY, GRY, GRY, GYD, T, T, T, T, T,
T, T, T, T, T, T, CYD, CYN, CYN, CYD, T, T, T, T, T, T,
T, T, T, T, T, T, CYN, WHT, WHT, CYN, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Spritesheet generation ────────────────────────── */
/* All sprite definitions for the sheet - row, column, pixel data */
@@ -551,10 +906,33 @@ static const SpriteDef s_sprite_defs[] = {
{3, 4, impact3},
{3, 5, enemy_bullet1},
{3, 6, enemy_bullet2},
/* Row 4: Hazards */
{4, 0, turret_idle1},
{4, 1, turret_fire1},
{4, 2, platform1},
{4, 3, flame_vent_idle1},
{4, 4, flame_vent_active1},
{4, 5, flame_vent_active2},
{4, 6, force_field_on1},
{4, 7, force_field_on2},
/* Row 5: Hazards continued + Powerups */
{5, 0, force_field_off1},
{5, 1, powerup_health1},
{5, 2, powerup_health2},
{5, 3, powerup_jetpack1},
{5, 4, powerup_jetpack2},
{5, 5, powerup_drone1},
{5, 6, powerup_drone2},
/* Row 6: Drone companion */
{6, 0, drone_frame1},
{6, 1, drone_frame2},
};
#define SHEET_COLS 8
#define SHEET_ROWS 4
#define SHEET_ROWS 7
SDL_Texture *sprites_generate(SDL_Renderer *renderer) {
int w = SHEET_COLS * SPRITE_CELL;
@@ -690,6 +1068,59 @@ static AnimFrame s_enemy_bullet_frames[] = {
FRAME(6, 3, 0.1f),
};
/* Hazard animations */
static AnimFrame s_turret_idle_frames[] = {
FRAME(0, 4, 1.0f),
};
static AnimFrame s_turret_fire_frames[] = {
FRAME(1, 4, 0.15f),
};
static AnimFrame s_platform_frames[] = {
FRAME(2, 4, 1.0f),
};
static AnimFrame s_flame_vent_idle_frames[] = {
FRAME(3, 4, 1.0f),
};
static AnimFrame s_flame_vent_active_frames[] = {
FRAME(4, 4, 0.08f),
FRAME(5, 4, 0.08f),
};
static AnimFrame s_force_field_on_frames[] = {
FRAME(6, 4, 0.15f),
FRAME(7, 4, 0.15f),
};
static AnimFrame s_force_field_off_frames[] = {
FRAME(0, 5, 1.0f),
};
/* Powerups */
static AnimFrame s_powerup_health_frames[] = {
FRAME(1, 5, 0.4f),
FRAME(2, 5, 0.4f),
};
static AnimFrame s_powerup_jetpack_frames[] = {
FRAME(3, 5, 0.3f),
FRAME(4, 5, 0.3f),
};
static AnimFrame s_powerup_drone_frames[] = {
FRAME(5, 5, 0.3f),
FRAME(6, 5, 0.3f),
};
/* Drone companion */
static AnimFrame s_drone_frames[] = {
FRAME(0, 6, 0.2f),
FRAME(1, 6, 0.2f),
};
/* Exported animation definitions */
AnimDef anim_player_idle;
AnimDef anim_player_run;
@@ -708,6 +1139,20 @@ AnimDef anim_bullet;
AnimDef anim_bullet_impact;
AnimDef anim_enemy_bullet;
AnimDef anim_turret_idle;
AnimDef anim_turret_fire;
AnimDef anim_platform;
AnimDef anim_flame_vent_idle;
AnimDef anim_flame_vent_active;
AnimDef anim_force_field_on;
AnimDef anim_force_field_off;
AnimDef anim_powerup_health;
AnimDef anim_powerup_jetpack;
AnimDef anim_powerup_drone;
AnimDef anim_drone;
void sprites_init_anims(void) {
anim_player_idle = (AnimDef){s_player_idle_frames, 2, true, NULL};
anim_player_run = (AnimDef){s_player_run_frames, 4, true, NULL};
@@ -725,4 +1170,18 @@ void sprites_init_anims(void) {
anim_bullet = (AnimDef){s_bullet_frames, 2, true, NULL};
anim_bullet_impact = (AnimDef){s_impact_frames, 3, false, NULL};
anim_enemy_bullet = (AnimDef){s_enemy_bullet_frames, 2, true, NULL};
anim_turret_idle = (AnimDef){s_turret_idle_frames, 1, true, NULL};
anim_turret_fire = (AnimDef){s_turret_fire_frames, 1, false, NULL};
anim_platform = (AnimDef){s_platform_frames, 1, true, NULL};
anim_flame_vent_idle = (AnimDef){s_flame_vent_idle_frames, 1, true, NULL};
anim_flame_vent_active = (AnimDef){s_flame_vent_active_frames, 2, true, NULL};
anim_force_field_on = (AnimDef){s_force_field_on_frames, 2, true, NULL};
anim_force_field_off = (AnimDef){s_force_field_off_frames, 1, true, NULL};
anim_powerup_health = (AnimDef){s_powerup_health_frames, 2, true, NULL};
anim_powerup_jetpack = (AnimDef){s_powerup_jetpack_frames, 2, true, NULL};
anim_powerup_drone = (AnimDef){s_powerup_drone_frames, 2, true, NULL};
anim_drone = (AnimDef){s_drone_frames, 2, true, NULL};
}

View File

@@ -40,6 +40,23 @@ extern AnimDef anim_bullet;
extern AnimDef anim_bullet_impact;
extern AnimDef anim_enemy_bullet;
/* ── Hazard animations ─────────────────────────── */
extern AnimDef anim_turret_idle;
extern AnimDef anim_turret_fire;
extern AnimDef anim_platform;
extern AnimDef anim_flame_vent_idle;
extern AnimDef anim_flame_vent_active;
extern AnimDef anim_force_field_on;
extern AnimDef anim_force_field_off;
/* ── Powerup animations ────────────────────────── */
extern AnimDef anim_powerup_health;
extern AnimDef anim_powerup_jetpack;
extern AnimDef anim_powerup_drone;
/* ── Drone animation ───────────────────────────── */
extern AnimDef anim_drone;
/* Initialize all animation definitions */
void sprites_init_anims(void);

View File

@@ -1,38 +1,298 @@
#include "engine/core.h"
#include "engine/input.h"
#include "game/level.h"
#include "game/levelgen.h"
#include "game/editor.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <SDL2/SDL.h>
static Level s_level;
/* ═══════════════════════════════════════════════════
* Game modes
* ═══════════════════════════════════════════════════ */
static void game_init(void) {
if (!level_load(&s_level, "assets/levels/level01.lvl")) {
fprintf(stderr, "Failed to load level!\n");
typedef enum GameMode {
MODE_PLAY,
MODE_EDITOR,
} GameMode;
static Level s_level;
static Editor s_editor;
static GameMode s_mode = MODE_PLAY;
static bool s_use_procgen = false;
static bool s_dump_lvl = false;
static bool s_use_editor = false;
static uint32_t s_gen_seed = 0;
static char s_edit_path[256] = {0};
/* Track whether we came from the editor (for returning after test play) */
static bool s_testing_from_editor = false;
static const char *theme_name(LevelTheme t) {
switch (t) {
case THEME_PLANET_SURFACE: return "Planet Surface";
case THEME_PLANET_BASE: return "Planet Base";
case THEME_SPACE_STATION: return "Space Station";
default: return "Unknown";
}
}
static void load_generated_level(void) {
LevelGenConfig config = levelgen_default_config();
config.seed = s_gen_seed;
config.num_segments = 6;
config.difficulty = 0.5f;
/* Build a theme progression — start on surface, move indoors/upward.
* Derive from seed so it varies with each regeneration and doesn't
* depend on rand() state (which parallax generators clobber). */
uint32_t seed_for_theme = config.seed ? config.seed : (uint32_t)time(NULL);
int r = (int)(seed_for_theme % 4);
switch (r) {
case 0: /* Surface -> Base */
config.themes[0] = THEME_PLANET_SURFACE;
config.themes[1] = THEME_PLANET_SURFACE;
config.themes[2] = THEME_PLANET_BASE;
config.themes[3] = THEME_PLANET_BASE;
config.themes[4] = THEME_PLANET_BASE;
config.themes[5] = THEME_PLANET_BASE;
config.theme_count = 6;
break;
case 1: /* Base -> Station */
config.themes[0] = THEME_PLANET_BASE;
config.themes[1] = THEME_PLANET_BASE;
config.themes[2] = THEME_PLANET_BASE;
config.themes[3] = THEME_SPACE_STATION;
config.themes[4] = THEME_SPACE_STATION;
config.themes[5] = THEME_SPACE_STATION;
config.theme_count = 6;
break;
case 2: /* Surface -> Base -> Station (full journey) */
config.themes[0] = THEME_PLANET_SURFACE;
config.themes[1] = THEME_PLANET_SURFACE;
config.themes[2] = THEME_PLANET_BASE;
config.themes[3] = THEME_PLANET_BASE;
config.themes[4] = THEME_SPACE_STATION;
config.themes[5] = THEME_SPACE_STATION;
config.theme_count = 6;
break;
case 3: /* Single theme (derived from seed) */
default: {
LevelTheme single = (LevelTheme)(seed_for_theme / 4 % THEME_COUNT);
config.themes[0] = single;
config.theme_count = 1;
break;
}
}
printf("Theme sequence:");
for (int i = 0; i < config.theme_count; i++) {
printf(" %s", theme_name(config.themes[i]));
}
printf("\n");
Tilemap gen_map;
if (!levelgen_generate(&gen_map, &config)) {
fprintf(stderr, "Failed to generate level!\n");
g_engine.running = false;
return;
}
/* Optionally dump to file for inspection */
if (s_dump_lvl) {
levelgen_dump_lvl(&gen_map, "assets/levels/generated.lvl");
}
if (!level_load_generated(&s_level, &gen_map)) {
fprintf(stderr, "Failed to load generated level!\n");
g_engine.running = false;
}
}
/* ── Switch to editor mode ── */
static void enter_editor(void) {
if (s_mode == MODE_PLAY) {
level_free(&s_level);
}
s_mode = MODE_EDITOR;
editor_init(&s_editor);
if (s_edit_path[0]) {
if (!editor_load(&s_editor, s_edit_path)) {
fprintf(stderr, "Failed to load %s, creating new level\n", s_edit_path);
editor_new_level(&s_editor, 40, 23);
}
} else {
editor_new_level(&s_editor, 40, 23);
}
/* Change window title */
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
}
/* ── Switch to play mode (test play from editor) ── */
static void enter_test_play(void) {
/* Save the current level to a temp file */
editor_save_as(&s_editor, "assets/levels/_editor_test.lvl");
s_mode = MODE_PLAY;
s_testing_from_editor = true;
if (!level_load(&s_level, "assets/levels/_editor_test.lvl")) {
fprintf(stderr, "Failed to load editor test level!\n");
/* Fall back to editor */
s_mode = MODE_EDITOR;
s_testing_from_editor = false;
return;
}
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Testing");
}
/* ── Return from test play to editor ── */
static void return_to_editor(void) {
level_free(&s_level);
s_mode = MODE_EDITOR;
s_testing_from_editor = false;
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
}
/* ═══════════════════════════════════════════════════
* Game callbacks
* ═══════════════════════════════════════════════════ */
static void game_init(void) {
if (s_use_editor) {
enter_editor();
} else if (s_use_procgen) {
load_generated_level();
} else {
if (!level_load(&s_level, "assets/levels/level01.lvl")) {
fprintf(stderr, "Failed to load level!\n");
g_engine.running = false;
}
}
}
static void game_update(float dt) {
/* Quit on escape */
if (s_mode == MODE_EDITOR) {
editor_update(&s_editor, dt);
if (editor_wants_test_play(&s_editor)) {
enter_test_play();
}
if (editor_wants_quit(&s_editor)) {
g_engine.running = false;
}
return;
}
/* ── Play mode ── */
/* Quit / return to editor on escape */
if (input_pressed(ACTION_PAUSE)) {
if (s_testing_from_editor) {
return_to_editor();
return;
}
g_engine.running = false;
return;
}
/* E key: enter editor from gameplay (not during test play) */
if (!s_testing_from_editor && input_key_pressed(SDL_SCANCODE_E)) {
/* Save current level path for potential re-editing */
level_free(&s_level);
enter_editor();
return;
}
/* R key: regenerate level with new seed */
static bool r_was_pressed = false;
bool r_pressed = input_key_held(SDL_SCANCODE_R);
if (r_pressed && !r_was_pressed) {
printf("\n=== Regenerating level ===\n");
level_free(&s_level);
s_gen_seed = (uint32_t)time(NULL);
s_use_procgen = true;
load_generated_level();
}
r_was_pressed = r_pressed;
level_update(&s_level, dt);
}
static void game_render(float interpolation) {
level_render(&s_level, interpolation);
if (s_mode == MODE_EDITOR) {
editor_render(&s_editor, interpolation);
} else {
level_render(&s_level, interpolation);
}
}
static void game_shutdown(void) {
level_free(&s_level);
/* Always free both — editor may have been initialized even if we're
* currently in play mode (e.g. shutdown during test play). editor_free
* and level_free are safe to call on zeroed/already-freed structs. */
if (s_mode == MODE_PLAY || s_testing_from_editor) {
level_free(&s_level);
}
if (s_mode == MODE_EDITOR || s_use_editor) {
editor_free(&s_editor);
}
}
/* ═══════════════════════════════════════════════════
* Main
* ═══════════════════════════════════════════════════ */
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
/* Parse command-line arguments */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--generate") == 0 || strcmp(argv[i], "-g") == 0) {
s_use_procgen = true;
} else if (strcmp(argv[i], "--dump") == 0 || strcmp(argv[i], "-d") == 0) {
s_dump_lvl = true;
} else if (strcmp(argv[i], "--seed") == 0 || strcmp(argv[i], "-s") == 0) {
if (i + 1 < argc) {
s_gen_seed = (uint32_t)atoi(argv[++i]);
}
} else if (strcmp(argv[i], "--edit") == 0 || strcmp(argv[i], "-e") == 0) {
s_use_editor = true;
/* Optional: next arg is a file path */
if (i + 1 < argc && argv[i + 1][0] != '-') {
strncpy(s_edit_path, argv[++i], sizeof(s_edit_path) - 1);
}
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
printf("Usage: jnr [options]\n");
printf(" --generate, -g Load a procedurally generated level\n");
printf(" --dump, -d Dump generated level to assets/levels/generated.lvl\n");
printf(" --seed N, -s N Set RNG seed for generation\n");
printf(" --edit [file], -e [file] Open level editor (optionally load a .lvl file)\n");
printf("\nIn-game:\n");
printf(" R Regenerate level with new random seed\n");
printf(" E Open level editor\n");
printf(" ESC Quit (or return to editor from test play)\n");
printf("\nEditor:\n");
printf(" 1-5 Select tool (Pencil/Eraser/Fill/Entity/Spawn)\n");
printf(" Q/W/E Select layer (Collision/BG/FG)\n");
printf(" G Toggle grid\n");
printf(" V Toggle all-layer visibility\n");
printf(" Arrow keys / WASD Pan camera\n");
printf(" Scroll wheel Zoom in/out\n");
printf(" Middle mouse drag Pan camera\n");
printf(" Left click Paint/place (canvas) or select (palette)\n");
printf(" Right click Pick tile / delete entity\n");
printf(" Ctrl+S Save level\n");
printf(" Ctrl++/- Resize level width\n");
printf(" P Test play level\n");
printf(" ESC Quit editor\n");
return 0;
}
}
srand((unsigned)time(NULL));
if (!engine_init()) {
return 1;