348 lines
11 KiB
C++
348 lines
11 KiB
C++
#include <iterator>
|
|
#include <mutex>
|
|
#include <print>
|
|
#include <random>
|
|
#include <raylib.h>
|
|
|
|
#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<Tear, 100> 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<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>(viewport.level_left),
|
|
static_cast<int>(viewport.level_right));
|
|
const auto starting_y = Rng::generate(static_cast<int>(viewport.level_top),
|
|
static_cast<int>(viewport.level_bottom));
|
|
|
|
for (auto &enemy: this->values) {
|
|
if (enemy.alive()) continue;
|
|
enemy.radius = enemy_radius;
|
|
auto hp = Rng::generate(1, 4);
|
|
enemy.hp = static_cast<uint32_t>(hp);
|
|
if (num < 0.25) {
|
|
enemy.center = {.x = starting_x, .y = viewport.level_top};
|
|
} else if (0.25 <= num && num < 0.5) {
|
|
enemy.center = {
|
|
.x = starting_x, .y = viewport.level_bottom
|
|
};
|
|
} else if (0.5 <= num && num < 0.75) {
|
|
enemy.center = {.x = viewport.level_left, .y = starting_y};
|
|
} else {
|
|
enemy.center = {
|
|
.x = viewport.level_right, .y = starting_y
|
|
};
|
|
}
|
|
|
|
enemy.speed = Rng::generate(static_cast<uint32_t>(enemy_max_speed));
|
|
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
EnemySpawner enemy_spawner{1};
|
|
|
|
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(ITEM_TYPE_COUNT));
|
|
const auto x = Rng::generate(static_cast<int>(viewport.level_left), static_cast<int>(viewport.level_right));
|
|
const auto y = Rng::generate(static_cast<int>(viewport.level_top), static_cast<int>(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, auto& cancel) {
|
|
if (enemy.alive() && CheckCollisionCircles(center, player.tear_radius, enemy.center, enemy.radius)) {
|
|
active = false;
|
|
if (--enemy.hp == 0) score++;
|
|
cancel = true;
|
|
}
|
|
});
|
|
|
|
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<float>(GetScreenWidth()), static_cast<float>(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<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 (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<int>(viewport.level_width), static_cast<int>(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, auto& cancel) {
|
|
if (enemy.alive()) {
|
|
enemy.center = Vector2MoveTowards(enemy.center, player.center(), enemy.speed);
|
|
const Color enemy_color = enemy.hp == 1 ? PINK : enemy.hp == 2 ? ORANGE : RED;
|
|
DrawCircleV(enemy.center, enemy.radius, enemy_color);
|
|
|
|
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) {
|
|
// Basically removing the enemy from the pool;
|
|
enemy.hp = 0;
|
|
}
|
|
|
|
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, auto& cancel) {
|
|
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<int>(viewport.level_left) + 10;
|
|
|
|
DrawRectangle(static_cast<int>(viewport.level_left), static_cast<int>(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<int>(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<int>(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<int>(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;
|
|
}
|