diff --git a/DESIGN.md b/DESIGN.md index f9a9ddc..d10cdb7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -48,7 +48,7 @@ Each level defines its own atmosphere, affecting gameplay feel: | `MUSIC` | Level music track | assets/music/level1.ogg | | `PALETTE` | Color mood (warm, cold, toxic, void) | tint/filter values | -Already implemented: `GRAVITY`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR` (all per-level). Parallax backgrounds are procedurally generated (starfield + nebula) when no image path is specified. +Already implemented: `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR` (all per-level). Parallax backgrounds are procedurally generated (starfield + nebula) when no image path is specified. --- @@ -118,11 +118,10 @@ adding a new def. See `src/game/projectile.h` for the full definition. ## Levels ### Format (.lvl) -Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR`, `TILEDEF`, `ENTITY`, `LAYER` +Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PARALLAX_NEAR`, `TILEDEF`, `ENTITY`, `EXIT`, `LAYER` **Needed additions:** -- `EXIT ` — Level exit zone -- `WIND`, `STORM`, `DRAG` — Atmosphere settings +- `STORM`, `DRAG` — Remaining atmosphere settings ### Level Ideas @@ -166,7 +165,8 @@ Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `BG_COLOR`, `MUSIC`, ### Medium Priority - [x] In-game level editor (tile/entity placement, save/load, test play) -- [ ] Wind / drag atmosphere properties +- [x] Wind atmosphere property (`WIND` directive, affects all entities/particles/projectiles) +- [ ] Drag atmosphere property - [x] Parallax scrolling backgrounds (procedural stars + nebula, or from image files) - [x] Per-level background color (`BG_COLOR` directive) - [x] Music playback per level (`MUSIC` directive) diff --git a/src/engine/particle.c b/src/engine/particle.c index b14c696..3ef1d0c 100644 --- a/src/engine/particle.c +++ b/src/engine/particle.c @@ -82,6 +82,7 @@ void particle_emit(const ParticleBurst *b) { void particle_update(float dt) { float gravity = physics_get_gravity(); + float wind = physics_get_wind(); for (int i = 0; i < MAX_PARTICLES; i++) { Particle *p = &s_particles[i]; @@ -96,6 +97,11 @@ void particle_update(float dt) { /* Apply gravity */ p->vel.y += gravity * p->gravity_scale * dt; + /* Apply wind (reuse gravity_scale as environmental-force scale) */ + if (wind != 0.0f && p->gravity_scale > 0.0f) { + p->vel.x += wind * p->gravity_scale * dt; + } + /* Apply drag */ if (p->drag > 0) { float factor = 1.0f - p->drag * dt; diff --git a/src/engine/physics.c b/src/engine/physics.c index 5e82f31..a230706 100644 --- a/src/engine/physics.c +++ b/src/engine/physics.c @@ -4,9 +4,11 @@ #include static float s_gravity = DEFAULT_GRAVITY; +static float s_wind = 0.0f; void physics_init(void) { s_gravity = DEFAULT_GRAVITY; + s_wind = 0.0f; } void physics_set_gravity(float gravity) { @@ -17,6 +19,14 @@ float physics_get_gravity(void) { return s_gravity; } +void physics_set_wind(float wind) { + s_wind = wind; +} + +float physics_get_wind(void) { + return s_wind; +} + static void resolve_tilemap_x(Body *body, const Tilemap *map) { if (!map) return; @@ -102,6 +112,12 @@ void physics_update(Body *body, float dt, const Tilemap *map) { /* Apply gravity */ body->vel.y += s_gravity * body->gravity_scale * dt; + /* Apply wind — halved on ground (friction counteracts) */ + if (s_wind != 0.0f && body->gravity_scale > 0.0f) { + float wind_factor = body->on_ground ? 0.5f : 1.0f; + body->vel.x += s_wind * body->gravity_scale * wind_factor * dt; + } + /* Clamp fall speed */ if (body->vel.y > MAX_FALL_SPEED) { body->vel.y = MAX_FALL_SPEED; diff --git a/src/engine/physics.h b/src/engine/physics.h index c3610ad..50341f5 100644 --- a/src/engine/physics.h +++ b/src/engine/physics.h @@ -25,6 +25,10 @@ void physics_init(void); void physics_set_gravity(float gravity); float physics_get_gravity(void); +/* Set / get the current horizontal wind force (pixels/s^2, +=right) */ +void physics_set_wind(float wind); +float physics_get_wind(void); + /* Move body, apply gravity, resolve against tilemap */ void physics_update(Body *body, float dt, const Tilemap *map); diff --git a/src/engine/tilemap.c b/src/engine/tilemap.c index 51ddc63..65ae073 100644 --- a/src/engine/tilemap.c +++ b/src/engine/tilemap.c @@ -85,6 +85,8 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) { map->player_spawn = vec2(sx * TILE_SIZE, sy * TILE_SIZE); } else if (strncmp(line, "GRAVITY ", 8) == 0) { sscanf(line + 8, "%f", &map->gravity); + } else if (strncmp(line, "WIND ", 5) == 0) { + sscanf(line + 5, "%f", &map->wind); } else if (strncmp(line, "TILEDEF ", 8) == 0) { int id, tx, ty; uint32_t flags; diff --git a/src/engine/tilemap.h b/src/engine/tilemap.h index 7a2e98f..8ec1c4e 100644 --- a/src/engine/tilemap.h +++ b/src/engine/tilemap.h @@ -50,6 +50,7 @@ typedef struct Tilemap { char tileset_path[ASSET_PATH_MAX]; /* tileset file path */ Vec2 player_spawn; float gravity; /* level gravity (px/s^2), 0 = use default */ + float wind; /* horizontal wind force (px/s^2), +=right */ char music_path[ASSET_PATH_MAX]; /* level music file path */ SDL_Color bg_color; /* background clear color */ bool has_bg_color; /* true if BG_COLOR was set */ diff --git a/src/game/editor.c b/src/game/editor.c index cbbb2ef..1cac05d 100644 --- a/src/game/editor.c +++ b/src/game/editor.c @@ -450,6 +450,8 @@ static bool save_tilemap(const Tilemap *map, const char *path) { if (map->gravity > 0) fprintf(f, "GRAVITY %.0f\n", map->gravity); + if (map->wind != 0.0f) + fprintf(f, "WIND %.0f\n", map->wind); if (map->has_bg_color) fprintf(f, "BG_COLOR %d %d %d\n", map->bg_color.r, map->bg_color.g, map->bg_color.b); if (map->music_path[0]) diff --git a/src/game/enemy.c b/src/game/enemy.c index 4fcd06b..7617940 100644 --- a/src/game/enemy.c +++ b/src/game/enemy.c @@ -170,6 +170,13 @@ static void flyer_update(Entity *self, float dt, const Tilemap *map) { float bob_offset = sinf(fd->bob_timer * FLYER_BOB_SPD) * FLYER_BOB_AMP; body->pos.y = fd->base_y + bob_offset; + /* Wind drifts flyers (they bypass physics_update). + * Apply as gentle position offset matching first-frame physics. */ + float wind = physics_get_wind(); + if (wind != 0.0f) { + body->pos.x += wind * dt * dt; + } + /* Chase player if in range */ Entity *player = find_player(s_flyer_em); if (player && player->active && !(player->flags & ENTITY_DEAD)) { diff --git a/src/game/level.c b/src/game/level.c index 4986a0c..08cbdd7 100644 --- a/src/game/level.c +++ b/src/game/level.c @@ -57,6 +57,9 @@ static bool level_setup(Level *level) { physics_set_gravity(DEFAULT_GRAVITY); } + /* Apply level wind (0 = no wind) */ + physics_set_wind(level->map.wind); + /* Apply level background color */ if (level->map.has_bg_color) { renderer_set_clear_color(level->map.bg_color); diff --git a/src/game/levelgen.c b/src/game/levelgen.c index d9fc5d7..8749470 100644 --- a/src/game/levelgen.c +++ b/src/game/levelgen.c @@ -1631,6 +1631,10 @@ bool levelgen_dump_lvl(const Tilemap *map, const char *path) { fprintf(f, "GRAVITY %.0f\n", map->gravity); } + if (map->wind != 0.0f) { + fprintf(f, "WIND %.0f\n", map->wind); + } + if (map->has_bg_color) { fprintf(f, "BG_COLOR %d %d %d\n", map->bg_color.r, map->bg_color.g, map->bg_color.b); diff --git a/src/game/projectile.c b/src/game/projectile.c index d04dfbc..6b554e3 100644 --- a/src/game/projectile.c +++ b/src/game/projectile.c @@ -204,6 +204,12 @@ static void projectile_update(Entity *self, float dt, const Tilemap *map) { if (body->vel.y > MAX_FALL_SPEED) body->vel.y = MAX_FALL_SPEED; } + /* ── Apply wind ─────────────────────────── */ + float wind = physics_get_wind(); + if (wind != 0.0f) { + body->vel.x += wind * dt; + } + /* ── Move ────────────────────────────────── */ body->pos.x += body->vel.x * dt; body->pos.y += body->vel.y * dt;