Add moon surface intro level with asteroid hazards and unarmed mechanics
Introduce moon01.lvl as the starting level — a pure jump-and-run intro with no gun and no enemies, just platforming over gaps and dodging falling asteroids. The player picks up their gun upon transitioning to level01. New features: - Moon tileset and PARALLAX_STYLE_MOON with crater terrain backgrounds - Asteroid entity (ENT_ASTEROID): falls from sky, damages on contact, explodes on ground with particles, respawns after delay - PLAYER_UNARMED directive disables gun for the level - Pit rescue mechanic: falling costs 1 HP and auto-dashes upward - Gun powerup entity type for future armed-pickup levels - Segment-based procedural level generator with themed rooms - Extended editor with entity palette and improved tile cycling - Web shell improvements for Emscripten builds
This commit is contained in:
166
src/game/level.c
166
src/game/level.c
@@ -3,6 +3,8 @@
|
||||
#include "game/enemy.h"
|
||||
#include "game/projectile.h"
|
||||
#include "game/hazards.h"
|
||||
#include "game/powerup.h"
|
||||
#include "game/drone.h"
|
||||
#include "game/sprites.h"
|
||||
#include "game/entity_registry.h"
|
||||
#include "engine/core.h"
|
||||
@@ -15,10 +17,12 @@
|
||||
#include "engine/assets.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
/* ── Sound effects ───────────────────────────────── */
|
||||
static Sound s_sfx_hit;
|
||||
static Sound s_sfx_enemy_death;
|
||||
static Sound s_sfx_pickup;
|
||||
static bool s_sfx_loaded = false;
|
||||
|
||||
/* ── Shared level setup (after tilemap is ready) ─── */
|
||||
@@ -32,6 +36,7 @@ static bool level_setup(Level *level) {
|
||||
if (!s_sfx_loaded) {
|
||||
s_sfx_hit = audio_load_sound("assets/sounds/hitHurt.wav");
|
||||
s_sfx_enemy_death = audio_load_sound("assets/sounds/teleport.wav");
|
||||
s_sfx_pickup = audio_load_sound("assets/sounds/teleport.wav");
|
||||
s_sfx_loaded = true;
|
||||
}
|
||||
|
||||
@@ -94,6 +99,12 @@ static bool level_setup(Level *level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Disarm player if level requests it */
|
||||
if (level->map.player_unarmed) {
|
||||
PlayerData *ppd = (PlayerData *)player->data;
|
||||
if (ppd) ppd->has_gun = false;
|
||||
}
|
||||
|
||||
/* 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];
|
||||
@@ -276,10 +287,111 @@ static void handle_collisions(EntityManager *em) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Powerup pickup ────────────────────── */
|
||||
if (player && a->type == ENT_POWERUP && a->active &&
|
||||
!(a->flags & ENTITY_DEAD)) {
|
||||
if (physics_overlap(&a->body, &player->body)) {
|
||||
PowerupData *pd = (PowerupData *)a->data;
|
||||
bool picked_up = false;
|
||||
|
||||
if (pd) {
|
||||
switch (pd->kind) {
|
||||
case POWERUP_HEALTH:
|
||||
if (player->health < player->max_health) {
|
||||
player->health++;
|
||||
picked_up = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case POWERUP_JETPACK: {
|
||||
PlayerData *ppd = (PlayerData *)player->data;
|
||||
if (ppd) {
|
||||
ppd->dash_charges = ppd->dash_max_charges;
|
||||
ppd->dash_recharge_timer = 0.0f;
|
||||
ppd->jetpack_boost_timer = PLAYER_JETPACK_BOOST_DURATION;
|
||||
picked_up = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case POWERUP_DRONE:
|
||||
drone_spawn(em, vec2(
|
||||
player->body.pos.x + player->body.size.x * 0.5f,
|
||||
player->body.pos.y
|
||||
));
|
||||
picked_up = true;
|
||||
break;
|
||||
|
||||
case POWERUP_GUN:
|
||||
if (!player_has_gun(player)) {
|
||||
player_give_gun(player);
|
||||
picked_up = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (picked_up) {
|
||||
/* Pickup particles */
|
||||
Vec2 center = vec2(
|
||||
a->body.pos.x + a->body.size.x * 0.5f,
|
||||
a->body.pos.y + a->body.size.y * 0.5f
|
||||
);
|
||||
particle_emit_spark(center, (SDL_Color){255, 255, 100, 255});
|
||||
audio_play_sound(s_sfx_pickup, 80);
|
||||
|
||||
/* Destroy the powerup */
|
||||
a->flags |= ENTITY_DEAD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Exit zone checking ──────────────────────────── */
|
||||
|
||||
static void check_exit_zones(Level *level) {
|
||||
if (level->exit_triggered) return;
|
||||
if (level->map.exit_zone_count == 0) return;
|
||||
|
||||
/* Find the player */
|
||||
Entity *player = NULL;
|
||||
for (int i = 0; i < level->entities.count; i++) {
|
||||
Entity *e = &level->entities.entities[i];
|
||||
if (e->active && e->type == ENT_PLAYER && !(e->flags & ENTITY_DEAD)) {
|
||||
player = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!player) return;
|
||||
|
||||
for (int i = 0; i < level->map.exit_zone_count; i++) {
|
||||
const ExitZone *ez = &level->map.exit_zones[i];
|
||||
if (physics_aabb_overlap(
|
||||
player->body.pos, player->body.size,
|
||||
vec2(ez->x, ez->y), vec2(ez->w, ez->h))) {
|
||||
level->exit_triggered = true;
|
||||
snprintf(level->exit_target, sizeof(level->exit_target),
|
||||
"%s", ez->target);
|
||||
printf("Exit zone triggered -> %s\n",
|
||||
ez->target[0] ? ez->target : "(victory)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool level_exit_triggered(const Level *level) {
|
||||
return level->exit_triggered;
|
||||
}
|
||||
|
||||
void level_update(Level *level, float dt) {
|
||||
/* Don't update if exit already triggered (transition pending) */
|
||||
if (level->exit_triggered) return;
|
||||
|
||||
/* Start music on first update (deferred so browser audio context
|
||||
* is unlocked by the first user interaction / keypress) */
|
||||
if (!level->music_started && level->music.music) {
|
||||
@@ -297,6 +409,9 @@ void level_update(Level *level, float dt) {
|
||||
/* Handle collisions */
|
||||
handle_collisions(&level->entities);
|
||||
|
||||
/* Check exit zones */
|
||||
check_exit_zones(level);
|
||||
|
||||
/* Check for player respawn */
|
||||
for (int i = 0; i < level->entities.count; i++) {
|
||||
Entity *e = &level->entities.entities[i];
|
||||
@@ -347,6 +462,39 @@ void level_render(Level *level, float interpolation) {
|
||||
tilemap_render_layer(&level->map, level->map.collision_layer,
|
||||
cam, g_engine.renderer);
|
||||
|
||||
/* Render exit zones (pulsing glow on ground layer) */
|
||||
if (level->map.exit_zone_count > 0) {
|
||||
/* Pulse alpha between 40 and 100 using a sine wave */
|
||||
static float s_exit_pulse = 0.0f;
|
||||
s_exit_pulse += 3.0f * DT; /* ~3 Hz pulse */
|
||||
float pulse = 0.5f + 0.5f * sinf(s_exit_pulse);
|
||||
uint8_t alpha = (uint8_t)(40.0f + pulse * 60.0f);
|
||||
|
||||
SDL_Renderer *r = g_engine.renderer;
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||
|
||||
for (int i = 0; i < level->map.exit_zone_count; i++) {
|
||||
const ExitZone *ez = &level->map.exit_zones[i];
|
||||
Vec2 screen_pos = camera_world_to_screen(cam, vec2(ez->x, ez->y));
|
||||
float zoom = cam->zoom > 0.0f ? cam->zoom : 1.0f;
|
||||
|
||||
SDL_Rect rect = {
|
||||
(int)screen_pos.x,
|
||||
(int)screen_pos.y,
|
||||
(int)(ez->w * zoom + 0.5f),
|
||||
(int)(ez->h * zoom + 0.5f)
|
||||
};
|
||||
|
||||
/* Green/cyan fill */
|
||||
SDL_SetRenderDrawColor(r, 50, 230, 180, alpha);
|
||||
SDL_RenderFillRect(r, &rect);
|
||||
|
||||
/* Brighter border */
|
||||
SDL_SetRenderDrawColor(r, 80, 255, 200, (uint8_t)(alpha + 40));
|
||||
SDL_RenderDrawRect(r, &rect);
|
||||
}
|
||||
}
|
||||
|
||||
/* Render entities */
|
||||
entity_render_all(&level->entities, cam);
|
||||
|
||||
@@ -384,7 +532,17 @@ void level_render(Level *level, float interpolation) {
|
||||
/* Draw jetpack charge indicators */
|
||||
int charges, max_charges;
|
||||
float recharge_pct;
|
||||
if (player_get_dash_charges(player, &charges, &max_charges, &recharge_pct)) {
|
||||
bool boosted = false;
|
||||
if (player_get_dash_charges(player, &charges, &max_charges,
|
||||
&recharge_pct, &boosted)) {
|
||||
/* Blue when boosted, orange normally */
|
||||
SDL_Color full_color = boosted
|
||||
? (SDL_Color){50, 150, 255, 255}
|
||||
: (SDL_Color){255, 180, 50, 255};
|
||||
SDL_Color partial_color = boosted
|
||||
? (SDL_Color){40, 120, 200, 180}
|
||||
: (SDL_Color){200, 140, 40, 180};
|
||||
|
||||
for (int i = 0; i < max_charges; i++) {
|
||||
float bx = 8.0f + i * 10.0f;
|
||||
float by = 22.0f;
|
||||
@@ -396,15 +554,15 @@ void level_render(Level *level, float interpolation) {
|
||||
(SDL_Color){50, 50, 60, 255}, LAYER_HUD, cam);
|
||||
|
||||
if (i < charges) {
|
||||
/* Full charge — bright orange */
|
||||
/* Full charge */
|
||||
renderer_draw_rect(vec2(bx, by), vec2(bw, bh),
|
||||
(SDL_Color){255, 180, 50, 255}, LAYER_HUD, cam);
|
||||
full_color, LAYER_HUD, cam);
|
||||
} else if (i == charges) {
|
||||
/* Currently recharging — partial fill */
|
||||
float fill = recharge_pct * bw;
|
||||
if (fill > 0.5f) {
|
||||
renderer_draw_rect(vec2(bx, by), vec2(fill, bh),
|
||||
(SDL_Color){200, 140, 40, 180}, LAYER_HUD, cam);
|
||||
partial_color, LAYER_HUD, cam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user