#include "game/level.h" #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" #include "engine/particle.h" #include "engine/audio.h" #include "engine/input.h" #include "engine/camera.h" #include "engine/assets.h" #include #include /* ── Sound effects ───────────────────────────────── */ static Sound s_sfx_hit; static Sound s_sfx_enemy_death; static bool s_sfx_loaded = false; /* ── 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); particle_init(); /* Load combat sound effects */ 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_loaded = true; } /* Generate spritesheet */ sprites_init_anims(); if (!sprites_generate(g_engine.renderer)) { fprintf(stderr, "Warning: failed to generate spritesheet\n"); } /* Register all entity types via the central registry */ entity_registry_init(&level->entities); /* Apply level gravity (0 = use default) */ if (level->map.gravity > 0) { physics_set_gravity(level->map.gravity); } else { physics_set_gravity(DEFAULT_GRAVITY); } /* Apply level background color */ if (level->map.has_bg_color) { renderer_set_clear_color(level->map.bg_color); } /* Initialize parallax backgrounds */ parallax_init(&level->parallax); if (level->map.parallax_far_path[0]) { SDL_Texture *far_tex = assets_get_texture(level->map.parallax_far_path); if (far_tex) parallax_set_far(&level->parallax, far_tex, 0.05f, 0.05f); } if (level->map.parallax_near_path[0]) { SDL_Texture *near_tex = assets_get_texture(level->map.parallax_near_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 && !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 */ camera_set_bounds(&level->camera, (float)(level->map.width * TILE_SIZE), (float)(level->map.height * TILE_SIZE)); /* Spawn player at map spawn point */ Entity *player = player_spawn(&level->entities, level->map.player_spawn); if (!player) { fprintf(stderr, "Failed to spawn player!\n"); return 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]; Vec2 pos = vec2(es->x, es->y); entity_registry_spawn(&level->entities, es->type_name, pos); } /* Load level music (playback deferred to first update — * browsers require user interaction before playing audio) */ if (level->map.music_path[0]) { level->music = audio_load_music(level->map.music_path); } level->music_started = false; printf("Level loaded successfully.\n"); 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 */ static Camera *s_active_camera = NULL; static void damage_entity(Entity *target, int damage) { target->health -= damage; if (target->health <= 0) { target->flags |= ENTITY_DEAD; /* Death particles — centered on entity */ Vec2 center = vec2( target->body.pos.x + target->body.size.x * 0.5f, target->body.pos.y + target->body.size.y * 0.5f ); SDL_Color death_color; if (target->type == ENT_ENEMY_GRUNT) { 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 */ } particle_emit_death_puff(center, death_color); /* Screen shake on kill */ if (s_active_camera) { camera_shake(s_active_camera, 2.0f, 0.15f); } audio_play_sound(s_sfx_enemy_death, 80); } } static void damage_player(Entity *player, int damage, Entity *source) { PlayerData *ppd = (PlayerData *)player->data; damage_entity(player, damage); /* Screen shake on player hit (stronger) */ if (s_active_camera) { camera_shake(s_active_camera, 4.0f, 0.2f); } audio_play_sound(s_sfx_hit, 100); if (player->health > 0 && ppd) { ppd->inv_timer = PLAYER_INV_TIME; player->flags |= ENTITY_INVINCIBLE; /* Knockback away from source */ if (source) { float knock_dir = (player->body.pos.x < source->body.pos.x) ? -1.0f : 1.0f; player->body.vel.x = knock_dir * 150.0f; player->body.vel.y = -150.0f; } } } static bool is_enemy(const Entity *e) { return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER || e->type == ENT_TURRET; } static void handle_collisions(EntityManager *em) { /* Find the player */ Entity *player = NULL; for (int i = 0; i < em->count; i++) { Entity *e = &em->entities[i]; if (e->active && e->type == ENT_PLAYER && !(e->flags & ENTITY_DEAD)) { player = e; break; } } for (int i = 0; i < em->count; i++) { Entity *a = &em->entities[i]; if (!a->active) continue; /* ── Projectile vs entities ──────────── */ if (a->type == ENT_PROJECTILE) { if (projectile_is_impacting(a)) continue; bool from_player = projectile_is_from_player(a); for (int j = 0; j < em->count; j++) { Entity *b = &em->entities[j]; if (!b->active || b == a) continue; if (b->flags & ENTITY_DEAD) continue; bool hit = false; /* Player bullet hits enemies */ if (from_player && is_enemy(b)) { if (physics_overlap(&a->body, &b->body)) { damage_entity(b, a->damage); hit = true; } } /* Enemy bullet hits player */ if (!from_player && b->type == ENT_PLAYER && !(b->flags & ENTITY_INVINCIBLE)) { if (physics_overlap(&a->body, &b->body)) { damage_player(b, a->damage, NULL); hit = true; } } if (hit) { projectile_hit(a); /* If projectile was destroyed or is impacting, stop checking */ if (!a->active || projectile_is_impacting(a)) break; } } } /* ── Enemy contact damage to player ──── */ if (player && !(player->flags & ENTITY_INVINCIBLE) && is_enemy(a) && !(a->flags & ENTITY_DEAD)) { if (physics_overlap(&a->body, &player->body)) { /* Check if player is stomping (falling onto enemy from above) */ bool stomping = (player->body.vel.y > 0) && (player->body.pos.y + player->body.size.y < a->body.pos.y + a->body.size.y * 0.5f); if (stomping) { damage_entity(a, 2); player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f; } else { damage_player(player, a->damage, a); } } } } } void level_update(Level *level, float dt) { /* 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) { audio_set_music_volume(64); audio_play_music(level->music, true); level->music_started = true; } /* Set camera pointer for collision shake triggers */ s_active_camera = &level->camera; /* Update all entities */ entity_update_all(&level->entities, dt, &level->map); /* Handle collisions */ handle_collisions(&level->entities); /* Check for player respawn */ for (int i = 0; i < level->entities.count; i++) { Entity *e = &level->entities.entities[i]; if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) { player_respawn(e, level->map.player_spawn); /* Re-snap camera to player immediately */ Vec2 center = vec2( e->body.pos.x + e->body.size.x * 0.5f, e->body.pos.y + e->body.size.y * 0.5f ); camera_follow(&level->camera, center, vec2_zero(), dt); break; } } /* Update particles */ particle_update(dt); /* Find player for camera tracking */ for (int i = 0; i < level->entities.count; i++) { Entity *e = &level->entities.entities[i]; if (e->active && e->type == ENT_PLAYER) { float look_offset = player_get_look_up_offset(e); Vec2 center = vec2( e->body.pos.x + e->body.size.x * 0.5f, e->body.pos.y + e->body.size.y * 0.5f + look_offset ); camera_follow(&level->camera, center, e->body.vel, dt); break; } } /* Update screen shake */ camera_update_shake(&level->camera, dt); } void level_render(Level *level, float interpolation) { (void)interpolation; /* TODO: use for render interpolation */ Camera *cam = &level->camera; /* Render parallax backgrounds (behind everything) */ parallax_render(&level->parallax, cam, g_engine.renderer); /* Render tile layers */ tilemap_render_layer(&level->map, level->map.bg_layer, cam, g_engine.renderer); tilemap_render_layer(&level->map, level->map.collision_layer, cam, g_engine.renderer); /* Render entities */ entity_render_all(&level->entities, cam); /* Render particles (between entities and foreground) */ particle_render(cam); /* Render foreground tiles */ tilemap_render_layer(&level->map, level->map.fg_layer, cam, g_engine.renderer); /* Render HUD - health display */ 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) { player = e; break; } } if (player) { /* Draw health hearts */ for (int i = 0; i < player->max_health; i++) { SDL_Color heart_color; if (i < player->health) { heart_color = (SDL_Color){220, 50, 50, 255}; /* red = full */ } else { heart_color = (SDL_Color){80, 80, 80, 255}; /* grey = empty */ } Vec2 pos = vec2(8.0f + i * 14.0f, 8.0f); Vec2 size = vec2(10.0f, 10.0f); renderer_draw_rect(pos, size, heart_color, LAYER_HUD, cam); } /* Draw jetpack charge indicators */ int charges, max_charges; float recharge_pct; if (player_get_dash_charges(player, &charges, &max_charges, &recharge_pct)) { for (int i = 0; i < max_charges; i++) { float bx = 8.0f + i * 10.0f; float by = 22.0f; float bw = 7.0f; float bh = 5.0f; /* Background (empty slot) */ renderer_draw_rect(vec2(bx, by), vec2(bw, bh), (SDL_Color){50, 50, 60, 255}, LAYER_HUD, cam); if (i < charges) { /* Full charge — bright orange */ renderer_draw_rect(vec2(bx, by), vec2(bw, bh), (SDL_Color){255, 180, 50, 255}, 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); } } } } } /* Flush the renderer */ renderer_flush(cam); } 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); tilemap_free(&level->map); /* Free spritesheet */ if (g_spritesheet) { SDL_DestroyTexture(g_spritesheet); g_spritesheet = NULL; } }