diff --git a/TODO.md b/TODO.md index b181969..609c9ca 100644 --- a/TODO.md +++ b/TODO.md @@ -21,10 +21,12 @@ sizes. Key areas to check: - Camera bounds and coordinate overflow (float precision at large coords) - Level file parsing (row lines could exceed fgets buffer at 5000+ columns) -## Spacecraft at level exit -Ship landing at exit zone when player approaches — player enters, ship takes -off, triggers level transition. The intro/start sequence is done; this is the -exit counterpart. +## ~~Spacecraft at level exit~~ ✓ +Implemented: `spacecraft_spawn_exit()` with `is_exit_ship` flag. Proximity +trigger in `level.c` spawns exit ship when player is within ~2 screen widths +of an exit zone. Ship flies in, lands near exit. Player overlaps landed ship → +player deactivated, ship takes off, camera holds still, level transition fires +after ship departs (SC_DONE). ## ~~Asteroid refinement~~ ✓ Implemented: base speed 120→200, accel 200→350, respawn 3→6s, stagger 0-3→0-8s, @@ -43,6 +45,27 @@ Extend the moon into a 3-level sequence connected by spacecraft takeoff/landing: - Moon exit transitions require the "spacecraft at level exit" feature so the ship flies in when the player approaches the exit zone. +## Level generator: height zones (verticality) +Add vertical variety to generated levels using a "height zones" approach: +- Double tilemap height from 23 to ~46 tiles (two screens tall) +- Define two elevation bands: HIGH (ground ~row 17) and LOW (ground ~row 40) +- Existing segment generators get a `y_offset` parameter so internal logic + barely changes — platforms, enemies, corridors all shift by offset +- New vertical connector segments (ramps/climbs) bridge between zones +- Theme decides zone usage: Surface stays LOW, Station uses both, Base uses HIGH +- Camera needs vertical look-ahead when player has vertical velocity + +### Implementation steps +1. Add vertical camera look-ahead (small Y lead when player moves vertically) +2. Create a handcrafted tall test level (~40x46) to validate camera, physics, + rendering, and entities all work with vertical scrolling +3. Refactor `set_tile()` / `fill_ground()` to accept a height parameter instead + of hardcoded `SEG_HEIGHT` +4. Add `y_offset` parameter to all segment generators +5. Add `SEG_CLIMB` connector segment type (shaft/platforms between zones) +6. Update `levelgen_generate()` to assign zones per segment and insert climbs +7. The station generator stays at 23 tiles (corridor envelope already constrains) + ## Jetpack boost blue flame effects - Add continuous blue flame particles trailing from the player during jetpack boost (while boost is active, not just on burst) diff --git a/src/game/level.c b/src/game/level.c index dc4ae47..dfa1625 100644 --- a/src/game/level.c +++ b/src/game/level.c @@ -360,21 +360,121 @@ static void handle_collisions(EntityManager *em) { } } -/* ── Exit zone checking ──────────────────────────── */ +/* ── Exit zone / exit ship ───────────────────────── */ -static void check_exit_zones(Level *level) { +/* How close the player must be to an exit zone to trigger the exit ship + * fly-in. Roughly 2 screen widths. */ +#define EXIT_SHIP_TRIGGER_DIST (SCREEN_WIDTH * 2.0f) + +/* Landing offset: where the ship lands relative to the exit zone center. + * The ship lands a bit to the left of the exit zone so the player walks + * rightward into it. */ +#define EXIT_SHIP_LAND_OFFSET_X (-SPACECRAFT_WIDTH * 0.5f) + +/* Find the active, alive player entity (or NULL). */ +static Entity *find_player(EntityManager *em) { + for (int i = 0; i < em->count; i++) { + Entity *e = &em->entities[i]; + if (e->active && e->type == ENT_PLAYER && !(e->flags & ENTITY_DEAD)) + return e; + } + return NULL; +} + +/* Find the exit spacecraft entity (or NULL). */ +static Entity *find_exit_ship(EntityManager *em) { + for (int i = 0; i < em->count; i++) { + Entity *e = &em->entities[i]; + if (e->active && e->type == ENT_SPACECRAFT && spacecraft_is_exit_ship(e)) + return e; + } + return NULL; +} + +/* Spawn the exit ship when the player gets close to an exit zone. */ +static void check_exit_ship_proximity(Level *level) { if (level->exit_triggered) return; + if (level->exit_ship_spawned) return; if (level->map.exit_zone_count == 0) return; - /* Find the player */ - 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 && !(e->flags & ENTITY_DEAD)) { - player = e; - break; + Entity *player = find_player(&level->entities); + if (!player) return; + + Vec2 pc = vec2( + player->body.pos.x + player->body.size.x * 0.5f, + player->body.pos.y + player->body.size.y * 0.5f + ); + + for (int i = 0; i < level->map.exit_zone_count; i++) { + const ExitZone *ez = &level->map.exit_zones[i]; + Vec2 ez_center = vec2(ez->x + ez->w * 0.5f, ez->y + ez->h * 0.5f); + float dx = pc.x - ez_center.x; + float dy = pc.y - ez_center.y; + float dist = sqrtf(dx * dx + dy * dy); + + if (dist < EXIT_SHIP_TRIGGER_DIST) { + /* Land the ship so its bottom aligns with the exit zone bottom. + * land_pos is the top-left of the ship's resting position. */ + float land_x = ez->x + ez->w * 0.5f + EXIT_SHIP_LAND_OFFSET_X; + float land_y = ez->y + ez->h - (float)SPACECRAFT_HEIGHT; + + Entity *ship = spacecraft_spawn_exit(&level->entities, + vec2(land_x, land_y)); + if (ship) { + level->exit_ship_spawned = true; + level->exit_zone_idx = i; + printf("Exit ship triggered (zone %d, dist=%.0f)\n", i, dist); + } + return; } } +} + +/* Handle the exit ship boarding and departure sequence. */ +static void update_exit_ship(Level *level) { + if (level->exit_triggered) return; + if (!level->exit_ship_spawned) return; + + Entity *ship = find_exit_ship(&level->entities); + + /* Ship may have been destroyed (SC_DONE) */ + if (!ship) { + if (level->exit_ship_boarded) { + /* Ship finished its departure — trigger the level exit */ + const ExitZone *ez = &level->map.exit_zones[level->exit_zone_idx]; + level->exit_triggered = true; + snprintf(level->exit_target, sizeof(level->exit_target), + "%s", ez->target); + printf("Exit ship departed -> %s\n", + ez->target[0] ? ez->target : "(victory)"); + } + return; + } + + /* Wait for the ship to land, then check for player boarding */ + if (!level->exit_ship_boarded && spacecraft_is_landed(ship)) { + Entity *player = find_player(&level->entities); + if (player) { + /* Check overlap between player and the landed ship */ + if (physics_overlap(&player->body, &ship->body)) { + /* Board the ship: deactivate the player, start takeoff */ + level->exit_ship_boarded = true; + player->active = false; + + spacecraft_takeoff(ship); + printf("Player boarded exit ship\n"); + } + } + } +} + +/* Direct exit zone overlap — fallback for levels without spacecraft exit. */ +static void check_exit_zones(Level *level) { + if (level->exit_triggered) return; + if (level->exit_ship_spawned) return; /* ship handles the exit instead */ + if (level->map.exit_zone_count == 0) return; + + Entity *player = find_player(&level->entities); if (!player) return; for (int i = 0; i < level->map.exit_zone_count; i++) { @@ -417,8 +517,9 @@ void level_update(Level *level, float dt) { for (int i = 0; i < level->entities.count; i++) { Entity *e = &level->entities.entities[i]; if (e->active && e->type == ENT_SPACECRAFT && + !spacecraft_is_exit_ship(e) && spacecraft_is_landed(e)) { - /* Ship has landed — spawn the player at spawn point */ + /* Intro ship has landed — spawn the player at spawn point */ Entity *player = player_spawn(&level->entities, level->map.player_spawn); if (player) { @@ -443,20 +544,26 @@ void level_update(Level *level, float dt) { if (level->player_spawned) { handle_collisions(&level->entities); - /* Check exit zones */ + /* Exit ship: proximity trigger + boarding/departure */ + check_exit_ship_proximity(level); + update_exit_ship(level); + + /* Fallback direct exit zone check (for levels without ship exit) */ 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); - 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 (skip if player boarded exit ship) */ + if (!level->exit_ship_boarded) { + 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; + } } } } @@ -464,9 +571,18 @@ void level_update(Level *level, float dt) { /* Update particles */ particle_update(dt); - /* Camera tracking — follow player if spawned, otherwise follow spacecraft */ + /* Camera tracking: + * 1. Exit ship boarded → hold camera still (ship flies out of frame) + * 2. Player spawned → follow the player + * 3. Intro ship flying in → follow the spacecraft */ bool cam_tracked = false; - if (level->player_spawned) { + + if (level->exit_ship_boarded) { + /* Camera stays put — the ship flies out of frame on its own */ + cam_tracked = true; + } + + if (!cam_tracked && 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) { @@ -481,11 +597,13 @@ void level_update(Level *level, float dt) { } } } + 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) { + if (e->active && e->type == ENT_SPACECRAFT && + !spacecraft_is_exit_ship(e)) { Vec2 center = vec2( e->body.pos.x + e->body.size.x * 0.5f, e->body.pos.y + e->body.size.y * 0.5f diff --git a/src/game/level.h b/src/game/level.h index 0188939..f42d3f0 100644 --- a/src/game/level.h +++ b/src/game/level.h @@ -22,6 +22,11 @@ typedef struct Level { /* ── Intro sequence state ────────────── */ bool has_intro_ship; /* level has a spacecraft intro */ bool player_spawned; /* player has been spawned */ + + /* ── Exit ship sequence state ────────── */ + bool exit_ship_spawned; /* exit spacecraft has been spawned */ + bool exit_ship_boarded; /* player has entered the ship */ + int exit_zone_idx; /* which exit zone the ship is for */ } Level; bool level_load(Level *level, const char *path); diff --git a/src/game/spacecraft.c b/src/game/spacecraft.c index ba7e0d1..539c4d4 100644 --- a/src/game/spacecraft.c +++ b/src/game/spacecraft.c @@ -350,6 +350,48 @@ Entity *spacecraft_spawn(EntityManager *em, Vec2 land_pos) { return e; } +Entity *spacecraft_spawn_exit(EntityManager *em, Vec2 land_pos) { + load_texture(); + + Vec2 target = land_pos; + + /* Start off-screen: above and to the right (same approach as intro) */ + 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; + 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 = true; + sd->engine_channel = -1; + e->data = sd; + + engine_start(sd); + + printf("Exit 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; @@ -370,3 +412,9 @@ bool spacecraft_is_landed(const Entity *ship) { const SpacecraftData *sd = (const SpacecraftData *)ship->data; return sd->state == SC_LANDED; } + +bool spacecraft_is_exit_ship(const Entity *ship) { + if (!ship || !ship->data) return false; + const SpacecraftData *sd = (const SpacecraftData *)ship->data; + return sd->is_exit_ship; +} diff --git a/src/game/spacecraft.h b/src/game/spacecraft.h index 75dc26d..53140ff 100644 --- a/src/game/spacecraft.h +++ b/src/game/spacecraft.h @@ -55,9 +55,14 @@ typedef struct SpacecraftData { void spacecraft_register(EntityManager *em); /* Spawn a spacecraft that will fly in and land at the given position. - * The position is the bottom-center of where the ship should land. */ + * The position is the top-left of where the ship should rest (from .lvl ENTITY). */ Entity *spacecraft_spawn(EntityManager *em, Vec2 land_pos); +/* Spawn an exit spacecraft that flies in from the right and lands at the + * given position. Used for the level-exit sequence. The is_exit_ship flag + * is set to true so level code can identify this ship. */ +Entity *spacecraft_spawn_exit(EntityManager *em, Vec2 land_pos); + /* Trigger takeoff on an existing landed spacecraft */ void spacecraft_takeoff(Entity *ship); @@ -67,4 +72,7 @@ bool spacecraft_is_done(const Entity *ship); /* Check if the spacecraft is in the landed state */ bool spacecraft_is_landed(const Entity *ship); +/* Check if this is the exit ship */ +bool spacecraft_is_exit_ship(const Entity *ship); + #endif /* JNR_SPACECRAFT_H */