forked from tas/major_tom
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:
@@ -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
|
||||
|
||||
@@ -88,6 +88,12 @@ void audio_stop_music(void) {
|
||||
Mix_HaltMusic();
|
||||
}
|
||||
|
||||
void audio_free_music(Music *m) {
|
||||
if (!m || !m->music) return;
|
||||
Mix_FreeMusic((Mix_Music *)m->music);
|
||||
m->music = NULL;
|
||||
}
|
||||
|
||||
void audio_set_music_volume(int volume) {
|
||||
if (!s_initialized) return;
|
||||
Mix_VolumeMusic(volume);
|
||||
|
||||
@@ -17,6 +17,7 @@ Music audio_load_music(const char *path);
|
||||
void audio_play_sound(Sound s, int volume); /* 0-128, fire and forget */
|
||||
void audio_play_music(Music m, bool loop);
|
||||
void audio_stop_music(void);
|
||||
void audio_free_music(Music *m);
|
||||
void audio_set_music_volume(int volume); /* 0-128 */
|
||||
void audio_shutdown(void);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ void camera_init(Camera *c, float vp_w, float vp_h) {
|
||||
c->smoothing = 5.0f;
|
||||
c->deadzone = vec2(30.0f, 20.0f);
|
||||
c->look_ahead = vec2(40.0f, 0.0f);
|
||||
c->zoom = 1.0f;
|
||||
}
|
||||
|
||||
void camera_set_bounds(Camera *c, float world_w, float world_h) {
|
||||
@@ -40,6 +41,10 @@ void camera_follow(Camera *c, Vec2 target, Vec2 velocity, float dt) {
|
||||
|
||||
Vec2 camera_world_to_screen(const Camera *c, Vec2 world_pos) {
|
||||
Vec2 screen = vec2_sub(world_pos, c->pos);
|
||||
/* Apply zoom */
|
||||
if (c->zoom != 1.0f && c->zoom > 0.0f) {
|
||||
screen = vec2_scale(screen, c->zoom);
|
||||
}
|
||||
/* Apply shake offset */
|
||||
screen.x += c->shake_offset.x;
|
||||
screen.y += c->shake_offset.y;
|
||||
@@ -47,7 +52,15 @@ Vec2 camera_world_to_screen(const Camera *c, Vec2 world_pos) {
|
||||
}
|
||||
|
||||
Vec2 camera_screen_to_world(const Camera *c, Vec2 screen_pos) {
|
||||
return vec2_add(screen_pos, c->pos);
|
||||
Vec2 world = screen_pos;
|
||||
/* Remove shake */
|
||||
world.x -= c->shake_offset.x;
|
||||
world.y -= c->shake_offset.y;
|
||||
/* Reverse zoom */
|
||||
if (c->zoom != 1.0f && c->zoom > 0.0f) {
|
||||
world = vec2_scale(world, 1.0f / c->zoom);
|
||||
}
|
||||
return vec2_add(world, c->pos);
|
||||
}
|
||||
|
||||
/* ── Screen shake ────────────────────────────────── */
|
||||
|
||||
@@ -12,6 +12,8 @@ typedef struct Camera {
|
||||
float smoothing; /* higher = slower follow (0=instant) */
|
||||
Vec2 deadzone; /* half-size of deadzone rect */
|
||||
Vec2 look_ahead; /* pixels to lead in move direction */
|
||||
/* Zoom (used by editor; gameplay keeps 1.0) */
|
||||
float zoom; /* 1.0 = normal, <1 = zoomed out */
|
||||
/* Screen shake */
|
||||
float shake_timer; /* remaining shake time */
|
||||
float shake_intensity; /* current max pixel offset */
|
||||
|
||||
@@ -20,6 +20,12 @@ typedef enum EntityType {
|
||||
ENT_PROJECTILE,
|
||||
ENT_PICKUP,
|
||||
ENT_PARTICLE,
|
||||
ENT_TURRET,
|
||||
ENT_MOVING_PLATFORM,
|
||||
ENT_FLAME_VENT,
|
||||
ENT_FORCE_FIELD,
|
||||
ENT_POWERUP,
|
||||
ENT_DRONE,
|
||||
ENT_TYPE_COUNT
|
||||
} EntityType;
|
||||
|
||||
|
||||
@@ -14,6 +14,19 @@ static bool s_latched_released[ACTION_COUNT];
|
||||
|
||||
static bool s_quit_requested;
|
||||
|
||||
/* ── Mouse state ──────────────────────────────────── */
|
||||
static int s_mouse_x, s_mouse_y;
|
||||
static bool s_mouse_current[MOUSE_BUTTON_COUNT];
|
||||
static bool s_mouse_previous[MOUSE_BUTTON_COUNT];
|
||||
static bool s_mouse_latched_pressed[MOUSE_BUTTON_COUNT];
|
||||
static bool s_mouse_latched_released[MOUSE_BUTTON_COUNT];
|
||||
static int s_mouse_scroll;
|
||||
|
||||
/* ── Raw keyboard state ───────────────────────────── */
|
||||
static const Uint8 *s_key_state = NULL;
|
||||
static Uint8 s_prev_keys[SDL_NUM_SCANCODES];
|
||||
static Uint8 s_latched_keys[SDL_NUM_SCANCODES];
|
||||
|
||||
/* Default key bindings (primary + alternate) */
|
||||
static SDL_Scancode s_bindings[ACTION_COUNT] = {
|
||||
[ACTION_LEFT] = SDL_SCANCODE_LEFT,
|
||||
@@ -36,13 +49,28 @@ void input_init(void) {
|
||||
memset(s_previous, 0, sizeof(s_previous));
|
||||
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
|
||||
memset(s_latched_released, 0, sizeof(s_latched_released));
|
||||
memset(s_mouse_current, 0, sizeof(s_mouse_current));
|
||||
memset(s_mouse_previous, 0, sizeof(s_mouse_previous));
|
||||
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
|
||||
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
|
||||
memset(s_prev_keys, 0, sizeof(s_prev_keys));
|
||||
memset(s_latched_keys, 0, sizeof(s_latched_keys));
|
||||
s_mouse_x = s_mouse_y = 0;
|
||||
s_mouse_scroll = 0;
|
||||
s_quit_requested = false;
|
||||
}
|
||||
|
||||
void input_poll(void) {
|
||||
/* Save previous state */
|
||||
memcpy(s_previous, s_current, sizeof(s_current));
|
||||
memcpy(s_mouse_previous, s_mouse_current, sizeof(s_mouse_current));
|
||||
s_quit_requested = false;
|
||||
s_mouse_scroll = 0;
|
||||
|
||||
/* Save previous raw key state */
|
||||
if (s_key_state) {
|
||||
memcpy(s_prev_keys, s_key_state, SDL_NUM_SCANCODES);
|
||||
}
|
||||
|
||||
/* Process SDL events */
|
||||
SDL_Event event;
|
||||
@@ -51,14 +79,17 @@ void input_poll(void) {
|
||||
case SDL_QUIT:
|
||||
s_quit_requested = true;
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
s_mouse_scroll += event.wheel.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Read keyboard state */
|
||||
const Uint8 *keys = SDL_GetKeyboardState(NULL);
|
||||
s_key_state = SDL_GetKeyboardState(NULL);
|
||||
for (int i = 0; i < ACTION_COUNT; i++) {
|
||||
s_current[i] = keys[s_bindings[i]];
|
||||
if (s_alt_bindings[i] && keys[s_alt_bindings[i]]) {
|
||||
s_current[i] = s_key_state[s_bindings[i]];
|
||||
if (s_alt_bindings[i] && s_key_state[s_alt_bindings[i]]) {
|
||||
s_current[i] = true;
|
||||
}
|
||||
|
||||
@@ -70,12 +101,50 @@ void input_poll(void) {
|
||||
s_latched_released[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Latch raw key edges */
|
||||
if (s_key_state) {
|
||||
for (int i = 0; i < SDL_NUM_SCANCODES; i++) {
|
||||
if (s_key_state[i] && !s_prev_keys[i]) {
|
||||
s_latched_keys[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Read mouse state */
|
||||
Uint32 buttons = SDL_GetMouseState(&s_mouse_x, &s_mouse_y);
|
||||
|
||||
/* Convert window coords to logical coords (SDL handles this via
|
||||
SDL_RenderSetLogicalSize, but GetMouseState returns window coords) */
|
||||
SDL_Renderer *r = SDL_GetRenderer(SDL_GetMouseFocus());
|
||||
if (r) {
|
||||
float lx, ly;
|
||||
SDL_RenderWindowToLogical(r, s_mouse_x, s_mouse_y, &lx, &ly);
|
||||
s_mouse_x = (int)lx;
|
||||
s_mouse_y = (int)ly;
|
||||
}
|
||||
|
||||
s_mouse_current[MOUSE_LEFT] = (buttons & SDL_BUTTON_LMASK) != 0;
|
||||
s_mouse_current[MOUSE_MIDDLE] = (buttons & SDL_BUTTON_MMASK) != 0;
|
||||
s_mouse_current[MOUSE_RIGHT] = (buttons & SDL_BUTTON_RMASK) != 0;
|
||||
|
||||
for (int i = 0; i < MOUSE_BUTTON_COUNT; i++) {
|
||||
if (s_mouse_current[i] && !s_mouse_previous[i]) {
|
||||
s_mouse_latched_pressed[i] = true;
|
||||
}
|
||||
if (!s_mouse_current[i] && s_mouse_previous[i]) {
|
||||
s_mouse_latched_released[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void input_consume(void) {
|
||||
/* Clear latched states after an update tick has read them */
|
||||
memset(s_latched_pressed, 0, sizeof(s_latched_pressed));
|
||||
memset(s_latched_released, 0, sizeof(s_latched_released));
|
||||
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
|
||||
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
|
||||
memset(s_latched_keys, 0, sizeof(s_latched_keys));
|
||||
}
|
||||
|
||||
bool input_pressed(Action a) {
|
||||
@@ -94,6 +163,44 @@ bool input_quit_requested(void) {
|
||||
return s_quit_requested;
|
||||
}
|
||||
|
||||
/* ── Mouse queries ────────────────────────────────── */
|
||||
|
||||
void input_mouse_pos(int *x, int *y) {
|
||||
if (x) *x = s_mouse_x;
|
||||
if (y) *y = s_mouse_y;
|
||||
}
|
||||
|
||||
bool input_mouse_pressed(MouseButton btn) {
|
||||
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
||||
return s_mouse_latched_pressed[btn];
|
||||
}
|
||||
|
||||
bool input_mouse_held(MouseButton btn) {
|
||||
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
||||
return s_mouse_current[btn];
|
||||
}
|
||||
|
||||
bool input_mouse_released(MouseButton btn) {
|
||||
if (btn < 0 || btn >= MOUSE_BUTTON_COUNT) return false;
|
||||
return s_mouse_latched_released[btn];
|
||||
}
|
||||
|
||||
int input_mouse_scroll(void) {
|
||||
return s_mouse_scroll;
|
||||
}
|
||||
|
||||
/* ── Raw keyboard queries ─────────────────────────── */
|
||||
|
||||
bool input_key_pressed(SDL_Scancode key) {
|
||||
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
|
||||
return s_latched_keys[key] != 0;
|
||||
}
|
||||
|
||||
bool input_key_held(SDL_Scancode key) {
|
||||
if (key < 0 || key >= SDL_NUM_SCANCODES) return false;
|
||||
return s_key_state && s_key_state[key];
|
||||
}
|
||||
|
||||
void input_shutdown(void) {
|
||||
/* Nothing to clean up */
|
||||
}
|
||||
|
||||
@@ -27,4 +27,27 @@ void input_shutdown(void);
|
||||
/* Returns true if SDL_QUIT was received */
|
||||
bool input_quit_requested(void);
|
||||
|
||||
/* ── Mouse state ────────────────────────────────── */
|
||||
typedef enum MouseButton {
|
||||
MOUSE_LEFT = 0,
|
||||
MOUSE_MIDDLE = 1,
|
||||
MOUSE_RIGHT = 2,
|
||||
MOUSE_BUTTON_COUNT
|
||||
} MouseButton;
|
||||
|
||||
/* Mouse position in logical (game) coordinates */
|
||||
void input_mouse_pos(int *x, int *y);
|
||||
|
||||
/* Mouse button queries (same semantics as keyboard) */
|
||||
bool input_mouse_pressed(MouseButton btn);
|
||||
bool input_mouse_held(MouseButton btn);
|
||||
bool input_mouse_released(MouseButton btn);
|
||||
|
||||
/* Scroll wheel delta since last poll (positive = up) */
|
||||
int input_mouse_scroll(void);
|
||||
|
||||
/* Raw keyboard access for text-like input in editor */
|
||||
bool input_key_pressed(SDL_Scancode key);
|
||||
bool input_key_held(SDL_Scancode key);
|
||||
|
||||
#endif /* JNR_INPUT_H */
|
||||
|
||||
@@ -231,6 +231,491 @@ void parallax_generate_nebula(Parallax *p, SDL_Renderer *renderer) {
|
||||
p->near_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
/* ── Themed: Alien Sky (planet surface) ──────────────── */
|
||||
|
||||
static void generate_alien_sky_far(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(55);
|
||||
|
||||
/* Fewer stars than deep space — atmosphere scatters light */
|
||||
for (int i = 0; i < 60; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h * 0.6f); /* stars only in upper portion */
|
||||
uint8_t brightness = (uint8_t)(60 + (int)(randf() * 80));
|
||||
/* Warm-tinted: alien atmosphere filters starlight */
|
||||
uint8_t r = clamp_u8(brightness + 20);
|
||||
uint8_t g = clamp_u8(brightness - 10);
|
||||
uint8_t b = clamp_u8(brightness - 30);
|
||||
uint8_t a = (uint8_t)(100 + (int)(randf() * 80));
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
||||
SDL_Rect dot = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
/* A few brighter stars peeking through */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h * 0.4f);
|
||||
SDL_SetRenderDrawColor(renderer, 255, 220, 180, 200);
|
||||
SDL_Rect dot = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
/* Distant mountain/terrain silhouette at bottom */
|
||||
int terrain_base = (int)(h * 0.70f);
|
||||
for (int x = 0; x < w; x++) {
|
||||
/* Jagged terrain profile using multiple sine waves */
|
||||
float t = (float)x / (float)w;
|
||||
float h1 = sinf(t * 6.28f * 2.0f) * 15.0f;
|
||||
float h2 = sinf(t * 6.28f * 5.3f + 1.2f) * 8.0f;
|
||||
float h3 = sinf(t * 6.28f * 11.7f + 3.1f) * 4.0f;
|
||||
int peak = terrain_base - (int)(h1 + h2 + h3);
|
||||
if (peak < terrain_base - 30) peak = terrain_base - 30;
|
||||
|
||||
/* Dark terrain silhouette */
|
||||
for (int y = peak; y < h; y++) {
|
||||
/* Gradient: darker at bottom, slightly lighter at peaks */
|
||||
int depth = y - peak;
|
||||
uint8_t r = clamp_u8(8 - depth / 8);
|
||||
uint8_t g = clamp_u8(5 - depth / 10);
|
||||
uint8_t b = clamp_u8(12 - depth / 6);
|
||||
uint8_t a = (uint8_t)(depth < 3 ? 120 : 180);
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
||||
SDL_Rect px = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &px);
|
||||
}
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->far_layer.texture = tex;
|
||||
p->far_layer.tex_w = w;
|
||||
p->far_layer.tex_h = h;
|
||||
p->far_layer.scroll_x = 0.03f; /* very slow — distant landscape */
|
||||
p->far_layer.scroll_y = 0.03f;
|
||||
p->far_layer.active = true;
|
||||
p->far_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
static void generate_alien_sky_near(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(188);
|
||||
|
||||
/* Atmospheric haze / dust clouds — warm oranges and reds */
|
||||
typedef struct { uint8_t r, g, b; } HazeColor;
|
||||
HazeColor haze_palette[] = {
|
||||
{ 80, 30, 15}, /* rusty orange */
|
||||
{ 60, 20, 25}, /* dark red */
|
||||
{ 50, 35, 20}, /* brown dust */
|
||||
{ 70, 25, 40}, /* murky purple */
|
||||
{ 45, 40, 30}, /* sandy */
|
||||
};
|
||||
int haze_count = sizeof(haze_palette) / sizeof(haze_palette[0]);
|
||||
|
||||
/* Haze bands across the sky */
|
||||
for (int band = 0; band < 4; band++) {
|
||||
float cy = randf() * h * 0.7f + h * 0.15f;
|
||||
HazeColor col = haze_palette[band % haze_count];
|
||||
int blobs = 20 + (int)(randf() * 15);
|
||||
for (int b = 0; b < blobs; b++) {
|
||||
float angle = randf() * (float)(2.0 * M_PI);
|
||||
float dist = randf() * 100.0f;
|
||||
int bx = (int)(randf() * w);
|
||||
int by = (int)(cy + sinf(angle) * dist * 0.3f);
|
||||
int bw = 15 + (int)(randf() * 30);
|
||||
int bh = 5 + (int)(randf() * 10);
|
||||
uint8_t br = clamp_u8(col.r + (int)(randf() * 20 - 10));
|
||||
uint8_t bg = clamp_u8(col.g + (int)(randf() * 20 - 10));
|
||||
uint8_t bb = clamp_u8(col.b + (int)(randf() * 20 - 10));
|
||||
SDL_SetRenderDrawColor(renderer, br, bg, bb, (uint8_t)(6 + (int)(randf() * 12)));
|
||||
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
}
|
||||
|
||||
/* Floating dust particles */
|
||||
for (int i = 0; i < 40; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
HazeColor col = haze_palette[(int)(randf() * haze_count)];
|
||||
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
||||
(uint8_t)(25 + (int)(randf() * 35)));
|
||||
SDL_Rect dot = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->near_layer.texture = tex;
|
||||
p->near_layer.tex_w = w;
|
||||
p->near_layer.tex_h = h;
|
||||
p->near_layer.scroll_x = 0.10f;
|
||||
p->near_layer.scroll_y = 0.06f;
|
||||
p->near_layer.active = true;
|
||||
p->near_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
/* ── Themed: Interior (planet base) ─────────────────── */
|
||||
|
||||
static void generate_interior_far(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(73);
|
||||
|
||||
/* Wall panel grid — subtle horizontal and vertical lines */
|
||||
/* Horizontal beams */
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int y = (int)(randf() * h);
|
||||
uint8_t a = (uint8_t)(15 + (int)(randf() * 20));
|
||||
SDL_SetRenderDrawColor(renderer, 25, 30, 40, a);
|
||||
SDL_Rect beam = {0, y, w, 2};
|
||||
SDL_RenderFillRect(renderer, &beam);
|
||||
}
|
||||
|
||||
/* Vertical structural columns */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int col_w = 2 + (int)(randf() * 3);
|
||||
uint8_t a = (uint8_t)(12 + (int)(randf() * 18));
|
||||
SDL_SetRenderDrawColor(renderer, 20, 25, 35, a);
|
||||
SDL_Rect col_rect = {x, 0, col_w, h};
|
||||
SDL_RenderFillRect(renderer, &col_rect);
|
||||
}
|
||||
|
||||
/* Recessed wall panels (darker rectangles) */
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int px = (int)(randf() * w);
|
||||
int py = (int)(randf() * h);
|
||||
int pw = 20 + (int)(randf() * 40);
|
||||
int ph = 15 + (int)(randf() * 30);
|
||||
SDL_SetRenderDrawColor(renderer, 8, 10, 18, (uint8_t)(20 + (int)(randf() * 15)));
|
||||
SDL_Rect panel = {px, py, pw, ph};
|
||||
SDL_RenderFillRect(renderer, &panel);
|
||||
/* Panel border highlight (top/left edge) */
|
||||
SDL_SetRenderDrawColor(renderer, 30, 35, 50, 20);
|
||||
SDL_Rect edge_h = {px, py, pw, 1};
|
||||
SDL_Rect edge_v = {px, py, 1, ph};
|
||||
SDL_RenderFillRect(renderer, &edge_h);
|
||||
SDL_RenderFillRect(renderer, &edge_v);
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->far_layer.texture = tex;
|
||||
p->far_layer.tex_w = w;
|
||||
p->far_layer.tex_h = h;
|
||||
p->far_layer.scroll_x = 0.02f; /* very slow — background wall */
|
||||
p->far_layer.scroll_y = 0.02f;
|
||||
p->far_layer.active = true;
|
||||
p->far_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
static void generate_interior_near(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(209);
|
||||
|
||||
/* Pipes running along ceiling/floor areas */
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int py = (int)(randf() * h);
|
||||
int pipe_h = 2 + (int)(randf() * 2);
|
||||
/* Pipe body */
|
||||
uint8_t r = (uint8_t)(20 + (int)(randf() * 15));
|
||||
uint8_t g = (uint8_t)(25 + (int)(randf() * 15));
|
||||
uint8_t b = (uint8_t)(35 + (int)(randf() * 15));
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, 25);
|
||||
SDL_Rect pipe = {0, py, w, pipe_h};
|
||||
SDL_RenderFillRect(renderer, &pipe);
|
||||
/* Pipe highlight (top edge) */
|
||||
SDL_SetRenderDrawColor(renderer, r + 15, g + 15, b + 15, 18);
|
||||
SDL_Rect highlight = {0, py, w, 1};
|
||||
SDL_RenderFillRect(renderer, &highlight);
|
||||
}
|
||||
|
||||
/* Small indicator lights / LEDs scattered on walls */
|
||||
for (int i = 0; i < 15; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
float r_chance = randf();
|
||||
if (r_chance < 0.4f) {
|
||||
/* Green status light */
|
||||
SDL_SetRenderDrawColor(renderer, 30, 180, 60, 60);
|
||||
} else if (r_chance < 0.65f) {
|
||||
/* Amber warning */
|
||||
SDL_SetRenderDrawColor(renderer, 200, 150, 30, 50);
|
||||
} else if (r_chance < 0.80f) {
|
||||
/* Red alert */
|
||||
SDL_SetRenderDrawColor(renderer, 200, 40, 30, 45);
|
||||
} else {
|
||||
/* Blue data */
|
||||
SDL_SetRenderDrawColor(renderer, 40, 100, 200, 50);
|
||||
}
|
||||
SDL_Rect led = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &led);
|
||||
/* Tiny glow around the LED */
|
||||
SDL_SetRenderDrawColor(renderer, 40, 60, 80, 8);
|
||||
SDL_Rect glow = {x - 1, y - 1, 3, 3};
|
||||
SDL_RenderFillRect(renderer, &glow);
|
||||
}
|
||||
|
||||
/* Ventilation grate patterns */
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int gx = (int)(randf() * w);
|
||||
int gy = (int)(randf() * h);
|
||||
int gw = 8 + (int)(randf() * 12);
|
||||
int gh = 4 + (int)(randf() * 6);
|
||||
/* Grate slots (horizontal lines within a rectangle) */
|
||||
for (int s = 0; s < gh; s += 2) {
|
||||
SDL_SetRenderDrawColor(renderer, 15, 18, 28, 25);
|
||||
SDL_Rect slot = {gx, gy + s, gw, 1};
|
||||
SDL_RenderFillRect(renderer, &slot);
|
||||
}
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->near_layer.texture = tex;
|
||||
p->near_layer.tex_w = w;
|
||||
p->near_layer.tex_h = h;
|
||||
p->near_layer.scroll_x = 0.08f;
|
||||
p->near_layer.scroll_y = 0.05f;
|
||||
p->near_layer.active = true;
|
||||
p->near_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
/* ── Themed: Deep Space (space station viewports) ───── */
|
||||
|
||||
static void generate_deep_space_far(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(99);
|
||||
|
||||
/* Dense starfield — station viewports show deep space clearly */
|
||||
/* Many small stars */
|
||||
for (int i = 0; i < 180; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
uint8_t brightness = (uint8_t)(120 + (int)(randf() * 100));
|
||||
uint8_t r = brightness, g = brightness, b = brightness;
|
||||
float tint = randf();
|
||||
if (tint < 0.3f) {
|
||||
b = clamp_u8(brightness + 50);
|
||||
g = clamp_u8(brightness + 10); /* cool blue-white */
|
||||
} else if (tint < 0.45f) {
|
||||
r = clamp_u8(brightness + 30); /* warm */
|
||||
}
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, (uint8_t)(180 + (int)(randf() * 75)));
|
||||
SDL_Rect dot = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
/* Medium stars with halos — more prominent than default */
|
||||
for (int i = 0; i < 40; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
uint8_t brightness = (uint8_t)(200 + (int)(randf() * 55));
|
||||
uint8_t r = brightness, g = brightness, b = 255;
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, 255);
|
||||
SDL_Rect dot = {x, y, 1, 1};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
if (randf() < 0.5f) {
|
||||
SDL_SetRenderDrawColor(renderer, r, g, b, 90);
|
||||
SDL_Rect hx = {x - 1, y, 3, 1};
|
||||
SDL_Rect hy = {x, y - 1, 1, 3};
|
||||
SDL_RenderFillRect(renderer, &hx);
|
||||
SDL_RenderFillRect(renderer, &hy);
|
||||
}
|
||||
}
|
||||
|
||||
/* Bright feature stars */
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
||||
SDL_Rect core = {x, y, 2, 2};
|
||||
SDL_RenderFillRect(renderer, &core);
|
||||
/* Cyan-white glow */
|
||||
SDL_SetRenderDrawColor(renderer, 150, 220, 255, (uint8_t)(100 + (int)(randf() * 80)));
|
||||
SDL_Rect ch = {x - 1, y, 4, 2};
|
||||
SDL_Rect cv = {x, y - 1, 2, 4};
|
||||
SDL_RenderFillRect(renderer, &ch);
|
||||
SDL_RenderFillRect(renderer, &cv);
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->far_layer.texture = tex;
|
||||
p->far_layer.tex_w = w;
|
||||
p->far_layer.tex_h = h;
|
||||
p->far_layer.scroll_x = 0.05f;
|
||||
p->far_layer.scroll_y = 0.05f;
|
||||
p->far_layer.active = true;
|
||||
p->far_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
static void generate_deep_space_near(Parallax *p, SDL_Renderer *renderer) {
|
||||
int w = SCREEN_WIDTH;
|
||||
int h = SCREEN_HEIGHT;
|
||||
|
||||
SDL_Texture *tex = SDL_CreateTexture(renderer,
|
||||
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
|
||||
if (!tex) return;
|
||||
|
||||
SDL_SetRenderTarget(renderer, tex);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
unsigned int saved_seed = (unsigned int)rand();
|
||||
srand(251);
|
||||
|
||||
/* Vivid nebula clouds — cyan, electric blue, violet */
|
||||
typedef struct { uint8_t r, g, b; } SpaceColor;
|
||||
SpaceColor palette[] = {
|
||||
{ 20, 60, 140}, /* electric blue */
|
||||
{ 30, 90, 130}, /* cyan-blue */
|
||||
{ 50, 30, 120}, /* deep violet */
|
||||
{ 15, 80, 100}, /* teal */
|
||||
{ 70, 20, 100}, /* purple */
|
||||
};
|
||||
int palette_count = sizeof(palette) / sizeof(palette[0]);
|
||||
|
||||
for (int cloud = 0; cloud < 6; cloud++) {
|
||||
float cx = randf() * w;
|
||||
float cy = randf() * h;
|
||||
SpaceColor col = palette[cloud % palette_count];
|
||||
int blobs = 35 + (int)(randf() * 25);
|
||||
for (int b = 0; b < blobs; b++) {
|
||||
float angle = randf() * (float)(2.0 * M_PI);
|
||||
float dist = randf() * 90.0f + randf() * 50.0f;
|
||||
int bx = (int)(cx + cosf(angle) * dist);
|
||||
int by = (int)(cy + sinf(angle) * dist);
|
||||
int bw = 10 + (int)(randf() * 24);
|
||||
int bh = 8 + (int)(randf() * 18);
|
||||
uint8_t br = clamp_u8(col.r + (int)(randf() * 30 - 15));
|
||||
uint8_t bg = clamp_u8(col.g + (int)(randf() * 30 - 15));
|
||||
uint8_t bb = clamp_u8(col.b + (int)(randf() * 30 - 15));
|
||||
SDL_SetRenderDrawColor(renderer, br, bg, bb, (uint8_t)(10 + (int)(randf() * 20)));
|
||||
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scattered bright dust */
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int x = (int)(randf() * w);
|
||||
int y = (int)(randf() * h);
|
||||
SpaceColor col = palette[(int)(randf() * palette_count)];
|
||||
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
|
||||
(uint8_t)(35 + (int)(randf() * 45)));
|
||||
SDL_Rect dot = {x, y, 2, 2};
|
||||
SDL_RenderFillRect(renderer, &dot);
|
||||
}
|
||||
|
||||
srand(saved_seed);
|
||||
SDL_SetRenderTarget(renderer, NULL);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
|
||||
p->near_layer.texture = tex;
|
||||
p->near_layer.tex_w = w;
|
||||
p->near_layer.tex_h = h;
|
||||
p->near_layer.scroll_x = 0.15f;
|
||||
p->near_layer.scroll_y = 0.10f;
|
||||
p->near_layer.active = true;
|
||||
p->near_layer.owns_texture = true;
|
||||
}
|
||||
|
||||
/* ── Themed parallax dispatcher ─────────────────────── */
|
||||
|
||||
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style) {
|
||||
switch (style) {
|
||||
case PARALLAX_STYLE_ALIEN_SKY:
|
||||
generate_alien_sky_far(p, renderer);
|
||||
generate_alien_sky_near(p, renderer);
|
||||
break;
|
||||
case PARALLAX_STYLE_INTERIOR:
|
||||
generate_interior_far(p, renderer);
|
||||
generate_interior_near(p, renderer);
|
||||
break;
|
||||
case PARALLAX_STYLE_DEEP_SPACE:
|
||||
generate_deep_space_far(p, renderer);
|
||||
generate_deep_space_near(p, renderer);
|
||||
break;
|
||||
case PARALLAX_STYLE_DEFAULT:
|
||||
default:
|
||||
parallax_generate_stars(p, renderer);
|
||||
parallax_generate_nebula(p, renderer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Render ──────────────────────────────────────────── */
|
||||
|
||||
static void render_layer(const ParallaxLayer *layer, const Camera *cam,
|
||||
|
||||
@@ -37,6 +37,17 @@ void parallax_generate_stars(Parallax *p, SDL_Renderer *renderer);
|
||||
/* Generate procedural nebula/dust texture (near layer) */
|
||||
void parallax_generate_nebula(Parallax *p, SDL_Renderer *renderer);
|
||||
|
||||
/* Themed parallax generation styles */
|
||||
typedef enum ParallaxStyle {
|
||||
PARALLAX_STYLE_DEFAULT, /* generic space (same as stars+nebula) */
|
||||
PARALLAX_STYLE_ALIEN_SKY, /* alien planet surface: dusty, hazy */
|
||||
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
|
||||
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
|
||||
} ParallaxStyle;
|
||||
|
||||
/* Generate both layers with a unified style */
|
||||
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style);
|
||||
|
||||
/* Render both layers (call before tile/entity rendering) */
|
||||
void parallax_render(const Parallax *p, const Camera *cam, SDL_Renderer *renderer);
|
||||
|
||||
|
||||
@@ -43,11 +43,18 @@ void renderer_flush(const Camera *cam) {
|
||||
screen_pos = camera_world_to_screen(cam, s->pos);
|
||||
}
|
||||
|
||||
float zw = s->size.x;
|
||||
float zh = s->size.y;
|
||||
if (layer != LAYER_HUD && cam && cam->zoom != 1.0f && cam->zoom > 0.0f) {
|
||||
zw *= cam->zoom;
|
||||
zh *= cam->zoom;
|
||||
}
|
||||
|
||||
SDL_Rect dst = {
|
||||
(int)screen_pos.x,
|
||||
(int)screen_pos.y,
|
||||
(int)s->size.x,
|
||||
(int)s->size.y
|
||||
(int)(zw + 0.5f),
|
||||
(int)(zh + 0.5f)
|
||||
};
|
||||
|
||||
SDL_RendererFlip flip = SDL_FLIP_NONE;
|
||||
@@ -75,15 +82,20 @@ void renderer_present(void) {
|
||||
void renderer_draw_rect(Vec2 pos, Vec2 size, SDL_Color color,
|
||||
DrawLayer layer, const Camera *cam) {
|
||||
Vec2 screen_pos = pos;
|
||||
float zw = size.x, zh = size.y;
|
||||
if (layer != LAYER_HUD && cam) {
|
||||
screen_pos = camera_world_to_screen(cam, pos);
|
||||
if (cam->zoom != 1.0f && cam->zoom > 0.0f) {
|
||||
zw *= cam->zoom;
|
||||
zh *= cam->zoom;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Rect rect = {
|
||||
(int)screen_pos.x,
|
||||
(int)screen_pos.y,
|
||||
(int)size.x,
|
||||
(int)size.y
|
||||
(int)(zw + 0.5f),
|
||||
(int)(zh + 0.5f)
|
||||
};
|
||||
|
||||
SDL_SetRenderDrawColor(s_renderer, color.r, color.g, color.b, color.a);
|
||||
|
||||
@@ -129,10 +129,11 @@ void tilemap_render_layer(const Tilemap *map, const uint16_t *layer,
|
||||
int end_x = map->width, end_y = map->height;
|
||||
|
||||
if (cam) {
|
||||
float inv_zoom = (cam->zoom > 0.0f) ? (1.0f / cam->zoom) : 1.0f;
|
||||
start_x = (int)(cam->pos.x / TILE_SIZE) - 1;
|
||||
start_y = (int)(cam->pos.y / TILE_SIZE) - 1;
|
||||
end_x = start_x + (int)(cam->viewport.x / TILE_SIZE) + 3;
|
||||
end_y = start_y + (int)(cam->viewport.y / TILE_SIZE) + 3;
|
||||
end_x = start_x + (int)(cam->viewport.x * inv_zoom / TILE_SIZE) + 3;
|
||||
end_y = start_y + (int)(cam->viewport.y * inv_zoom / TILE_SIZE) + 3;
|
||||
|
||||
if (start_x < 0) start_x = 0;
|
||||
if (start_y < 0) start_y = 0;
|
||||
@@ -163,11 +164,16 @@ void tilemap_render_layer(const Tilemap *map, const uint16_t *layer,
|
||||
Vec2 world_pos = vec2(tile_to_world(x), tile_to_world(y));
|
||||
Vec2 screen_pos = cam ? camera_world_to_screen(cam, world_pos) : world_pos;
|
||||
|
||||
float tile_draw_size = TILE_SIZE;
|
||||
if (cam && cam->zoom != 1.0f && cam->zoom > 0.0f) {
|
||||
tile_draw_size = TILE_SIZE * cam->zoom;
|
||||
}
|
||||
|
||||
SDL_Rect dst = {
|
||||
(int)screen_pos.x,
|
||||
(int)screen_pos.y,
|
||||
TILE_SIZE,
|
||||
TILE_SIZE
|
||||
(int)(tile_draw_size + 0.5f),
|
||||
(int)(tile_draw_size + 0.5f)
|
||||
};
|
||||
|
||||
SDL_RenderCopy(renderer, map->tileset, &src, &dst);
|
||||
|
||||
@@ -46,6 +46,7 @@ typedef struct Tilemap {
|
||||
bool has_bg_color; /* true if BG_COLOR was set */
|
||||
char parallax_far_path[ASSET_PATH_MAX]; /* far bg image path */
|
||||
char parallax_near_path[ASSET_PATH_MAX]; /* near bg image path */
|
||||
int parallax_style; /* procedural bg style (0=default) */
|
||||
EntitySpawn entity_spawns[MAX_ENTITY_SPAWNS];
|
||||
int entity_spawn_count;
|
||||
} Tilemap;
|
||||
|
||||
217
src/game/drone.c
Normal file
217
src/game/drone.c
Normal 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
33
src/game/drone.h
Normal 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
1118
src/game/editor.c
Normal file
File diff suppressed because it is too large
Load Diff
98
src/game/editor.h
Normal file
98
src/game/editor.h
Normal 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
108
src/game/entity_registry.c
Normal 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);
|
||||
}
|
||||
52
src/game/entity_registry.h
Normal file
52
src/game/entity_registry.h
Normal 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
599
src/game/hazards.c
Normal 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
90
src/game/hazards.h
Normal 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 */
|
||||
@@ -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);
|
||||
|
||||
@@ -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
903
src/game/levelgen.c
Normal 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
61
src/game/levelgen.h
Normal 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
142
src/game/powerup.c
Normal 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
40
src/game/powerup.h
Normal 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 */
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
278
src/main.c
278
src/main.c
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user