#include "game/spacecraft.h" #include "engine/assets.h" #include "engine/renderer.h" #include "engine/particle.h" #include "engine/audio.h" #include "config.h" #include #include /* ═══════════════════════════════════════════════════ * 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; }