Add spatial audio, spacecraft entity, and level intro sequence
Distance-based sound effects with stereo panning for explosions, impacts, and pickups. Spacecraft entity with full state machine (fly-in, land, take off, fly-out), engine/synth sound loops, thruster particles, and PNG spritesheet. Moon level intro defers player spawn until ship lands. Also untrack build/ objects that were committed by mistake.
This commit is contained in:
148
src/game/level.c
148
src/game/level.c
@@ -5,6 +5,7 @@
|
||||
#include "game/hazards.h"
|
||||
#include "game/powerup.h"
|
||||
#include "game/drone.h"
|
||||
#include "game/spacecraft.h"
|
||||
#include "game/sprites.h"
|
||||
#include "game/entity_registry.h"
|
||||
#include "engine/core.h"
|
||||
@@ -36,7 +37,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_pickup = audio_load_sound("assets/sounds/powerUp.wav");
|
||||
s_sfx_loaded = true;
|
||||
}
|
||||
|
||||
@@ -92,24 +93,31 @@ static bool level_setup(Level *level) {
|
||||
(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;
|
||||
}
|
||||
|
||||
/* 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) */
|
||||
level->has_intro_ship = false;
|
||||
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);
|
||||
if (strcmp(es->type_name, "spacecraft") == 0) {
|
||||
level->has_intro_ship = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spawn player — deferred if the level has an intro spacecraft */
|
||||
if (level->has_intro_ship) {
|
||||
level->player_spawned = false;
|
||||
} else {
|
||||
Entity *player = player_spawn(&level->entities, level->map.player_spawn);
|
||||
if (!player) {
|
||||
fprintf(stderr, "Failed to spawn player!\n");
|
||||
return false;
|
||||
}
|
||||
if (level->map.player_unarmed) {
|
||||
PlayerData *ppd = (PlayerData *)player->data;
|
||||
if (ppd) ppd->has_gun = false;
|
||||
}
|
||||
level->player_spawned = true;
|
||||
}
|
||||
|
||||
/* Load level music (playback deferred to first update —
|
||||
@@ -184,7 +192,7 @@ static void damage_entity(Entity *target, int damage) {
|
||||
camera_shake(s_active_camera, 2.0f, 0.15f);
|
||||
}
|
||||
|
||||
audio_play_sound(s_sfx_enemy_death, 80);
|
||||
audio_play_sound_at(s_sfx_enemy_death, 80, center, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +350,7 @@ static void handle_collisions(EntityManager *em) {
|
||||
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);
|
||||
audio_play_sound_at(s_sfx_pickup, 80, center, 0);
|
||||
|
||||
/* Destroy the powerup */
|
||||
a->flags |= ENTITY_DEAD;
|
||||
@@ -403,46 +411,100 @@ void level_update(Level *level, float dt) {
|
||||
/* Set camera pointer for collision shake triggers */
|
||||
s_active_camera = &level->camera;
|
||||
|
||||
/* ── Deferred player spawn: wait for spacecraft to land ── */
|
||||
if (level->has_intro_ship && !level->player_spawned) {
|
||||
/* Find the spacecraft and check if it has landed */
|
||||
for (int i = 0; i < level->entities.count; i++) {
|
||||
Entity *e = &level->entities.entities[i];
|
||||
if (e->active && e->type == ENT_SPACECRAFT &&
|
||||
spacecraft_is_landed(e)) {
|
||||
/* Ship has landed — spawn the player at spawn point */
|
||||
Entity *player = player_spawn(&level->entities,
|
||||
level->map.player_spawn);
|
||||
if (player) {
|
||||
if (level->map.player_unarmed) {
|
||||
PlayerData *ppd = (PlayerData *)player->data;
|
||||
if (ppd) ppd->has_gun = false;
|
||||
}
|
||||
level->player_spawned = true;
|
||||
|
||||
/* Trigger takeoff now that the player has exited */
|
||||
spacecraft_takeoff(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Update all entities */
|
||||
entity_update_all(&level->entities, dt, &level->map);
|
||||
|
||||
/* Handle collisions */
|
||||
handle_collisions(&level->entities);
|
||||
/* Handle collisions (only meaningful once player exists) */
|
||||
if (level->player_spawned) {
|
||||
handle_collisions(&level->entities);
|
||||
|
||||
/* Check exit zones */
|
||||
check_exit_zones(level);
|
||||
/* 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];
|
||||
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;
|
||||
/* 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);
|
||||
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;
|
||||
/* Camera tracking — follow player if spawned, otherwise follow spacecraft */
|
||||
bool cam_tracked = false;
|
||||
if (level->player_spawned) {
|
||||
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);
|
||||
cam_tracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cam_tracked) {
|
||||
/* Follow the spacecraft during intro */
|
||||
for (int i = 0; i < level->entities.count; i++) {
|
||||
Entity *e = &level->entities.entities[i];
|
||||
if (e->active && e->type == ENT_SPACECRAFT) {
|
||||
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);
|
||||
cam_tracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set audio listener to camera center */
|
||||
if (cam_tracked) {
|
||||
Vec2 listener = vec2(
|
||||
level->camera.pos.x + level->camera.viewport.x * 0.5f,
|
||||
level->camera.pos.y + level->camera.viewport.y * 0.5f
|
||||
);
|
||||
audio_set_listener(listener);
|
||||
}
|
||||
|
||||
/* Update screen shake */
|
||||
camera_update_shake(&level->camera, dt);
|
||||
|
||||
Reference in New Issue
Block a user