forked from tas/major_tom
Add spacecraft exit sequence at level exit zones
Spawn an exit ship when the player approaches an exit zone. The ship flies in, lands near the exit, and waits for the player to board. On boarding the player is deactivated, the ship takes off, and the level transition fires after departure.
This commit is contained in:
31
TODO.md
31
TODO.md
@@ -21,10 +21,12 @@ sizes. Key areas to check:
|
|||||||
- Camera bounds and coordinate overflow (float precision at large coords)
|
- Camera bounds and coordinate overflow (float precision at large coords)
|
||||||
- Level file parsing (row lines could exceed fgets buffer at 5000+ columns)
|
- Level file parsing (row lines could exceed fgets buffer at 5000+ columns)
|
||||||
|
|
||||||
## Spacecraft at level exit
|
## ~~Spacecraft at level exit~~ ✓
|
||||||
Ship landing at exit zone when player approaches — player enters, ship takes
|
Implemented: `spacecraft_spawn_exit()` with `is_exit_ship` flag. Proximity
|
||||||
off, triggers level transition. The intro/start sequence is done; this is the
|
trigger in `level.c` spawns exit ship when player is within ~2 screen widths
|
||||||
exit counterpart.
|
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~~ ✓
|
## ~~Asteroid refinement~~ ✓
|
||||||
Implemented: base speed 120→200, accel 200→350, respawn 3→6s, stagger 0-3→0-8s,
|
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
|
- Moon exit transitions require the "spacecraft at level exit" feature so
|
||||||
the ship flies in when the player approaches the exit zone.
|
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
|
## Jetpack boost blue flame effects
|
||||||
- Add continuous blue flame particles trailing from the player during jetpack
|
- Add continuous blue flame particles trailing from the player during jetpack
|
||||||
boost (while boost is active, not just on burst)
|
boost (while boost is active, not just on burst)
|
||||||
|
|||||||
168
src/game/level.c
168
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_triggered) return;
|
||||||
|
if (level->exit_ship_spawned) return;
|
||||||
if (level->map.exit_zone_count == 0) return;
|
if (level->map.exit_zone_count == 0) return;
|
||||||
|
|
||||||
/* Find the player */
|
Entity *player = find_player(&level->entities);
|
||||||
Entity *player = NULL;
|
if (!player) return;
|
||||||
for (int i = 0; i < level->entities.count; i++) {
|
|
||||||
Entity *e = &level->entities.entities[i];
|
Vec2 pc = vec2(
|
||||||
if (e->active && e->type == ENT_PLAYER && !(e->flags & ENTITY_DEAD)) {
|
player->body.pos.x + player->body.size.x * 0.5f,
|
||||||
player = e;
|
player->body.pos.y + player->body.size.y * 0.5f
|
||||||
break;
|
);
|
||||||
|
|
||||||
|
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;
|
if (!player) return;
|
||||||
|
|
||||||
for (int i = 0; i < level->map.exit_zone_count; i++) {
|
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++) {
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
Entity *e = &level->entities.entities[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) &&
|
||||||
spacecraft_is_landed(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,
|
Entity *player = player_spawn(&level->entities,
|
||||||
level->map.player_spawn);
|
level->map.player_spawn);
|
||||||
if (player) {
|
if (player) {
|
||||||
@@ -443,20 +544,26 @@ void level_update(Level *level, float dt) {
|
|||||||
if (level->player_spawned) {
|
if (level->player_spawned) {
|
||||||
handle_collisions(&level->entities);
|
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_exit_zones(level);
|
||||||
|
|
||||||
/* Check for player respawn */
|
/* Check for player respawn (skip if player boarded exit ship) */
|
||||||
for (int i = 0; i < level->entities.count; i++) {
|
if (!level->exit_ship_boarded) {
|
||||||
Entity *e = &level->entities.entities[i];
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
|
Entity *e = &level->entities.entities[i];
|
||||||
player_respawn(e, level->map.player_spawn);
|
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
|
||||||
Vec2 center = vec2(
|
player_respawn(e, level->map.player_spawn);
|
||||||
e->body.pos.x + e->body.size.x * 0.5f,
|
Vec2 center = vec2(
|
||||||
e->body.pos.y + e->body.size.y * 0.5f
|
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;
|
camera_follow(&level->camera, center, vec2_zero(), dt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,9 +571,18 @@ void level_update(Level *level, float dt) {
|
|||||||
/* Update particles */
|
/* Update particles */
|
||||||
particle_update(dt);
|
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;
|
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++) {
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
Entity *e = &level->entities.entities[i];
|
Entity *e = &level->entities.entities[i];
|
||||||
if (e->active && e->type == ENT_PLAYER) {
|
if (e->active && e->type == ENT_PLAYER) {
|
||||||
@@ -481,11 +597,13 @@ void level_update(Level *level, float dt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cam_tracked) {
|
if (!cam_tracked) {
|
||||||
/* Follow the spacecraft during intro */
|
/* Follow the spacecraft during intro */
|
||||||
for (int i = 0; i < level->entities.count; i++) {
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
Entity *e = &level->entities.entities[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(
|
Vec2 center = vec2(
|
||||||
e->body.pos.x + e->body.size.x * 0.5f,
|
e->body.pos.x + e->body.size.x * 0.5f,
|
||||||
e->body.pos.y + e->body.size.y * 0.5f
|
e->body.pos.y + e->body.size.y * 0.5f
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ typedef struct Level {
|
|||||||
/* ── Intro sequence state ────────────── */
|
/* ── Intro sequence state ────────────── */
|
||||||
bool has_intro_ship; /* level has a spacecraft intro */
|
bool has_intro_ship; /* level has a spacecraft intro */
|
||||||
bool player_spawned; /* player has been spawned */
|
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;
|
} Level;
|
||||||
|
|
||||||
bool level_load(Level *level, const char *path);
|
bool level_load(Level *level, const char *path);
|
||||||
|
|||||||
@@ -350,6 +350,48 @@ Entity *spacecraft_spawn(EntityManager *em, Vec2 land_pos) {
|
|||||||
return e;
|
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) {
|
void spacecraft_takeoff(Entity *ship) {
|
||||||
if (!ship || !ship->data) return;
|
if (!ship || !ship->data) return;
|
||||||
SpacecraftData *sd = (SpacecraftData *)ship->data;
|
SpacecraftData *sd = (SpacecraftData *)ship->data;
|
||||||
@@ -370,3 +412,9 @@ bool spacecraft_is_landed(const Entity *ship) {
|
|||||||
const SpacecraftData *sd = (const SpacecraftData *)ship->data;
|
const SpacecraftData *sd = (const SpacecraftData *)ship->data;
|
||||||
return sd->state == SC_LANDED;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,9 +55,14 @@ typedef struct SpacecraftData {
|
|||||||
void spacecraft_register(EntityManager *em);
|
void spacecraft_register(EntityManager *em);
|
||||||
|
|
||||||
/* Spawn a spacecraft that will fly in and land at the given position.
|
/* 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);
|
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 */
|
/* Trigger takeoff on an existing landed spacecraft */
|
||||||
void spacecraft_takeoff(Entity *ship);
|
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 */
|
/* Check if the spacecraft is in the landed state */
|
||||||
bool spacecraft_is_landed(const Entity *ship);
|
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 */
|
#endif /* JNR_SPACECRAFT_H */
|
||||||
|
|||||||
Reference in New Issue
Block a user