Add new level transition state machine
This commit is contained in:
@@ -146,7 +146,7 @@ adding a new def. See `src/game/projectile.h` for the full definition.
|
|||||||
## Levels
|
## Levels
|
||||||
|
|
||||||
### Format (.lvl)
|
### Format (.lvl)
|
||||||
Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR`, `TILEDEF`, `ENTITY`, `EXIT`, `LAYER`
|
Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR`, `TRANSITION_IN`, `TRANSITION_OUT`, `TILEDEF`, `ENTITY`, `EXIT`, `LAYER`
|
||||||
|
|
||||||
**Needed additions:**
|
**Needed additions:**
|
||||||
- `STORM`, `DRAG` — Remaining atmosphere settings
|
- `STORM`, `DRAG` — Remaining atmosphere settings
|
||||||
|
|||||||
30
TODO.md
30
TODO.md
@@ -160,11 +160,25 @@ The spacecraft fly-in animation should only play on surface levels (moon01,
|
|||||||
mars01, etc.). Interior/base levels (mars02, mars03, generated mars_base,
|
mars01, etc.). Interior/base levels (mars02, mars03, generated mars_base,
|
||||||
generated station) should skip it — the player is already indoors.
|
generated station) should skip it — the player is already indoors.
|
||||||
|
|
||||||
## New level transition styles: elevator and teleporter
|
## ~~New level transition styles: elevator and teleporter~~ ✓
|
||||||
Two new transition animations to complement the spacecraft fly-in:
|
Implemented: `src/game/transition.h` / `transition.c` module with two-phase
|
||||||
- **Elevator** — Doors slide shut, brief pause (screen shake / rumble),
|
transition state machine (outro on old level → level swap → intro on new level).
|
||||||
doors slide open onto the new level. Good for base/station interior
|
|
||||||
transitions (mars02 → mars_base, between generated station levels).
|
- **Elevator** — Two horizontal doors slide inward from top/bottom (0.6 s),
|
||||||
- **Teleporter** — Energy charge-up effect around the player, flash/warp
|
hold closed with screen-shake rumble (0.3 s), then slide apart on the new
|
||||||
distortion, player materialises in the new level. Good for cross-planet
|
level (0.6 s). Smooth ease-in-out motion, dark gray industrial color,
|
||||||
jumps or generated-to-handcrafted transitions.
|
bright seam at the meeting edge. Used for base/station interior transitions.
|
||||||
|
- **Teleporter** — Scanline dissolve: 3 px-tall horizontal bands sweep
|
||||||
|
across the screen in alternating directions with staggered top-to-bottom
|
||||||
|
timing (0.5 s), then white flash (0.15 s). Intro reverses the sweep
|
||||||
|
bottom-to-top (0.5 s). Uses `teleport.wav` sound effect.
|
||||||
|
|
||||||
|
New `MODE_TRANSITION` game state in `main.c` pauses gameplay during the
|
||||||
|
animation. Level-load dispatch extracted into `dispatch_level_load()` helper,
|
||||||
|
called both from instant transitions and from the transition state machine.
|
||||||
|
|
||||||
|
New `.lvl` directives `TRANSITION_IN` and `TRANSITION_OUT` with values
|
||||||
|
`none`, `spacecraft`, `elevator`, `teleporter`. Parsed in `tilemap.c`,
|
||||||
|
saved by editor and level generator dump. All three procedural generators
|
||||||
|
(generic, station, mars_base) set `TRANS_ELEVATOR` for interior themes.
|
||||||
|
Handcrafted levels updated: mars02, mars03, level01, level02.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ SPAWN 3 18
|
|||||||
GRAVITY 400
|
GRAVITY 400
|
||||||
BG_COLOR 15 15 30
|
BG_COLOR 15 15 30
|
||||||
MUSIC assets/sounds/algardalgar.ogg
|
MUSIC assets/sounds/algardalgar.ogg
|
||||||
|
TRANSITION_OUT elevator
|
||||||
|
|
||||||
# Spacecraft landing intro (arriving from moon)
|
# Spacecraft landing intro (arriving from moon)
|
||||||
ENTITY spacecraft 1 14
|
ENTITY spacecraft 1 14
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ SPAWN 3 18
|
|||||||
GRAVITY 600
|
GRAVITY 600
|
||||||
BG_COLOR 10 10 25
|
BG_COLOR 10 10 25
|
||||||
MUSIC assets/sounds/algardalgar.ogg
|
MUSIC assets/sounds/algardalgar.ogg
|
||||||
|
TRANSITION_IN elevator
|
||||||
|
TRANSITION_OUT elevator
|
||||||
|
|
||||||
# Enemies
|
# Enemies
|
||||||
ENTITY grunt 12 18
|
ENTITY grunt 12 18
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ GRAVITY 700
|
|||||||
BG_COLOR 20 10 6
|
BG_COLOR 20 10 6
|
||||||
PARALLAX_STYLE 2
|
PARALLAX_STYLE 2
|
||||||
MUSIC assets/sounds/kaffe_og_kage.ogg
|
MUSIC assets/sounds/kaffe_og_kage.ogg
|
||||||
|
TRANSITION_OUT elevator
|
||||||
|
|
||||||
ENTITY spacecraft 1 3
|
ENTITY spacecraft 1 3
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ GRAVITY 700
|
|||||||
BG_COLOR 15 8 5
|
BG_COLOR 15 8 5
|
||||||
PARALLAX_STYLE 3
|
PARALLAX_STYLE 3
|
||||||
MUSIC assets/sounds/kaffe_og_kage.ogg
|
MUSIC assets/sounds/kaffe_og_kage.ogg
|
||||||
|
TRANSITION_IN elevator
|
||||||
|
TRANSITION_OUT elevator
|
||||||
|
|
||||||
# Gun pickup right at spawn — the player needs it
|
# Gun pickup right at spawn — the player needs it
|
||||||
ENTITY powerup_gun 5 18
|
ENTITY powerup_gun 5 18
|
||||||
|
|||||||
@@ -28,6 +28,14 @@
|
|||||||
/* ── Level transitions ─────────────────────────────── */
|
/* ── Level transitions ─────────────────────────────── */
|
||||||
#define MAX_EXIT_ZONES 16 /* max exit zones per level */
|
#define MAX_EXIT_ZONES 16 /* max exit zones per level */
|
||||||
|
|
||||||
|
typedef enum TransitionStyle {
|
||||||
|
TRANS_NONE, /* instant cut (default) */
|
||||||
|
TRANS_SPACECRAFT, /* handled by spacecraft entity */
|
||||||
|
TRANS_ELEVATOR, /* doors close, rumble, doors open */
|
||||||
|
TRANS_TELEPORTER, /* scanline dissolve, flash, materialize */
|
||||||
|
TRANS_STYLE_COUNT
|
||||||
|
} TransitionStyle;
|
||||||
|
|
||||||
/* ── Rendering ──────────────────────────────────────── */
|
/* ── Rendering ──────────────────────────────────────── */
|
||||||
#define MAX_SPRITES 2048 /* max queued sprites per frame */
|
#define MAX_SPRITES 2048 /* max queued sprites per frame */
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ── Transition style name → enum mapping ── */
|
||||||
|
static TransitionStyle parse_transition_style(const char *name) {
|
||||||
|
if (strcmp(name, "spacecraft") == 0) return TRANS_SPACECRAFT;
|
||||||
|
if (strcmp(name, "elevator") == 0) return TRANS_ELEVATOR;
|
||||||
|
if (strcmp(name, "teleporter") == 0) return TRANS_TELEPORTER;
|
||||||
|
return TRANS_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
/* Read a full line from f into a dynamically growing buffer.
|
/* Read a full line from f into a dynamically growing buffer.
|
||||||
* *buf and *cap track the heap buffer; the caller must free *buf.
|
* *buf and *cap track the heap buffer; the caller must free *buf.
|
||||||
* Returns the line length, or -1 on EOF/error. */
|
* Returns the line length, or -1 on EOF/error. */
|
||||||
@@ -120,6 +128,16 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
|||||||
}
|
}
|
||||||
} else if (strncmp(line, "PLAYER_UNARMED", 14) == 0) {
|
} else if (strncmp(line, "PLAYER_UNARMED", 14) == 0) {
|
||||||
map->player_unarmed = true;
|
map->player_unarmed = true;
|
||||||
|
} else if (strncmp(line, "TRANSITION_IN ", 14) == 0) {
|
||||||
|
char tname[32] = {0};
|
||||||
|
if (sscanf(line + 14, "%31s", tname) == 1) {
|
||||||
|
map->transition_in = parse_transition_style(tname);
|
||||||
|
}
|
||||||
|
} else if (strncmp(line, "TRANSITION_OUT ", 15) == 0) {
|
||||||
|
char tname[32] = {0};
|
||||||
|
if (sscanf(line + 15, "%31s", tname) == 1) {
|
||||||
|
map->transition_out = parse_transition_style(tname);
|
||||||
|
}
|
||||||
} else if (strncmp(line, "EXIT ", 5) == 0) {
|
} else if (strncmp(line, "EXIT ", 5) == 0) {
|
||||||
if (map->exit_zone_count < MAX_EXIT_ZONES) {
|
if (map->exit_zone_count < MAX_EXIT_ZONES) {
|
||||||
ExitZone *ez = &map->exit_zones[map->exit_zone_count];
|
ExitZone *ez = &map->exit_zones[map->exit_zone_count];
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ typedef struct Tilemap {
|
|||||||
char parallax_near_path[ASSET_PATH_MAX]; /* near bg image path */
|
char parallax_near_path[ASSET_PATH_MAX]; /* near bg image path */
|
||||||
int parallax_style; /* procedural bg style (0=default) */
|
int parallax_style; /* procedural bg style (0=default) */
|
||||||
bool player_unarmed; /* if true, player starts without gun */
|
bool player_unarmed; /* if true, player starts without gun */
|
||||||
|
TransitionStyle transition_in; /* transition animation for level entry */
|
||||||
|
TransitionStyle transition_out; /* transition animation for level exit */
|
||||||
EntitySpawn entity_spawns[MAX_ENTITY_SPAWNS];
|
EntitySpawn entity_spawns[MAX_ENTITY_SPAWNS];
|
||||||
int entity_spawn_count;
|
int entity_spawn_count;
|
||||||
ExitZone exit_zones[MAX_EXIT_ZONES];
|
ExitZone exit_zones[MAX_EXIT_ZONES];
|
||||||
|
|||||||
@@ -9,9 +9,20 @@
|
|||||||
/* Initialize client_id in localStorage and store the analytics
|
/* Initialize client_id in localStorage and store the analytics
|
||||||
* API URL + key. Called once at startup. */
|
* API URL + key. Called once at startup. */
|
||||||
EM_JS(void, js_analytics_init, (), {
|
EM_JS(void, js_analytics_init, (), {
|
||||||
/* Generate or retrieve a persistent client UUID */
|
/* Generate or retrieve a persistent client UUID.
|
||||||
|
* crypto.randomUUID() requires a secure context (HTTPS) and is
|
||||||
|
* absent in older browsers, so fall back to a manual v4 UUID. */
|
||||||
if (!localStorage.getItem('jnr_client_id')) {
|
if (!localStorage.getItem('jnr_client_id')) {
|
||||||
localStorage.setItem('jnr_client_id', crypto.randomUUID());
|
var uuid;
|
||||||
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
|
uuid = crypto.randomUUID();
|
||||||
|
} else {
|
||||||
|
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
var r = Math.random() * 16 | 0;
|
||||||
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
localStorage.setItem('jnr_client_id', uuid);
|
||||||
}
|
}
|
||||||
/* Store config on the Module for later use by other EM_JS calls.
|
/* Store config on the Module for later use by other EM_JS calls.
|
||||||
* ANALYTICS_URL and ANALYTICS_KEY are replaced at build time via
|
* ANALYTICS_URL and ANALYTICS_KEY are replaced at build time via
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "game/editor.h"
|
#include "game/editor.h"
|
||||||
#include "game/entity_registry.h"
|
#include "game/entity_registry.h"
|
||||||
|
#include "game/transition.h"
|
||||||
#include "engine/core.h"
|
#include "engine/core.h"
|
||||||
#include "engine/input.h"
|
#include "engine/input.h"
|
||||||
#include "engine/renderer.h"
|
#include "engine/renderer.h"
|
||||||
@@ -359,6 +360,16 @@ static bool save_tilemap(const Tilemap *map, const char *path) {
|
|||||||
if (map->player_unarmed)
|
if (map->player_unarmed)
|
||||||
fprintf(f, "PLAYER_UNARMED\n");
|
fprintf(f, "PLAYER_UNARMED\n");
|
||||||
|
|
||||||
|
/* Transition styles */
|
||||||
|
if (map->transition_in != TRANS_NONE) {
|
||||||
|
fprintf(f, "TRANSITION_IN %s\n",
|
||||||
|
transition_style_name(map->transition_in));
|
||||||
|
}
|
||||||
|
if (map->transition_out != TRANS_NONE) {
|
||||||
|
fprintf(f, "TRANSITION_OUT %s\n",
|
||||||
|
transition_style_name(map->transition_out));
|
||||||
|
}
|
||||||
|
|
||||||
fprintf(f, "\n");
|
fprintf(f, "\n");
|
||||||
|
|
||||||
/* Entity spawns */
|
/* Entity spawns */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "game/levelgen.h"
|
#include "game/levelgen.h"
|
||||||
|
#include "game/transition.h"
|
||||||
#include "engine/parallax.h"
|
#include "engine/parallax.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -1317,6 +1318,14 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg");
|
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transition style — interior themes use elevator, surface uses none
|
||||||
|
* (spacecraft entity handles surface transitions). */
|
||||||
|
if (primary_theme == THEME_PLANET_BASE || primary_theme == THEME_MARS_BASE
|
||||||
|
|| primary_theme == THEME_SPACE_STATION) {
|
||||||
|
map->transition_in = TRANS_ELEVATOR;
|
||||||
|
map->transition_out = TRANS_ELEVATOR;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tileset */
|
/* Tileset */
|
||||||
/* NOTE: tileset texture will be loaded by level_load_generated */
|
/* NOTE: tileset texture will be loaded by level_load_generated */
|
||||||
|
|
||||||
@@ -1826,6 +1835,10 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
/* Music */
|
/* Music */
|
||||||
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg");
|
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg");
|
||||||
|
|
||||||
|
/* Interior levels use elevator transitions. */
|
||||||
|
map->transition_in = TRANS_ELEVATOR;
|
||||||
|
map->transition_out = TRANS_ELEVATOR;
|
||||||
|
|
||||||
printf("levelgen_station: generated %dx%d level (%d segments, seed=%u, gravity=%.0f)\n",
|
printf("levelgen_station: generated %dx%d level (%d segments, seed=%u, gravity=%.0f)\n",
|
||||||
map->width, map->height, num_segs, s_rng_state, map->gravity);
|
map->width, map->height, num_segs, s_rng_state, map->gravity);
|
||||||
printf(" segments:");
|
printf(" segments:");
|
||||||
@@ -2485,6 +2498,10 @@ bool levelgen_generate_mars_base(Tilemap *map, const LevelGenConfig *config) {
|
|||||||
/* Music */
|
/* Music */
|
||||||
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/kaffe_og_kage.ogg");
|
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/kaffe_og_kage.ogg");
|
||||||
|
|
||||||
|
/* Interior levels use elevator transitions. */
|
||||||
|
map->transition_in = TRANS_ELEVATOR;
|
||||||
|
map->transition_out = TRANS_ELEVATOR;
|
||||||
|
|
||||||
printf("levelgen_mars_base: generated %dx%d level (%d segments, seed=%u)\n",
|
printf("levelgen_mars_base: generated %dx%d level (%d segments, seed=%u)\n",
|
||||||
map->width, map->height, num_segs, s_rng_state);
|
map->width, map->height, num_segs, s_rng_state);
|
||||||
printf(" segments:");
|
printf(" segments:");
|
||||||
@@ -2541,6 +2558,16 @@ bool levelgen_dump_lvl(const Tilemap *map, const char *path) {
|
|||||||
fprintf(f, "PLAYER_UNARMED\n");
|
fprintf(f, "PLAYER_UNARMED\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transition styles */
|
||||||
|
if (map->transition_in != TRANS_NONE) {
|
||||||
|
fprintf(f, "TRANSITION_IN %s\n",
|
||||||
|
transition_style_name(map->transition_in));
|
||||||
|
}
|
||||||
|
if (map->transition_out != TRANS_NONE) {
|
||||||
|
fprintf(f, "TRANSITION_OUT %s\n",
|
||||||
|
transition_style_name(map->transition_out));
|
||||||
|
}
|
||||||
|
|
||||||
fprintf(f, "\n");
|
fprintf(f, "\n");
|
||||||
|
|
||||||
/* Entity spawns */
|
/* Entity spawns */
|
||||||
|
|||||||
351
src/game/transition.c
Normal file
351
src/game/transition.c
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
#include "game/transition.h"
|
||||||
|
#include "engine/core.h"
|
||||||
|
#include "engine/audio.h"
|
||||||
|
#include "engine/particle.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Constants
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Elevator timing ── */
|
||||||
|
#define ELEVATOR_CLOSE_DURATION 0.6f /* doors slide shut */
|
||||||
|
#define ELEVATOR_HOLD_DURATION 0.3f /* closed with rumble */
|
||||||
|
#define ELEVATOR_OPEN_DURATION 0.6f /* doors slide open */
|
||||||
|
|
||||||
|
#define ELEVATOR_OUT_DURATION (ELEVATOR_CLOSE_DURATION + ELEVATOR_HOLD_DURATION)
|
||||||
|
#define ELEVATOR_IN_DURATION (ELEVATOR_HOLD_DURATION + ELEVATOR_OPEN_DURATION)
|
||||||
|
|
||||||
|
/* ── Teleporter timing ── */
|
||||||
|
#define TELEPORT_DISSOLVE_DURATION 0.5f /* scanline sweep out */
|
||||||
|
#define TELEPORT_FLASH_DURATION 0.15f /* white flash */
|
||||||
|
#define TELEPORT_MATERIALIZE_DURATION 0.5f /* scanline sweep in */
|
||||||
|
|
||||||
|
#define TELEPORT_OUT_DURATION (TELEPORT_DISSOLVE_DURATION + TELEPORT_FLASH_DURATION)
|
||||||
|
#define TELEPORT_IN_DURATION (TELEPORT_FLASH_DURATION + TELEPORT_MATERIALIZE_DURATION)
|
||||||
|
|
||||||
|
/* ── Scanline dissolve parameters ── */
|
||||||
|
#define SCANLINE_HEIGHT 3 /* pixel height per band */
|
||||||
|
#define SCANLINE_STAGGER 0.3f /* time spread between first/last band */
|
||||||
|
|
||||||
|
/* ── Elevator colors ── */
|
||||||
|
#define ELEV_R 40
|
||||||
|
#define ELEV_G 42
|
||||||
|
#define ELEV_B 48
|
||||||
|
|
||||||
|
/* ── Sound effects ── */
|
||||||
|
static Sound s_sfx_teleport;
|
||||||
|
static bool s_sfx_loaded = false;
|
||||||
|
|
||||||
|
static void ensure_sfx(void) {
|
||||||
|
if (s_sfx_loaded) return;
|
||||||
|
s_sfx_teleport = audio_load_sound("assets/sounds/teleport.wav");
|
||||||
|
s_sfx_loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Outro duration for a given style
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
static float outro_duration(TransitionStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case TRANS_ELEVATOR: return ELEVATOR_OUT_DURATION;
|
||||||
|
case TRANS_TELEPORTER: return TELEPORT_OUT_DURATION;
|
||||||
|
default: return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float intro_duration(TransitionStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case TRANS_ELEVATOR: return ELEVATOR_IN_DURATION;
|
||||||
|
case TRANS_TELEPORTER: return TELEPORT_IN_DURATION;
|
||||||
|
default: return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Public API
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
void transition_start_out(TransitionState *ts, TransitionStyle out_style) {
|
||||||
|
ensure_sfx();
|
||||||
|
ts->out_style = out_style;
|
||||||
|
ts->in_style = TRANS_NONE;
|
||||||
|
ts->phase = TRANS_PHASE_OUT;
|
||||||
|
ts->timer = 0.0f;
|
||||||
|
ts->phase_dur = outro_duration(out_style);
|
||||||
|
ts->sound_played = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void transition_set_in_style(TransitionState *ts, TransitionStyle in_style) {
|
||||||
|
ts->in_style = in_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
void transition_update(TransitionState *ts, float dt, Camera *cam) {
|
||||||
|
if (ts->phase == TRANS_IDLE || ts->phase == TRANS_PHASE_DONE) return;
|
||||||
|
|
||||||
|
ts->timer += dt;
|
||||||
|
|
||||||
|
/* ── Outro phase ── */
|
||||||
|
if (ts->phase == TRANS_PHASE_OUT) {
|
||||||
|
/* Play sound once at start of teleporter dissolve. */
|
||||||
|
if (ts->out_style == TRANS_TELEPORTER && !ts->sound_played) {
|
||||||
|
audio_play_sound(s_sfx_teleport, 80);
|
||||||
|
ts->sound_played = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elevator rumble during the hold period. */
|
||||||
|
if (ts->out_style == TRANS_ELEVATOR && cam) {
|
||||||
|
if (ts->timer > ELEVATOR_CLOSE_DURATION) {
|
||||||
|
camera_shake(cam, 3.0f, 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts->timer >= ts->phase_dur) {
|
||||||
|
ts->phase = TRANS_PHASE_LOAD;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Load phase: caller must call transition_begin_intro() ── */
|
||||||
|
if (ts->phase == TRANS_PHASE_LOAD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Intro phase ── */
|
||||||
|
if (ts->phase == TRANS_PHASE_IN) {
|
||||||
|
/* Elevator rumble at the start of intro (before doors open). */
|
||||||
|
if (ts->in_style == TRANS_ELEVATOR && cam) {
|
||||||
|
if (ts->timer < ELEVATOR_HOLD_DURATION) {
|
||||||
|
camera_shake(cam, 2.5f, 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts->timer >= ts->phase_dur) {
|
||||||
|
ts->phase = TRANS_PHASE_DONE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool transition_needs_load(const TransitionState *ts) {
|
||||||
|
return ts->phase == TRANS_PHASE_LOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
void transition_begin_intro(TransitionState *ts) {
|
||||||
|
ts->phase = TRANS_PHASE_IN;
|
||||||
|
ts->timer = 0.0f;
|
||||||
|
ts->phase_dur = intro_duration(ts->in_style);
|
||||||
|
ts->sound_played = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool transition_is_done(const TransitionState *ts) {
|
||||||
|
return ts->phase == TRANS_PHASE_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void transition_reset(TransitionState *ts) {
|
||||||
|
ts->phase = TRANS_IDLE;
|
||||||
|
ts->timer = 0.0f;
|
||||||
|
ts->phase_dur = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Elevator rendering helpers
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Returns door coverage 0.0 (fully open) to 1.0 (fully closed). */
|
||||||
|
static float elevator_coverage(const TransitionState *ts) {
|
||||||
|
if (ts->phase == TRANS_PHASE_OUT) {
|
||||||
|
/* Closing: 0 → 1 over ELEVATOR_CLOSE_DURATION, then hold at 1. */
|
||||||
|
float t = ts->timer / ELEVATOR_CLOSE_DURATION;
|
||||||
|
if (t > 1.0f) t = 1.0f;
|
||||||
|
/* Ease-in-out for smooth motion. */
|
||||||
|
return t * t * (3.0f - 2.0f * t);
|
||||||
|
}
|
||||||
|
if (ts->phase == TRANS_PHASE_IN) {
|
||||||
|
/* Hold closed during ELEVATOR_HOLD_DURATION, then open. */
|
||||||
|
float open_t = ts->timer - ELEVATOR_HOLD_DURATION;
|
||||||
|
if (open_t <= 0.0f) return 1.0f;
|
||||||
|
float t = open_t / ELEVATOR_OPEN_DURATION;
|
||||||
|
if (t > 1.0f) t = 1.0f;
|
||||||
|
float ease = t * t * (3.0f - 2.0f * t);
|
||||||
|
return 1.0f - ease;
|
||||||
|
}
|
||||||
|
/* PHASE_LOAD: fully closed. */
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_elevator(const TransitionState *ts) {
|
||||||
|
float coverage = elevator_coverage(ts);
|
||||||
|
if (coverage <= 0.0f) return;
|
||||||
|
|
||||||
|
int half_h = (int)(coverage * (float)SCREEN_HEIGHT * 0.5f + 0.5f);
|
||||||
|
|
||||||
|
SDL_Renderer *r = g_engine.renderer;
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
|
SDL_SetRenderDrawColor(r, ELEV_R, ELEV_G, ELEV_B, 255);
|
||||||
|
|
||||||
|
/* Top door. */
|
||||||
|
SDL_Rect top = {0, 0, SCREEN_WIDTH, half_h};
|
||||||
|
SDL_RenderFillRect(r, &top);
|
||||||
|
|
||||||
|
/* Bottom door. */
|
||||||
|
SDL_Rect bot = {0, SCREEN_HEIGHT - half_h, SCREEN_WIDTH, half_h};
|
||||||
|
SDL_RenderFillRect(r, &bot);
|
||||||
|
|
||||||
|
/* Thin bright seam at the meeting edge (visual detail). */
|
||||||
|
if (coverage > 0.7f) {
|
||||||
|
int seam_y = half_h - 1;
|
||||||
|
SDL_SetRenderDrawColor(r, 100, 110, 130, 255);
|
||||||
|
SDL_Rect seam_top = {0, seam_y, SCREEN_WIDTH, 1};
|
||||||
|
SDL_RenderFillRect(r, &seam_top);
|
||||||
|
|
||||||
|
int seam_bot_y = SCREEN_HEIGHT - half_h;
|
||||||
|
SDL_Rect seam_bot = {0, seam_bot_y, SCREEN_WIDTH, 1};
|
||||||
|
SDL_RenderFillRect(r, &seam_bot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Teleporter rendering helpers
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Scanline dissolve progress: each band sweeps across the screen.
|
||||||
|
* band_idx: which band (0 = top), total_bands: how many bands,
|
||||||
|
* global_progress: 0.0–1.0 across the dissolve duration,
|
||||||
|
* reverse: if true, sweep right-to-left / bottom-to-top. */
|
||||||
|
static float band_progress(int band_idx, int total_bands,
|
||||||
|
float global_progress, bool reverse) {
|
||||||
|
float band_offset;
|
||||||
|
if (reverse) {
|
||||||
|
band_offset = (float)(total_bands - 1 - band_idx) / (float)total_bands
|
||||||
|
* SCANLINE_STAGGER;
|
||||||
|
} else {
|
||||||
|
band_offset = (float)band_idx / (float)total_bands * SCANLINE_STAGGER;
|
||||||
|
}
|
||||||
|
float local = (global_progress - band_offset) / (1.0f - SCANLINE_STAGGER);
|
||||||
|
if (local < 0.0f) local = 0.0f;
|
||||||
|
if (local > 1.0f) local = 1.0f;
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_teleporter_scanlines(float global_progress,
|
||||||
|
bool reverse) {
|
||||||
|
int total_bands = SCREEN_HEIGHT / SCANLINE_HEIGHT;
|
||||||
|
SDL_Renderer *r = g_engine.renderer;
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
|
SDL_SetRenderDrawColor(r, 0, 0, 0, 255);
|
||||||
|
|
||||||
|
for (int i = 0; i < total_bands; i++) {
|
||||||
|
float bp = band_progress(i, total_bands, global_progress, reverse);
|
||||||
|
if (bp <= 0.0f) continue;
|
||||||
|
|
||||||
|
int y = i * SCANLINE_HEIGHT;
|
||||||
|
int w = (int)(bp * (float)SCREEN_WIDTH + 0.5f);
|
||||||
|
if (w <= 0) continue;
|
||||||
|
if (w > SCREEN_WIDTH) w = SCREEN_WIDTH;
|
||||||
|
|
||||||
|
/* Alternate sweep direction per band for visual interest. */
|
||||||
|
int x = (i % 2 == 0) ? 0 : (SCREEN_WIDTH - w);
|
||||||
|
|
||||||
|
SDL_Rect band = {x, y, w, SCANLINE_HEIGHT};
|
||||||
|
SDL_RenderFillRect(r, &band);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_teleporter_flash(float alpha_f) {
|
||||||
|
if (alpha_f <= 0.0f) return;
|
||||||
|
uint8_t alpha = (uint8_t)(alpha_f * 255.0f);
|
||||||
|
if (alpha == 0) return;
|
||||||
|
|
||||||
|
SDL_Renderer *r = g_engine.renderer;
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(r, 255, 255, 255, alpha);
|
||||||
|
SDL_Rect rect = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
|
||||||
|
SDL_RenderFillRect(r, &rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_teleporter(const TransitionState *ts) {
|
||||||
|
if (ts->phase == TRANS_PHASE_OUT) {
|
||||||
|
/* Phase 1: scanline dissolve, then flash builds up. */
|
||||||
|
if (ts->timer < TELEPORT_DISSOLVE_DURATION) {
|
||||||
|
float progress = ts->timer / TELEPORT_DISSOLVE_DURATION;
|
||||||
|
render_teleporter_scanlines(progress, false);
|
||||||
|
} else {
|
||||||
|
/* Dissolve complete — full black + rising flash. */
|
||||||
|
render_teleporter_scanlines(1.0f, false);
|
||||||
|
float flash_t = (ts->timer - TELEPORT_DISSOLVE_DURATION)
|
||||||
|
/ TELEPORT_FLASH_DURATION;
|
||||||
|
if (flash_t > 1.0f) flash_t = 1.0f;
|
||||||
|
render_teleporter_flash(flash_t);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts->phase == TRANS_PHASE_LOAD) {
|
||||||
|
/* Fully covered: black + white flash. */
|
||||||
|
render_teleporter_scanlines(1.0f, false);
|
||||||
|
render_teleporter_flash(1.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts->phase == TRANS_PHASE_IN) {
|
||||||
|
/* Phase 2: flash fades out, then scanlines recede. */
|
||||||
|
if (ts->timer < TELEPORT_FLASH_DURATION) {
|
||||||
|
/* Flash fading over black. */
|
||||||
|
render_teleporter_scanlines(1.0f, true);
|
||||||
|
float flash_t = 1.0f - ts->timer / TELEPORT_FLASH_DURATION;
|
||||||
|
render_teleporter_flash(flash_t);
|
||||||
|
} else {
|
||||||
|
/* Scanlines receding to reveal the new level. */
|
||||||
|
float mat_t = (ts->timer - TELEPORT_FLASH_DURATION)
|
||||||
|
/ TELEPORT_MATERIALIZE_DURATION;
|
||||||
|
if (mat_t > 1.0f) mat_t = 1.0f;
|
||||||
|
/* Progress inverted: 1.0 = fully covered, 0.0 = revealed. */
|
||||||
|
render_teleporter_scanlines(1.0f - mat_t, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Render dispatch
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
void transition_render(const TransitionState *ts) {
|
||||||
|
if (ts->phase == TRANS_IDLE || ts->phase == TRANS_PHASE_DONE) return;
|
||||||
|
|
||||||
|
/* Pick the active style based on which phase we are in. */
|
||||||
|
TransitionStyle style = (ts->phase == TRANS_PHASE_IN)
|
||||||
|
? ts->in_style : ts->out_style;
|
||||||
|
|
||||||
|
switch (style) {
|
||||||
|
case TRANS_ELEVATOR: render_elevator(ts); break;
|
||||||
|
case TRANS_TELEPORTER: render_teleporter(ts); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Name ↔ enum conversion
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
TransitionStyle transition_style_from_name(const char *name) {
|
||||||
|
if (!name) return TRANS_NONE;
|
||||||
|
if (strcmp(name, "spacecraft") == 0) return TRANS_SPACECRAFT;
|
||||||
|
if (strcmp(name, "elevator") == 0) return TRANS_ELEVATOR;
|
||||||
|
if (strcmp(name, "teleporter") == 0) return TRANS_TELEPORTER;
|
||||||
|
return TRANS_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *transition_style_name(TransitionStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case TRANS_SPACECRAFT: return "spacecraft";
|
||||||
|
case TRANS_ELEVATOR: return "elevator";
|
||||||
|
case TRANS_TELEPORTER: return "teleporter";
|
||||||
|
default: return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/game/transition.h
Normal file
68
src/game/transition.h
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#ifndef JNR_TRANSITION_H
|
||||||
|
#define JNR_TRANSITION_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "engine/camera.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════
|
||||||
|
* Level transition animations
|
||||||
|
*
|
||||||
|
* Two-phase system: outro (on the old level) then
|
||||||
|
* intro (on the new level). The caller is responsible
|
||||||
|
* for freeing the old level and loading the new one
|
||||||
|
* between phases (when transition_needs_load() is true).
|
||||||
|
*
|
||||||
|
* TransitionStyle enum is defined in config.h so that
|
||||||
|
* both engine (tilemap) and game code can reference it
|
||||||
|
* without circular includes.
|
||||||
|
* ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
typedef enum TransitionPhase {
|
||||||
|
TRANS_IDLE, /* no transition active */
|
||||||
|
TRANS_PHASE_OUT, /* outro animation on the old level */
|
||||||
|
TRANS_PHASE_LOAD, /* ready for the caller to swap levels */
|
||||||
|
TRANS_PHASE_IN, /* intro animation on the new level */
|
||||||
|
TRANS_PHASE_DONE, /* transition complete, return to play */
|
||||||
|
} TransitionPhase;
|
||||||
|
|
||||||
|
typedef struct TransitionState {
|
||||||
|
TransitionStyle out_style; /* outro style (from departing level) */
|
||||||
|
TransitionStyle in_style; /* intro style (from arriving level) */
|
||||||
|
TransitionPhase phase;
|
||||||
|
float timer; /* elapsed time in current phase */
|
||||||
|
float phase_dur; /* total duration of current phase */
|
||||||
|
bool sound_played; /* flag to fire a sound once per phase */
|
||||||
|
} TransitionState;
|
||||||
|
|
||||||
|
/* Start the outro phase. out_style comes from the departing level. */
|
||||||
|
void transition_start_out(TransitionState *ts, TransitionStyle out_style);
|
||||||
|
|
||||||
|
/* Set the intro style (call after loading the new level). */
|
||||||
|
void transition_set_in_style(TransitionState *ts, TransitionStyle in_style);
|
||||||
|
|
||||||
|
/* Advance the transition. cam may be NULL during the load gap. */
|
||||||
|
void transition_update(TransitionState *ts, float dt, Camera *cam);
|
||||||
|
|
||||||
|
/* True when the outro is done and the caller should swap levels. */
|
||||||
|
bool transition_needs_load(const TransitionState *ts);
|
||||||
|
|
||||||
|
/* Acknowledge the load — advance to the intro phase. */
|
||||||
|
void transition_begin_intro(TransitionState *ts);
|
||||||
|
|
||||||
|
/* True when the full transition is finished. */
|
||||||
|
bool transition_is_done(const TransitionState *ts);
|
||||||
|
|
||||||
|
/* Render the transition overlay (call AFTER rendering the level). */
|
||||||
|
void transition_render(const TransitionState *ts);
|
||||||
|
|
||||||
|
/* Reset to idle. */
|
||||||
|
void transition_reset(TransitionState *ts);
|
||||||
|
|
||||||
|
/* Parse a style name string ("none", "elevator", etc.). */
|
||||||
|
TransitionStyle transition_style_from_name(const char *name);
|
||||||
|
|
||||||
|
/* Return the directive string for a style. */
|
||||||
|
const char *transition_style_name(TransitionStyle style);
|
||||||
|
|
||||||
|
#endif /* JNR_TRANSITION_H */
|
||||||
132
src/main.c
132
src/main.c
@@ -6,6 +6,7 @@
|
|||||||
#include "game/editor.h"
|
#include "game/editor.h"
|
||||||
#include "game/stats.h"
|
#include "game/stats.h"
|
||||||
#include "game/analytics.h"
|
#include "game/analytics.h"
|
||||||
|
#include "game/transition.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -24,6 +25,7 @@ typedef enum GameMode {
|
|||||||
MODE_PLAY,
|
MODE_PLAY,
|
||||||
MODE_EDITOR,
|
MODE_EDITOR,
|
||||||
MODE_PAUSED,
|
MODE_PAUSED,
|
||||||
|
MODE_TRANSITION,
|
||||||
} GameMode;
|
} GameMode;
|
||||||
|
|
||||||
static Level s_level;
|
static Level s_level;
|
||||||
@@ -56,6 +58,10 @@ static bool s_session_active = false;
|
|||||||
#define PAUSE_ITEM_COUNT 3
|
#define PAUSE_ITEM_COUNT 3
|
||||||
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
||||||
|
|
||||||
|
/* ── Level transition state ── */
|
||||||
|
static TransitionState s_transition;
|
||||||
|
static char s_pending_target[ASSET_PATH_MAX] = {0}; /* exit target stashed during transition */
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
/* JS-initiated level load request (level-select dropdown in shell). */
|
/* JS-initiated level load request (level-select dropdown in shell). */
|
||||||
static int s_js_load_request = 0;
|
static int s_js_load_request = 0;
|
||||||
@@ -321,6 +327,48 @@ static void restart_level(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Level load dispatch — loads the next level based on target string ── */
|
||||||
|
static void dispatch_level_load(const char *target) {
|
||||||
|
if (target[0] == '\0') {
|
||||||
|
/* Empty target = victory / end of game. */
|
||||||
|
printf("Level complete! (no next level)\n");
|
||||||
|
end_session("completed");
|
||||||
|
level_free(&s_level);
|
||||||
|
s_station_depth = 0;
|
||||||
|
s_mars_depth = 0;
|
||||||
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
||||||
|
g_engine.running = false;
|
||||||
|
}
|
||||||
|
begin_session();
|
||||||
|
} else if (strcmp(target, "generate") == 0) {
|
||||||
|
printf("Transitioning to generated level\n");
|
||||||
|
level_free(&s_level);
|
||||||
|
s_gen_seed = (uint32_t)time(NULL);
|
||||||
|
load_generated_level();
|
||||||
|
} else if (strcmp(target, "generate:station") == 0) {
|
||||||
|
printf("Transitioning to space station level\n");
|
||||||
|
level_free(&s_level);
|
||||||
|
s_gen_seed = (uint32_t)time(NULL);
|
||||||
|
load_station_level();
|
||||||
|
} else if (strcmp(target, "generate:mars_base") == 0) {
|
||||||
|
printf("Transitioning to Mars Base level\n");
|
||||||
|
level_free(&s_level);
|
||||||
|
s_gen_seed = (uint32_t)time(NULL);
|
||||||
|
load_mars_base_level();
|
||||||
|
} else {
|
||||||
|
printf("Transitioning to: %s\n", target);
|
||||||
|
char path[ASSET_PATH_MAX];
|
||||||
|
snprintf(path, sizeof(path), "%s", target);
|
||||||
|
level_free(&s_level);
|
||||||
|
if (!load_level_file(path)) {
|
||||||
|
fprintf(stderr, "Failed to load next level: %s\n", path);
|
||||||
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
||||||
|
g_engine.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
* Game callbacks
|
* Game callbacks
|
||||||
* ═══════════════════════════════════════════════════ */
|
* ═══════════════════════════════════════════════════ */
|
||||||
@@ -395,7 +443,9 @@ static void game_update(float dt) {
|
|||||||
end_session("quit");
|
end_session("quit");
|
||||||
|
|
||||||
/* Tear down whatever mode we are in. */
|
/* Tear down whatever mode we are in. */
|
||||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED) {
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
||||||
|
|| s_mode == MODE_TRANSITION) {
|
||||||
|
transition_reset(&s_transition);
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
} else if (s_mode == MODE_EDITOR) {
|
} else if (s_mode == MODE_EDITOR) {
|
||||||
editor_free(&s_editor);
|
editor_free(&s_editor);
|
||||||
@@ -440,6 +490,28 @@ static void game_update(float dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_mode == MODE_TRANSITION) {
|
||||||
|
transition_update(&s_transition, dt, &s_level.camera);
|
||||||
|
|
||||||
|
/* Outro finished — swap levels. */
|
||||||
|
if (transition_needs_load(&s_transition)) {
|
||||||
|
dispatch_level_load(s_pending_target);
|
||||||
|
s_pending_target[0] = '\0';
|
||||||
|
|
||||||
|
/* Use the new level's intro style. */
|
||||||
|
TransitionStyle in_style = s_level.map.transition_in;
|
||||||
|
transition_set_in_style(&s_transition, in_style);
|
||||||
|
transition_begin_intro(&s_transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro finished — return to play. */
|
||||||
|
if (transition_is_done(&s_transition)) {
|
||||||
|
transition_reset(&s_transition);
|
||||||
|
s_mode = MODE_PLAY;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Play mode ── */
|
/* ── Play mode ── */
|
||||||
|
|
||||||
/* Pause on escape (return to editor during test play) */
|
/* Pause on escape (return to editor during test play) */
|
||||||
@@ -492,49 +564,16 @@ static void game_update(float dt) {
|
|||||||
s_stats.levels_completed++;
|
s_stats.levels_completed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target[0] == '\0') {
|
TransitionStyle out_style = s_level.map.transition_out;
|
||||||
/* Empty target = victory / end of game */
|
|
||||||
printf("Level complete! (no next level)\n");
|
if (out_style == TRANS_ELEVATOR || out_style == TRANS_TELEPORTER) {
|
||||||
end_session("completed");
|
/* Animated transition: stash target, start outro. */
|
||||||
/* Loop back to the beginning, reset progression state */
|
snprintf(s_pending_target, sizeof(s_pending_target), "%s", target);
|
||||||
level_free(&s_level);
|
transition_start_out(&s_transition, out_style);
|
||||||
s_station_depth = 0;
|
s_mode = MODE_TRANSITION;
|
||||||
s_mars_depth = 0;
|
|
||||||
if (!load_level_file("assets/levels/moon01.lvl")) {
|
|
||||||
g_engine.running = false;
|
|
||||||
}
|
|
||||||
begin_session();
|
|
||||||
} else if (strcmp(target, "generate") == 0) {
|
|
||||||
/* Procedurally generated next level */
|
|
||||||
printf("Transitioning to generated level\n");
|
|
||||||
level_free(&s_level);
|
|
||||||
s_gen_seed = (uint32_t)time(NULL);
|
|
||||||
load_generated_level();
|
|
||||||
} else if (strcmp(target, "generate:station") == 0) {
|
|
||||||
/* Procedurally generated space station level */
|
|
||||||
printf("Transitioning to space station level\n");
|
|
||||||
level_free(&s_level);
|
|
||||||
s_gen_seed = (uint32_t)time(NULL);
|
|
||||||
load_station_level();
|
|
||||||
} else if (strcmp(target, "generate:mars_base") == 0) {
|
|
||||||
/* Procedurally generated Mars Base level */
|
|
||||||
printf("Transitioning to Mars Base level\n");
|
|
||||||
level_free(&s_level);
|
|
||||||
s_gen_seed = (uint32_t)time(NULL);
|
|
||||||
load_mars_base_level();
|
|
||||||
} else {
|
} else {
|
||||||
/* Load a specific level file */
|
/* Instant transition (none or spacecraft-driven). */
|
||||||
printf("Transitioning to: %s\n", target);
|
dispatch_level_load(target);
|
||||||
char path[ASSET_PATH_MAX];
|
|
||||||
snprintf(path, sizeof(path), "%s", target);
|
|
||||||
level_free(&s_level);
|
|
||||||
if (!load_level_file(path)) {
|
|
||||||
fprintf(stderr, "Failed to load next level: %s\n", path);
|
|
||||||
/* Fallback to moon01 */
|
|
||||||
if (!load_level_file("assets/levels/moon01.lvl")) {
|
|
||||||
g_engine.running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,6 +626,10 @@ static void game_render(float interpolation) {
|
|||||||
/* Render frozen game frame, then overlay the pause menu. */
|
/* Render frozen game frame, then overlay the pause menu. */
|
||||||
level_render(&s_level, interpolation);
|
level_render(&s_level, interpolation);
|
||||||
pause_render();
|
pause_render();
|
||||||
|
} else if (s_mode == MODE_TRANSITION) {
|
||||||
|
/* Render the level (frozen) with the transition overlay on top. */
|
||||||
|
level_render(&s_level, interpolation);
|
||||||
|
transition_render(&s_transition);
|
||||||
} else {
|
} else {
|
||||||
level_render(&s_level, interpolation);
|
level_render(&s_level, interpolation);
|
||||||
}
|
}
|
||||||
@@ -598,7 +641,8 @@ static void game_shutdown(void) {
|
|||||||
/* Always free both — editor may have been initialized even if we're
|
/* Always free both — editor may have been initialized even if we're
|
||||||
* currently in play mode (e.g. shutdown during test play). editor_free
|
* currently in play mode (e.g. shutdown during test play). editor_free
|
||||||
* and level_free are safe to call on zeroed/already-freed structs. */
|
* and level_free are safe to call on zeroed/already-freed structs. */
|
||||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED || s_testing_from_editor) {
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
||||||
|
|| s_mode == MODE_TRANSITION || s_testing_from_editor) {
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
}
|
}
|
||||||
if (s_mode == MODE_EDITOR || s_use_editor) {
|
if (s_mode == MODE_EDITOR || s_use_editor) {
|
||||||
|
|||||||
Reference in New Issue
Block a user