#include #include #include #include #include #include "raymath.h" #include "rng.h" #include "spawner.h" #include "types.h" Viewport viewport{800, 600}; Rectangle walls[] = {}; float player_width = 50; Player player = { .rect = { .x = viewport.level_width / 2 - player_width / 2, .y = viewport.level_height / 2 - player_width / 2, .width = player_width, .height = player_width, }, .lives = 3, .speed = 5.0, .invulnerability_secs = 1, .last_hit = -10, .tear_speed = 10, .tear_range = 300, .tear_radius = 10.0f, .last_fired = 0, .fire_rate = .5, }; uint32_t score; std::array tears; float enemy_max_speed = 3.0; float enemy_radius = 25.0; float item_radius = 10.0; Vector2 Player::center() const noexcept { return {.x = this->rect.x + this->rect.width / 2, .y = this->rect.y + this->rect.height / 2}; } bool Player::is_invulnerable(const double now) const noexcept { return now < last_hit + invulnerability_secs; } void Player::move(const Vector2 delta) { const auto [next_x, next_y] = Vector2Clamp( Vector2Add({this->rect.x, this->rect.y}, delta), {viewport.level_left, viewport.level_top}, Vector2Subtract({viewport.level_right, viewport.level_bottom}, {this->rect.width, this->rect.height}) ); bool collided = false; const Rectangle next_rect = { .x = next_x, .y = next_y, .width = this->rect.width, .height = this->rect.height }; for (const auto wall: walls) { if (CheckCollisionRecs(wall, next_rect)) { collided = true; break; } } if (collided) { } else { this->rect.x = next_x; this->rect.y = next_y; } } struct EnemySpawner final : Spawner { explicit EnemySpawner(const long long rate_secs) : Spawner(rate_secs) { } void spawn() override { const auto num = Rng::generate(); const auto starting_x = Rng::generate(static_cast(viewport.level_left), static_cast(viewport.level_right)); const auto starting_y = Rng::generate(static_cast(viewport.level_top), static_cast(viewport.level_bottom)); for (auto &[center, radius, speed, alive]: this->values) { if (alive) continue; alive = true; radius = enemy_radius; if (num < 0.25) { center = {.x = starting_x, .y = viewport.level_top}; } else if (0.25 <= num && num < 0.5) { center = { .x = starting_x, .y = viewport.level_bottom }; } else if (0.5 <= num && num < 0.75) { center = {.x = viewport.level_left, .y = starting_y}; } else { center = { .x = viewport.level_right, .y = starting_y }; } speed = Rng::generate(static_cast(enemy_max_speed)); break; } } }; EnemySpawner enemy_spawner{2}; struct ItemSpawner final : Spawner { explicit ItemSpawner(const long long rate_secs) : Spawner(rate_secs) { } void spawn() override { const auto item_type = static_cast(Rng::generate(ITEM_TYPE_COUNT)); const auto x = Rng::generate(static_cast(viewport.level_left), static_cast(viewport.level_right)); const auto y = Rng::generate(static_cast(viewport.level_top), static_cast(viewport.level_bottom)); for (auto &item: this->values) { if (item.active) continue; item.active = true; item.type = item_type; item.center = Vector2{x, y}; item.radius = item_radius; break; } } }; ItemSpawner item_spawner{5}; void spawn_tear(const Vector2 center, const Direction direction) { for (auto &[tear_center, tear_direction, tear_active, starting_center]: tears) { if (!tear_active) { tear_center = center; tear_direction = direction; tear_active = true; starting_center = center; break; } } } void update_tears() { for (auto &[center, direction, active, starting_center]: tears) { if (active) { for (const auto wall: walls) { if (CheckCollisionCircleRec(center, player.tear_radius, wall)) { active = false; break; } } enemy_spawner.for_each([¢er, &active](auto &enemy) { if (enemy.alive && CheckCollisionCircles(center, player.tear_radius, enemy.center, enemy.radius)) { active = false; enemy.alive = false; score++; } }); switch (direction) { case UP: center.y -= player.tear_speed; break; case DOWN: center.y += player.tear_speed; break; case LEFT: center.x -= player.tear_speed; break; case RIGHT: center.x += player.tear_speed; break; } if (player.tear_range < Vector2Distance(center, starting_center)) { active = false; } } } } int main() { SetConfigFlags(FLAG_WINDOW_RESIZABLE); InitWindow(800, 600, "Isaac++"); SetTargetFPS(60); enemy_spawner.start(); item_spawner.start(); std::string score_text_buffer; score_text_buffer.reserve(64); std::string lives_text_buffer; lives_text_buffer.reserve(64); std::string item_pickup_text_buffer; item_pickup_text_buffer.reserve(64); double item_last_picked_up = 0; while (!WindowShouldClose()) { constexpr double item_pickup_message_duration = 3; // float dt_secs = GetFrameTime(); if (IsWindowResized()) { viewport = {static_cast(GetScreenWidth()), static_cast(GetScreenHeight())}; } Vector2 delta{}; if (IsKeyDown(KEY_W)) delta.y -= player.speed; if (IsKeyDown(KEY_S)) delta.y += player.speed; if (IsKeyDown(KEY_A)) delta.x -= player.speed; if (IsKeyDown(KEY_D)) delta.x += player.speed; player.move(delta); std::optional tear_direction; if (IsKeyDown(KEY_LEFT)) { tear_direction = LEFT; } if (IsKeyDown(KEY_UP)) { tear_direction = UP; } if (IsKeyDown(KEY_RIGHT)) { tear_direction = RIGHT; } if (IsKeyDown(KEY_DOWN)) { tear_direction = DOWN; } const auto now = GetTime(); if (player.last_fired + player.fire_rate < now && tear_direction.has_value()) { player.last_fired = now; spawn_tear(player.center(), tear_direction.value()); } update_tears(); BeginDrawing(); ClearBackground(GRAY); DrawRectangle(50, 50, static_cast(viewport.level_width), static_cast(viewport.level_height), RAYWHITE); DrawRectangleRec(player.rect, player.is_invulnerable(now) ? GOLD : BLUE); for (const auto wall: walls) { DrawRectangleRec(wall, GREEN); } for (const auto tear: tears) { if (tear.active) { DrawCircleV(tear.center, player.tear_radius, SKYBLUE); } } enemy_spawner.for_each([now](Enemy &enemy) { if (enemy.alive) { enemy.center = Vector2MoveTowards(enemy.center, player.center(), enemy.speed); DrawCircleV(enemy.center, enemy.radius, RED); if (enemy.center.x < viewport.level_left || viewport.level_right < enemy.center.x || enemy.center.y < viewport.level_top || viewport.level_bottom < enemy.center.y) { enemy.alive = false; } if (CheckCollisionCircleRec(enemy.center, enemy.radius, player.rect) && player.last_hit + player.invulnerability_secs < now) { player.lives--; player.last_hit = now; } } }); item_spawner.for_each([&item_pickup_text_buffer, now, &item_last_picked_up](Item &item) { if (!item.active) return; DrawCircleV(item.center, item.radius, item.color()); if (CheckCollisionCircleRec(item.center, item.radius, player.rect)) { switch (item.type) { case TEARS_UP: player.fire_rate = std::max(player.fire_rate - 0.1, 0.1); break; case TEARS_DOWN: player.fire_rate += 0.1; break; case RANGE_UP: player.tear_range += 100.0f; break; case RANGE_DOWN: player.tear_range = std::max(player.tear_range - 100.0f, 100.0f); break; case ITEM_TYPE_COUNT: break; } if (item_pickup_text_buffer.empty()) { std::format_to(std::back_inserter(item_pickup_text_buffer), "{:t}!", item.type); } else { std::format_to(std::back_inserter(item_pickup_text_buffer), " {:t}!", item.type); } item_last_picked_up = now; item.active = false; } }); const int text_left = static_cast(viewport.level_left) + 10; DrawRectangle(static_cast(viewport.level_left), static_cast(viewport.level_height) - 110, 200, 110, {130, 130, 130, 100}); std::format_to(std::back_inserter(score_text_buffer), "Score: {}", score); DrawText(score_text_buffer.c_str(), text_left, static_cast(viewport.level_height) - 100, 20, BLACK); score_text_buffer.clear(); std::format_to(std::back_inserter(lives_text_buffer), "Lives: {}", player.lives); DrawText(lives_text_buffer.c_str(), text_left, static_cast(viewport.level_height) - 75, 20, BLACK); lives_text_buffer.clear(); if (now < item_last_picked_up + item_pickup_message_duration && !item_pickup_text_buffer.empty()) { DrawText(item_pickup_text_buffer.c_str(), text_left, static_cast(viewport.level_height) - 50, 20, BLACK); } if (item_last_picked_up + item_pickup_message_duration <= now && !item_pickup_text_buffer.empty()) { item_pickup_text_buffer.clear(); } EndDrawing(); } CloseWindow(); return 0; }