forked from tas/major_tom
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.
373 lines
12 KiB
C
373 lines
12 KiB
C
#include "game/spacecraft.h"
|
|
#include "engine/assets.h"
|
|
#include "engine/renderer.h"
|
|
#include "engine/particle.h"
|
|
#include "engine/audio.h"
|
|
#include "config.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Constants
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
#define SC_FLY_SPEED 180.0f /* px/s during fly-in / fly-out */
|
|
#define SC_LAND_DURATION 1.6f /* seconds for the landing sequence */
|
|
#define SC_TAKEOFF_DURATION 1.2f /* seconds for the takeoff sequence */
|
|
|
|
/* Spritesheet frame indices (each 80x48, laid out horizontally) */
|
|
#define SC_FRAME_LANDED 0
|
|
#define SC_FRAME_LAND1 1
|
|
#define SC_FRAME_LAND2 2
|
|
#define SC_FRAME_LAND3 3
|
|
#define SC_FRAME_LAND4 4
|
|
#define SC_FRAME_COUNT 5
|
|
|
|
/* How far off-screen the ship starts / exits */
|
|
#define SC_OFFSCREEN_DIST 200.0f
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Static state
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
static EntityManager *s_em = NULL;
|
|
static SDL_Texture *s_texture = NULL;
|
|
static bool s_texture_loaded = false;
|
|
|
|
/* ── Sound effects ──────────────────────────────── */
|
|
static Sound s_sfx_engine;
|
|
static Sound s_sfx_synth;
|
|
static bool s_sfx_loaded = false;
|
|
|
|
static void load_sfx(void) {
|
|
if (s_sfx_loaded) return;
|
|
s_sfx_engine = audio_load_sound("assets/sounds/engine.wav");
|
|
s_sfx_synth = audio_load_sound("assets/sounds/synth.wav");
|
|
s_sfx_loaded = true;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Helpers
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
static void load_texture(void) {
|
|
if (s_texture_loaded) return;
|
|
s_texture = assets_get_texture("assets/sprites/spacecraft.png");
|
|
s_texture_loaded = true;
|
|
}
|
|
|
|
/* Get the source rect for a given frame index (in PNG pixel coords) */
|
|
static SDL_Rect frame_rect(int frame_idx) {
|
|
return (SDL_Rect){
|
|
frame_idx * SPACECRAFT_SRC_W, 0,
|
|
SPACECRAFT_SRC_W, SPACECRAFT_SRC_H
|
|
};
|
|
}
|
|
|
|
/* Pick the landing animation frame based on progress (0.0 = start, 1.0 = landed) */
|
|
static int landing_frame(float progress) {
|
|
if (progress < 0.25f) return SC_FRAME_LAND1;
|
|
if (progress < 0.50f) return SC_FRAME_LAND2;
|
|
if (progress < 0.75f) return SC_FRAME_LAND3;
|
|
if (progress < 0.95f) return SC_FRAME_LAND4;
|
|
return SC_FRAME_LANDED;
|
|
}
|
|
|
|
/* Pick the takeoff animation frame (reverse of landing) */
|
|
static int takeoff_frame(float progress) {
|
|
return landing_frame(1.0f - progress);
|
|
}
|
|
|
|
/* Emit thruster particles behind the ship */
|
|
static void emit_thruster_particles(Vec2 ship_pos, float intensity) {
|
|
if (intensity <= 0.0f) return;
|
|
|
|
/* Particles emit from the left (rear) center of the ship */
|
|
Vec2 origin = vec2(
|
|
ship_pos.x + 8.0f,
|
|
ship_pos.y + SPACECRAFT_HEIGHT * 0.5f
|
|
);
|
|
|
|
int count = (int)(intensity * 3.0f);
|
|
if (count < 1) count = 1;
|
|
|
|
ParticleBurst b = {
|
|
.origin = origin,
|
|
.count = count,
|
|
.speed_min = 30.0f * intensity,
|
|
.speed_max = 80.0f * intensity,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.3f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.5f,
|
|
.spread = 0.6f,
|
|
.direction = 3.14159f, /* leftward */
|
|
.drag = 2.0f,
|
|
.gravity_scale = 0.0f,
|
|
.color = (SDL_Color){255, 180, 50, 200},
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&b);
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Entity callbacks
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
/* Start the engine loop sound, stopping any previous one */
|
|
static void engine_start(SpacecraftData *sd) {
|
|
if (sd->engine_channel >= 0) return; /* already playing */
|
|
sd->engine_channel = audio_play_sound_loop(s_sfx_engine, 70);
|
|
}
|
|
|
|
/* Stop the engine loop sound */
|
|
static void engine_stop(SpacecraftData *sd) {
|
|
if (sd->engine_channel < 0) return;
|
|
audio_stop_channel(sd->engine_channel);
|
|
sd->engine_channel = -1;
|
|
}
|
|
|
|
static void spacecraft_update(Entity *self, float dt, const Tilemap *map) {
|
|
(void)map;
|
|
SpacecraftData *sd = (SpacecraftData *)self->data;
|
|
if (!sd) return;
|
|
|
|
sd->state_timer += dt;
|
|
|
|
switch (sd->state) {
|
|
|
|
case SC_FLYING_IN: {
|
|
/* Move from start_pos toward target_pos */
|
|
Vec2 dir = vec2_sub(sd->target_pos, sd->start_pos);
|
|
float total_dist = vec2_len(dir);
|
|
if (total_dist < 1.0f) {
|
|
/* Already at target */
|
|
engine_stop(sd);
|
|
sd->state = SC_LANDING;
|
|
sd->state_timer = 0.0f;
|
|
self->body.pos = sd->target_pos;
|
|
break;
|
|
}
|
|
Vec2 norm = vec2_scale(dir, 1.0f / total_dist);
|
|
|
|
float traveled = sd->state_timer * sd->fly_speed;
|
|
if (traveled >= total_dist) {
|
|
/* Arrived at landing zone */
|
|
engine_stop(sd);
|
|
self->body.pos = sd->target_pos;
|
|
sd->state = SC_LANDING;
|
|
sd->state_timer = 0.0f;
|
|
} else {
|
|
self->body.pos = vec2_add(sd->start_pos,
|
|
vec2_scale(norm, traveled));
|
|
emit_thruster_particles(self->body.pos, 1.0f);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SC_LANDING: {
|
|
float progress = sd->state_timer / SC_LAND_DURATION;
|
|
if (progress >= 1.0f) {
|
|
progress = 1.0f;
|
|
self->body.pos = sd->target_pos;
|
|
sd->state = SC_LANDED;
|
|
sd->state_timer = 0.0f;
|
|
/* Play synth on touchdown */
|
|
audio_play_sound(s_sfx_synth, 80);
|
|
} else {
|
|
/* Ease down from slightly above target to target */
|
|
float height_offset = (1.0f - progress) * 30.0f;
|
|
self->body.pos = vec2(
|
|
sd->target_pos.x,
|
|
sd->target_pos.y - height_offset
|
|
);
|
|
emit_thruster_particles(self->body.pos, 1.0f - progress);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SC_LANDED: {
|
|
self->body.pos = sd->target_pos;
|
|
/* Stays landed until spacecraft_takeoff() is called externally
|
|
* (e.g. by level.c after spawning the player). */
|
|
break;
|
|
}
|
|
|
|
case SC_TAKEOFF: {
|
|
float progress = sd->state_timer / SC_TAKEOFF_DURATION;
|
|
if (progress >= 1.0f) {
|
|
sd->state = SC_FLYING_OUT;
|
|
sd->state_timer = 0.0f;
|
|
engine_start(sd);
|
|
} else {
|
|
float height_offset = progress * 30.0f;
|
|
self->body.pos = vec2(
|
|
sd->target_pos.x,
|
|
sd->target_pos.y - height_offset
|
|
);
|
|
emit_thruster_particles(self->body.pos, progress);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SC_FLYING_OUT: {
|
|
/* Fly upward and to the right, off-screen */
|
|
float speed = sd->fly_speed * 1.5f;
|
|
self->body.pos.x += speed * 0.7f * dt;
|
|
self->body.pos.y -= speed * dt;
|
|
|
|
emit_thruster_particles(self->body.pos, 1.0f);
|
|
|
|
/* Check if far enough off-screen */
|
|
if (self->body.pos.y < -SC_OFFSCREEN_DIST ||
|
|
self->body.pos.x > sd->target_pos.x + SC_OFFSCREEN_DIST * 2.0f) {
|
|
engine_stop(sd);
|
|
sd->state = SC_DONE;
|
|
sd->state_timer = 0.0f;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SC_DONE:
|
|
/* Remove the entity */
|
|
entity_destroy(s_em, self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void spacecraft_render(Entity *self, const Camera *cam) {
|
|
(void)cam;
|
|
SpacecraftData *sd = (SpacecraftData *)self->data;
|
|
if (!sd || !s_texture) return;
|
|
|
|
/* Determine which frame to show */
|
|
int frame = SC_FRAME_LANDED;
|
|
switch (sd->state) {
|
|
case SC_FLYING_IN:
|
|
frame = SC_FRAME_LAND1; /* thrusters on while flying */
|
|
break;
|
|
case SC_LANDING: {
|
|
float progress = sd->state_timer / SC_LAND_DURATION;
|
|
if (progress > 1.0f) progress = 1.0f;
|
|
frame = landing_frame(progress);
|
|
break;
|
|
}
|
|
case SC_LANDED:
|
|
frame = SC_FRAME_LANDED;
|
|
break;
|
|
case SC_TAKEOFF: {
|
|
float progress = sd->state_timer / SC_TAKEOFF_DURATION;
|
|
if (progress > 1.0f) progress = 1.0f;
|
|
frame = takeoff_frame(progress);
|
|
break;
|
|
}
|
|
case SC_FLYING_OUT:
|
|
frame = SC_FRAME_LAND1;
|
|
break;
|
|
case SC_DONE:
|
|
return;
|
|
}
|
|
|
|
SDL_Rect src = frame_rect(frame);
|
|
|
|
Sprite spr = {
|
|
.texture = s_texture,
|
|
.src = src,
|
|
.pos = self->body.pos,
|
|
.size = vec2(SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT),
|
|
.flip_x = false,
|
|
.flip_y = false,
|
|
.layer = LAYER_ENTITIES,
|
|
.alpha = 255,
|
|
.rotation = 0.0,
|
|
};
|
|
renderer_submit(&spr);
|
|
}
|
|
|
|
static void spacecraft_destroy(Entity *self) {
|
|
if (self->data) {
|
|
SpacecraftData *sd = (SpacecraftData *)self->data;
|
|
engine_stop(sd);
|
|
free(self->data);
|
|
self->data = NULL;
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Public API
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
void spacecraft_register(EntityManager *em) {
|
|
entity_register(em, ENT_SPACECRAFT,
|
|
spacecraft_update, spacecraft_render, spacecraft_destroy);
|
|
s_em = em;
|
|
load_texture();
|
|
load_sfx();
|
|
}
|
|
|
|
Entity *spacecraft_spawn(EntityManager *em, Vec2 land_pos) {
|
|
load_texture();
|
|
|
|
/* land_pos is the top-left of where the ship should rest on the
|
|
* ground (as provided by the ENTITY directive in .lvl files). */
|
|
Vec2 target = land_pos;
|
|
|
|
/* Start off-screen: above and to the right */
|
|
Vec2 start = vec2(
|
|
target.x + SC_OFFSCREEN_DIST,
|
|
target.y - SC_OFFSCREEN_DIST
|
|
);
|
|
|
|
Entity *e = entity_spawn(em, ENT_SPACECRAFT, start);
|
|
if (!e) return NULL;
|
|
|
|
e->body.size = vec2(SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT);
|
|
e->body.gravity_scale = 0.0f; /* manual movement, no physics */
|
|
e->health = 999;
|
|
e->max_health = 999;
|
|
e->damage = 0;
|
|
e->flags |= ENTITY_INVINCIBLE;
|
|
|
|
SpacecraftData *sd = calloc(1, sizeof(SpacecraftData));
|
|
if (!sd) {
|
|
entity_destroy(em, e);
|
|
return NULL;
|
|
}
|
|
sd->state = SC_FLYING_IN;
|
|
sd->state_timer = 0.0f;
|
|
sd->target_pos = target;
|
|
sd->start_pos = start;
|
|
sd->fly_speed = SC_FLY_SPEED;
|
|
sd->is_exit_ship = false;
|
|
sd->engine_channel = -1;
|
|
e->data = sd;
|
|
|
|
/* Start engine sound for fly-in */
|
|
engine_start(sd);
|
|
|
|
printf("Spacecraft spawned (fly-in to %.0f, %.0f)\n",
|
|
target.x, target.y);
|
|
return e;
|
|
}
|
|
|
|
void spacecraft_takeoff(Entity *ship) {
|
|
if (!ship || !ship->data) return;
|
|
SpacecraftData *sd = (SpacecraftData *)ship->data;
|
|
if (sd->state == SC_LANDED) {
|
|
sd->state = SC_TAKEOFF;
|
|
sd->state_timer = 0.0f;
|
|
}
|
|
}
|
|
|
|
bool spacecraft_is_done(const Entity *ship) {
|
|
if (!ship || !ship->data) return true;
|
|
const SpacecraftData *sd = (const SpacecraftData *)ship->data;
|
|
return sd->state == SC_DONE;
|
|
}
|
|
|
|
bool spacecraft_is_landed(const Entity *ship) {
|
|
if (!ship || !ship->data) return false;
|
|
const SpacecraftData *sd = (const SpacecraftData *)ship->data;
|
|
return sd->state == SC_LANDED;
|
|
}
|