Skip to content

Feature/actors #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/fonts/specialElite.ttf
Binary file not shown.
Binary file added assets/images/lighthouse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ext/src.zip
Binary file not shown.
9 changes: 9 additions & 0 deletions src/EnemyColliderInfo.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once
#include <glm/vec2.hpp>

namespace miracle {
struct CollisionParams {
glm::vec2 pos;
float diameter;
};
} // namespace miracle
14 changes: 4 additions & 10 deletions src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,17 @@
#include <game.hpp>
#include <klib/visitor.hpp>
#include <log.hpp>
#include <util/random.hpp>

namespace miracle {
namespace {
constexpr auto window_size = glm::ivec2{800, 800};
constexpr auto window_flags = le::default_window_flags_v & ~le::WindowFlag::Resizeable;
constexpr auto context_ci = le::Context::CreateInfo{
.window = le::WindowInfo{.size = {1280, 720}, .title = "miracle"},
.window = le::WindowInfo{.size = window_size, .title = "miracle", .flags = window_flags},
};
} // namespace

App::App() : m_context(context_ci), m_data_loader(le::FileDataLoader::upfind("assets")) {
bind_services();

// test code, remove later.
auto json = dj::Json{};
if (m_services.get<le::IDataLoader>().load_json(json, "test_file.json")) { log.info("loaded JSON: {}", json); }
log.debug("random_range(1, 100): {}", util::random_range(1, 100));
}
App::App() : m_context(context_ci), m_data_loader(le::FileDataLoader::upfind("assets")) { bind_services(); }

void App::run() {
auto game = Game{&m_services};
Expand Down
36 changes: 36 additions & 0 deletions src/enemy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include <enemy.hpp>
#include <algorithm>
#include "enemy_params.hpp"
#include "glm/geometric.hpp"
#include "glm/vec2.hpp"
#include "kvf/color.hpp"
#include "util/random.hpp"

namespace miracle {
Enemy::Enemy(gsl::not_null<le::ServiceLocator const*> services, EnemyParams const& params)
: m_services(services), m_target_pos(params.target_pos), m_move_speed(params.move_speed), m_diameter(util::random_range(40.0f, 60.0f)) {
m_sprite.create(m_diameter, kvf::red_v);
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
auto const radius = static_cast<float>(std::max(framebuffer_size.x, framebuffer_size.y)) / 2.0f;

m_sprite.transform.position = util::get_random_location_on_radius(radius);
// TODO: add proper textures
}

void Enemy::render(le::Renderer& renderer) const {
if (can_render) { m_sprite.draw(renderer); }
}

void Enemy::translate(kvf::Seconds const dt) {
glm::vec2 const direction = glm::normalize(m_target_pos - m_sprite.transform.position);
glm::vec2 const movement = direction * m_move_speed * dt.count();
m_sprite.transform.position += movement;
}

CollisionParams Enemy::get_collision_params() const { return {.pos = m_sprite.transform.position, .diameter = m_diameter}; }
void Enemy::take_damage(std::size_t dmg) {
can_render = true;
m_health = (dmg >= m_health) ? 0 : (m_health - dmg);
}

} // namespace miracle
37 changes: 37 additions & 0 deletions src/enemy.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once
#include <glm/gtx/norm.hpp>
#include <kvf/time.hpp>
#include <le2d/context.hpp>
#include <le2d/drawable/shape.hpp>
#include <le2d/event.hpp>
#include <le2d/renderer.hpp>
#include <le2d/service_locator.hpp>
#include <cstddef>
#include <optional>
#include "EnemyColliderInfo.hpp"
#include "enemy_params.hpp"
#include "glm/vec2.hpp"
#include "le2d/texture.hpp"

namespace miracle {
class Enemy {
public:
explicit Enemy(gsl::not_null<le::ServiceLocator const*> services, EnemyParams const& params);

void render(le::Renderer& renderer) const;
void translate(kvf::Seconds dt);
void take_damage(std::size_t dmg);
[[nodiscard]] std::size_t get_health() const { return m_health; }
[[nodiscard]] CollisionParams get_collision_params() const;
bool can_render{false};

private:
gsl::not_null<le::ServiceLocator const*> m_services;
std::optional<le::Texture> m_texture;
le::drawable::Circle m_sprite{};
glm::vec2 m_target_pos{};
float m_move_speed{};
float m_diameter{};
std::size_t m_health{100};
};
} // namespace miracle
7 changes: 7 additions & 0 deletions src/enemy_params.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#pragma once
#include "glm/vec2.hpp"

struct EnemyParams {
glm::vec2 target_pos{};
float move_speed{};
};
101 changes: 81 additions & 20 deletions src/game.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
#include <game.hpp>
#include <glm/gtx/norm.hpp>
#include <le2d/context.hpp>
#include <cmath>
#include <cstddef>
#include <format>
#include <iterator>
#include <string>
#include <vector>
#include "enemy.hpp"
#include "enemy_params.hpp"
#include "glm/ext/vector_float2.hpp"
#include "kvf/time.hpp"
#include "le2d/asset_loader.hpp"
#include "le2d/data_loader.hpp"
#include "le2d/drawable/text.hpp"
#include "lighhouse.hpp"
#include "util/random.hpp"

namespace miracle {
Game::Game(gsl::not_null<le::ServiceLocator const*> services) : m_services(services) {
m_triangle.vertices = {
le::Vertex{.position = {-50.0f, -50.0f}},
le::Vertex{.position = {+50.0f, -50.0f}},
le::Vertex{.position = {+0.0f, +75.0f}},
};
m_circle.create(50.0f);
Game::Game(gsl::not_null<le::ServiceLocator const*> services) : m_services(services), m_lighthouse(services), m_light(services) {
spawn_wave();
auto const& data_loader = services->get<le::IDataLoader>();
auto const& context = services->get<le::Context>();
auto const asset_loader = le::AssetLoader{&data_loader, &context};
m_font = asset_loader.load_font("fonts/specialElite.ttf");
}

void Game::on_cursor_pos(le::event::CursorPos const& cursor_pos) {
Expand All @@ -19,21 +31,70 @@ void Game::on_cursor_pos(le::event::CursorPos const& cursor_pos) {
}

void Game::tick([[maybe_unused]] kvf::Seconds const dt) {
m_circle.transform.position = m_cursor_pos;

auto const dist_sq = glm::length2(m_cursor_pos);
if (dist_sq > 0.1f) {
auto const dist = std::sqrt(dist_sq);
auto const normalized = m_cursor_pos / dist;
static constexpr auto up_v = glm::vec2{0.0f, 1.0f};
auto const dot = glm::dot(normalized, up_v);
auto const angle = glm::degrees(std::acos(dot));
m_triangle.transform.orientation = m_cursor_pos.x > 0.0f ? -angle : angle;
m_running = m_lighthouse.get_health() > 0;
if (!m_running) { return; }

m_time_since_last_wave_spawn += dt;
if (m_time_since_last_wave_spawn >= m_wave_interval) {
spawn_wave();
m_time_since_last_wave_spawn = kvf::Seconds{};
}
for (auto& enemy : m_enemies) {
m_light.check_enemy_collision(enemy);
m_lighthouse.check_visibility_range(enemy);
update_health_text();
enemy.translate(dt);
}
// Keep track of how many enemies were defeated and calculate score
auto res = std::erase_if(m_enemies, [](Enemy const& enemy) { return !enemy.get_health(); });
update_score(static_cast<int>(res * 10));
m_light.set_position(m_cursor_pos);
m_lighthouse.rotate_towards_cursor(m_cursor_pos);
}

void Game::render(le::Renderer& renderer) const {
m_triangle.draw(renderer);
m_circle.draw(renderer);
m_light.render(renderer);
m_lighthouse.render(renderer);
for (auto const& enemy : m_enemies) { enemy.render(renderer); }
m_score_text.draw(renderer);
m_health_text.draw(renderer);
}

void Game::spawn_wave() {
++m_wave_count;
m_wave_interval += kvf::Seconds{5};
std::vector<Enemy> new_wave;
std::size_t const wave_size = m_wave_count * 3;
new_wave.reserve(wave_size);
for (std::size_t i = 0; i < wave_size; ++i) {
new_wave.emplace_back(m_services, EnemyParams{.target_pos = glm::vec2{0.0f, 0.0f}, .move_speed = util::random_range(35.0f, 65.0f)});
}
m_enemies.insert(m_enemies.end(), std::make_move_iterator(new_wave.begin()), std::make_move_iterator(new_wave.end()));
}

void Game::update_score(int points) {
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
m_score_text.transform.position.y = static_cast<float>(framebuffer_size.y) / 2.0f - 50.0f;
m_score += points;
m_score_str.clear();
std::format_to(std::back_inserter(m_score_str), "Score: {}", m_score);
m_score_text.set_string(m_font, m_score_str);
}

void Game::update_health_text() {
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
float const x = (static_cast<float>(framebuffer_size.x) * 0.5f) - 150.0f;
float const y = (static_cast<float>(framebuffer_size.y) * 0.5f) - 50.0f;
m_health_text.transform.position = {x, y};

m_health_str.clear();
if (m_lighthouse.get_health() <= 0.0f) {
std::format_to(std::back_inserter(m_health_str), "Game Over");
} else {
std::format_to(std::back_inserter(m_health_str), "Health: {:.1f}", m_lighthouse.get_health());
}

m_health_text.set_string(m_font, m_health_str);
}

} // namespace miracle
25 changes: 22 additions & 3 deletions src/game.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
#include <le2d/event.hpp>
#include <le2d/renderer.hpp>
#include <le2d/service_locator.hpp>
#include <cstddef>
#include "enemy.hpp"
#include "le2d/drawable/text.hpp"
#include "le2d/font.hpp"
#include "lighhouse.hpp"
#include "light.hpp"

namespace miracle {
class Game {
Expand All @@ -14,13 +20,26 @@ class Game {

void tick(kvf::Seconds dt);
void render(le::Renderer& renderer) const;
void update_score(int points);
void update_health_text();
void spawn_wave();

private:
gsl::not_null<le::ServiceLocator const*> m_services;
Lighthouse m_lighthouse;
Light m_light;

le::drawable::Triangle m_triangle{};
le::drawable::Circle m_circle{};

le::Font m_font{};
le::drawable::Text m_score_text{};
le::drawable::Text m_health_text{};
int m_score{};
std::string m_score_str;
std::string m_health_str;
glm::vec2 m_cursor_pos{};
std::size_t m_wave_count{};
bool m_running{true};
kvf::Seconds m_wave_interval{};
kvf::Seconds m_time_since_last_wave_spawn{};
std::vector<Enemy> m_enemies{};
};
} // namespace miracle
34 changes: 34 additions & 0 deletions src/lighhouse.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once
#include <glm/gtx/norm.hpp>
#include <kvf/time.hpp>
#include <le2d/context.hpp>
#include <le2d/drawable/shape.hpp>
#include <le2d/event.hpp>
#include <le2d/renderer.hpp>
#include <le2d/service_locator.hpp>
#include <cstddef>
#include <optional>
#include "enemy.hpp"
#include "le2d/texture.hpp"

namespace miracle {
class Lighthouse {
public:
explicit Lighthouse(gsl::not_null<le::ServiceLocator const*> services);

void rotate_towards_cursor(glm::vec2 cursor_pos);
void render(le::Renderer& renderer) const;
void check_visibility_range(Enemy& enemy);
void check_damage_taken(Enemy& enemy);
void take_damage(float dmg);
[[nodiscard]] float get_health() const;

private:
gsl::not_null<le::ServiceLocator const*> m_services;
float m_hitbox_diameter{150.0f};
float m_visibility_diameter{250.0f};
std::optional<le::Texture> m_texture;
le::drawable::Circle m_sprite{};
float m_health{100};
};
} // namespace miracle
17 changes: 17 additions & 0 deletions src/light.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <light.hpp>
#include "kvf/color.hpp"

namespace miracle {
Light::Light(gsl::not_null<le::ServiceLocator const*> services) : m_services(services), m_diameter(100) { m_sprite.create(150.0f, kvf::white_v); }

void Light::check_enemy_collision(Enemy& enemy) {
auto const [pos, diameter] = enemy.get_collision_params();
if (glm::distance(pos, m_sprite.transform.position) < (diameter + m_diameter) / 2) {
enemy.take_damage(1);
} else {
enemy.can_render = false;
}
}
void Light::render(le::Renderer& renderer) const { m_sprite.draw(renderer); }
void Light::set_position(glm::vec2 cursor_pos) { m_sprite.transform.position = cursor_pos; }
} // namespace miracle
26 changes: 26 additions & 0 deletions src/light.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include "enemy.hpp"
#include "glm/vec2.hpp"
#include "gsl/pointers"
#include "le2d/drawable/shape.hpp"
#include "le2d/renderer.hpp"
#include "le2d/service_locator.hpp"
#include "le2d/texture.hpp"

namespace miracle {
class Light {
public:
explicit Light(gsl::not_null<le::ServiceLocator const*> services);

void render(le::Renderer& renderer) const;
void set_position(glm::vec2 cursor_pos);
void check_enemy_collision(Enemy& enemy);

private:
gsl::not_null<le::ServiceLocator const*> m_services;
std::optional<le::Texture> m_texture;
le::drawable::Circle m_sprite{};
float m_diameter{};
};
} // namespace miracle
Loading