#include "engine/physics.h" #include "engine/tilemap.h" #include "config.h" #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) { s_gravity = gravity; } 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; /* Check tiles the body overlaps after X movement */ int left = world_to_tile(body->pos.x); int right = world_to_tile(body->pos.x + body->size.x - 1); int top = world_to_tile(body->pos.y); int bottom = world_to_tile(body->pos.y + body->size.y - 1); body->on_wall_left = false; body->on_wall_right = false; for (int ty = top; ty <= bottom; ty++) { for (int tx = left; tx <= right; tx++) { if (!tilemap_is_solid(map, tx, ty)) continue; float tile_left = tile_to_world(tx); float tile_right = tile_to_world(tx + 1); if (body->vel.x > 0) { /* Moving right -> push left */ body->pos.x = tile_left - body->size.x; body->vel.x = 0; body->on_wall_right = true; } else if (body->vel.x < 0) { /* Moving left -> push right */ body->pos.x = tile_right; body->vel.x = 0; body->on_wall_left = true; } } } } static void resolve_tilemap_y(Body *body, const Tilemap *map) { if (!map) return; int left = world_to_tile(body->pos.x); int right = world_to_tile(body->pos.x + body->size.x - 1); int top = world_to_tile(body->pos.y); int bottom = world_to_tile(body->pos.y + body->size.y - 1); body->on_ground = false; body->on_ceiling = false; for (int ty = top; ty <= bottom; ty++) { for (int tx = left; tx <= right; tx++) { uint32_t flags = tilemap_flags_at(map, tx, ty); if (!(flags & TILE_SOLID)) { /* Check one-way platforms: block when falling or resting on top. * vel.y >= 0 prevents on_ground flickering when standing still — * gravity adds a tiny downward vel each frame, the platform * resolves it back to zero, and the cycle would repeat. */ if ((flags & TILE_PLATFORM) && body->vel.y >= 0) { float tile_top = tile_to_world(ty); float body_bottom = body->pos.y + body->size.y; /* Only resolve if we just crossed into the tile or are resting on it */ if (body_bottom - body->vel.y * DT <= tile_top + 2.0f) { body->pos.y = tile_top - body->size.y; body->vel.y = 0; body->on_ground = true; } } continue; } float tile_top = tile_to_world(ty); float tile_bottom = tile_to_world(ty + 1); if (body->vel.y > 0) { /* Falling -> land on top of tile */ body->pos.y = tile_top - body->size.y; body->vel.y = 0; body->on_ground = true; } else if (body->vel.y < 0) { /* Rising -> bonk on ceiling */ body->pos.y = tile_bottom; body->vel.y = 0; body->on_ceiling = true; } } } } 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; } /* Move X, resolve X */ body->pos.x += body->vel.x * dt; resolve_tilemap_x(body, map); /* Move Y, resolve Y */ body->pos.y += body->vel.y * dt; resolve_tilemap_y(body, map); /* Ground-stick probe: if not detected as on_ground but vel.y is * near zero (just landed or standing), check one pixel below. * Prevents flickering between grounded/airborne each frame due to * sub-pixel gravity accumulation. Checks both solid tiles and * one-way platforms. */ if (!body->on_ground && body->vel.y >= 0 && body->vel.y < s_gravity * dt * 2.0f && map) { int left = world_to_tile(body->pos.x); int right = world_to_tile(body->pos.x + body->size.x - 1); int probe = world_to_tile(body->pos.y + body->size.y + 1.0f); float body_bottom = body->pos.y + body->size.y; for (int tx = left; tx <= right; tx++) { uint32_t pf = tilemap_flags_at(map, tx, probe); if (pf & TILE_SOLID) { body->on_ground = true; body->vel.y = 0; break; } /* One-way platforms: only stick if feet are at or above the * platform top. Prevents snapping to a platform the player * is below (e.g. jumping up through it). */ if (pf & TILE_PLATFORM) { float tile_top = tile_to_world(probe); if (body_bottom <= tile_top + 2.0f) { body->on_ground = true; body->vel.y = 0; break; } } } } } bool physics_overlap(const Body *a, const Body *b) { return physics_aabb_overlap(a->pos, a->size, b->pos, b->size); } bool physics_aabb_overlap(Vec2 pos_a, Vec2 size_a, Vec2 pos_b, Vec2 size_b) { return pos_a.x < pos_b.x + size_b.x && pos_a.x + size_a.x > pos_b.x && pos_a.y < pos_b.y + size_b.y && pos_a.y + size_a.y > pos_b.y; }