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:
Thomas
2026-03-01 11:00:51 +00:00
parent dbb507bfd2
commit 49ed2d6f7b
33 changed files with 1026 additions and 61 deletions

View File

@@ -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);