Files
Isaacpp/main.cpp
2025-12-01 11:26:47 -05:00

361 lines
11 KiB
C++

#include <iterator>
#include <mutex>
#include <print>
#include <random>
#include <raylib.h>
#include "rng.h"
#include "spawner.h"
#include "types.h"
float screen_width = 800;
float screen_height = 600;
float padding = 50;
float level_width = screen_width - padding * 2;
float level_height = screen_height - padding * 2;
float level_left = padding;
float level_top = padding;
float level_right = level_width + padding;
float level_bottom = level_height + padding;
Rectangle walls[] = {};
Player player;
auto player_speed = 5.0f;
auto player_invulnerability = 1.0f;
uint32_t score;
Tear tears[100] = {};
float tear_speed = 10;
float tear_range = 300;
float tear_radius = 10.0f;
double last_fired = 0;
double fire_rate = .5;
float enemy_max_speed = 3.0;
float enemy_radius = 25.0;
float item_radius = 10.0;
std::random_device Rng::dev{};
std::mt19937 Rng::rng{dev()};
Vector2 Player::center() const noexcept {
return {.x = this->x + this->width / 2, .y = this->y + this->height / 2};
}
Rectangle Player::rect() const noexcept {
return {.x = x, .y = y, .width = width, .height = height};
}
bool Player::is_invulnerable(const double now) const noexcept {
return now < this->last_hit + player_invulnerability;
}
void Player::move(const float delta_x, const float delta_y) {
const auto x = std::min(
std::max(this->x + delta_x, 0.0f),
level_right - this->width);
const auto y = std::min(
std::max(this->y + delta_y, 0.0f),
level_bottom - this->height);
bool collided = false;
const Rectangle next_position = {
.x = x,
.y = y,
.width = this->width,
.height = this->height
};
for (const auto wall: walls) {
if (CheckCollisionRecs(wall, next_position)) {
collided = true;
break;
}
}
if (collided) {
} else {
this->x = x;
this->y = y;
}
}
struct EnemySpawner final : Spawner<Enemy, 100> {
explicit EnemySpawner(const long long rate_secs)
: Spawner<Enemy, 100>(rate_secs) {
}
void spawn() override {
const auto num = Rng::generate();
const auto starting_x = Rng::generate(static_cast<int>(level_left), static_cast<int>(level_right));
const auto starting_y = Rng::generate(static_cast<int>(level_top), static_cast<int>(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 = level_top};
} else if (0.25 <= num && num < 0.5) {
center = {
.x = starting_x, .y = level_bottom
};
} else if (0.5 <= num && num < 0.75) {
center = {.x = level_left, .y = starting_y};
} else {
center = {
.x = level_right, .y = starting_y
};
}
speed = Rng::generate(static_cast<uint32_t>(enemy_max_speed));
break;
}
}
};
EnemySpawner enemy_spawner{2};
struct ItemSpawner final : Spawner<Item, 100> {
explicit ItemSpawner(const long long rate_secs) : Spawner<Item, 100>(rate_secs) {
}
void spawn() override {
const auto item_type = static_cast<ItemType>(Rng::generate(ItemType::ITEM_TYPE_COUNT));
const auto x = Rng::generate(static_cast<int>(level_left), static_cast<int>(level_right));
const auto y = Rng::generate(static_cast<int>(level_top), static_cast<int>(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, tear_radius, wall)) {
active = false;
break;
}
}
enemy_spawner.for_each([&center, &active](auto &enemy) {
if (enemy.alive && CheckCollisionCircles(center, tear_radius, enemy.center, enemy.radius)) {
active = false;
enemy.alive = false;
score++;
}
});
switch (direction) {
case UP:
center.y -= tear_speed;
break;
case DOWN:
center.y += tear_speed;
break;
case LEFT:
center.x -= tear_speed;
break;
case RIGHT:
center.x += tear_speed;
break;
}
if (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();
player.width = 50;
player.height = 50;
player.x = level_width / 2 - player.width / 2;
player.y = level_height / 2 - player.height / 2;
player.lives = 3;
player.last_hit = -10;
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;
double item_pickup_message_duration = 3;
while (!WindowShouldClose()) {
if (IsWindowResized()) {
screen_height = static_cast<float>(GetScreenHeight());
screen_width = static_cast<float>(GetScreenWidth());
level_width = screen_width - padding * 2;
level_height = screen_height - padding * 2;
level_left = padding;
level_top = padding;
level_right = level_width + padding;
level_bottom = level_height + padding;
}
float delta_x = 0;
float delta_y = 0;
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_x, delta_y);
std::optional<Direction> 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 (last_fired + fire_rate < now && tear_direction.has_value()) {
last_fired = now;
spawn_tear(player.center(), tear_direction.value());
}
update_tears();
BeginDrawing();
ClearBackground(GRAY);
DrawRectangle(50, 50, static_cast<int>(level_width), static_cast<int>(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, 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 < level_left || level_right < enemy.center.x ||
enemy.center.y < level_top || level_bottom < enemy.center.y) {
enemy.alive = false;
}
if (CheckCollisionCircleRec(enemy.center, enemy.radius, player.rect()) &&
player.last_hit + player_invulnerability < 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:
fire_rate = std::max(fire_rate - 0.1, 0.1);
break;
case TEARS_DOWN:
fire_rate += 0.1;
break;
case RANGE_UP:
tear_range += 100.0f;
break;
case RANGE_DOWN:
tear_range = std::max(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<int>(level_left) + 10;
DrawRectangle(level_left, static_cast<int>(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<int>(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<int>(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<int>(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;
}