diff --git a/assets/fonts/specialElite.ttf b/assets/fonts/specialElite.ttf new file mode 100644 index 0000000..ba58d36 Binary files /dev/null and b/assets/fonts/specialElite.ttf differ diff --git a/assets/images/lighthouse.png b/assets/images/lighthouse.png new file mode 100644 index 0000000..ce3f73c Binary files /dev/null and b/assets/images/lighthouse.png differ diff --git a/ext/src.zip b/ext/src.zip index 3082690..9119d18 100644 Binary files a/ext/src.zip and b/ext/src.zip differ diff --git a/src/EnemyColliderInfo.hpp b/src/EnemyColliderInfo.hpp new file mode 100644 index 0000000..7d06d9c --- /dev/null +++ b/src/EnemyColliderInfo.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +namespace miracle { +struct CollisionParams { + glm::vec2 pos; + float diameter; +}; +} // namespace miracle diff --git a/src/app.cpp b/src/app.cpp index 1ea15cb..a583071 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,23 +4,17 @@ #include #include #include -#include 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().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}; diff --git a/src/enemy.cpp b/src/enemy.cpp new file mode 100644 index 0000000..8e54e1e --- /dev/null +++ b/src/enemy.cpp @@ -0,0 +1,36 @@ +#include +#include +#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 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().framebuffer_size(); + auto const radius = static_cast(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 diff --git a/src/enemy.hpp b/src/enemy.hpp new file mode 100644 index 0000000..961c946 --- /dev/null +++ b/src/enemy.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 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 m_services; + std::optional 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 diff --git a/src/enemy_params.hpp b/src/enemy_params.hpp new file mode 100644 index 0000000..aa8e588 --- /dev/null +++ b/src/enemy_params.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "glm/vec2.hpp" + +struct EnemyParams { + glm::vec2 target_pos{}; + float move_speed{}; +}; diff --git a/src/game.cpp b/src/game.cpp index c5ed010..aebad4f 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1,16 +1,28 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#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 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 services) : m_services(services), m_lighthouse(services), m_light(services) { + spawn_wave(); + auto const& data_loader = services->get(); + auto const& context = services->get(); + 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) { @@ -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(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 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().framebuffer_size(); + m_score_text.transform.position.y = static_cast(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().framebuffer_size(); + float const x = (static_cast(framebuffer_size.x) * 0.5f) - 150.0f; + float const y = (static_cast(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 diff --git a/src/game.hpp b/src/game.hpp index 1601115..857e444 100644 --- a/src/game.hpp +++ b/src/game.hpp @@ -4,6 +4,12 @@ #include #include #include +#include +#include "enemy.hpp" +#include "le2d/drawable/text.hpp" +#include "le2d/font.hpp" +#include "lighhouse.hpp" +#include "light.hpp" namespace miracle { class Game { @@ -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 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 m_enemies{}; }; } // namespace miracle diff --git a/src/lighhouse.hpp b/src/lighhouse.hpp new file mode 100644 index 0000000..cfc4bf2 --- /dev/null +++ b/src/lighhouse.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "enemy.hpp" +#include "le2d/texture.hpp" + +namespace miracle { +class Lighthouse { + public: + explicit Lighthouse(gsl::not_null 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 m_services; + float m_hitbox_diameter{150.0f}; + float m_visibility_diameter{250.0f}; + std::optional m_texture; + le::drawable::Circle m_sprite{}; + float m_health{100}; +}; +} // namespace miracle diff --git a/src/light.cpp b/src/light.cpp new file mode 100644 index 0000000..1f717cb --- /dev/null +++ b/src/light.cpp @@ -0,0 +1,17 @@ +#include +#include "kvf/color.hpp" + +namespace miracle { +Light::Light(gsl::not_null 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 diff --git a/src/light.hpp b/src/light.hpp new file mode 100644 index 0000000..5b0a1f4 --- /dev/null +++ b/src/light.hpp @@ -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 services); + + void render(le::Renderer& renderer) const; + void set_position(glm::vec2 cursor_pos); + void check_enemy_collision(Enemy& enemy); + + private: + gsl::not_null m_services; + std::optional m_texture; + le::drawable::Circle m_sprite{}; + float m_diameter{}; +}; +} // namespace miracle diff --git a/src/lighthouse.cpp b/src/lighthouse.cpp new file mode 100644 index 0000000..27ed7eb --- /dev/null +++ b/src/lighthouse.cpp @@ -0,0 +1,46 @@ +#include +#include "glm/geometric.hpp" +#include "le2d/asset_loader.hpp" +#include "le2d/data_loader.hpp" + +namespace miracle { +Lighthouse::Lighthouse(gsl::not_null services) : m_services(services) { + m_sprite.create(m_hitbox_diameter); + auto const& data_loader = services->get(); + auto const& context = services->get(); + auto const asset_loader = le::AssetLoader{&data_loader, &context}; + m_texture = asset_loader.load_texture("images/lighthouse.png"); + m_sprite.texture = &m_texture.value(); +} + +void Lighthouse::rotate_towards_cursor(glm::vec2 cursor_pos) { + auto const dist_sq = glm::length2((cursor_pos)); + if (dist_sq > 0.1f) { + auto const dist = std::sqrt(dist_sq); + auto const normalized = 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_sprite.transform.orientation = cursor_pos.x > 0.0f ? -angle : angle; + } +} +void Lighthouse::check_visibility_range(Enemy& enemy) { + auto [enemy_pos, enemy_diameter] = enemy.get_collision_params(); + if (glm::distance(m_sprite.transform.position, enemy_pos) < (m_visibility_diameter + enemy_diameter) / 2.0f) { + enemy.can_render = true; + // No need to check for damage taken if enemy isn't already in visibility radius + check_damage_taken(enemy); + } +} +void Lighthouse::check_damage_taken(Enemy& enemy) { + auto [enemy_pos, enemy_diameter] = enemy.get_collision_params(); + if (glm::distance(m_sprite.transform.position, enemy_pos) < (m_hitbox_diameter + enemy_diameter) / 2.0f) { + take_damage(enemy_diameter / 200); + } // magic numbers +} + +void Lighthouse::take_damage(float dmg) { m_health -= dmg; } + +float Lighthouse::get_health() const { return m_health; } +void Lighthouse::render(le::Renderer& renderer) const { m_sprite.draw(renderer); } +} // namespace miracle diff --git a/src/util/random.hpp b/src/util/random.hpp index 7300b26..7f6976f 100644 --- a/src/util/random.hpp +++ b/src/util/random.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include +#include #include - namespace miracle::util { /// \brief Wrapper that reuses the same random engine for all calls. class Random { @@ -34,4 +36,9 @@ template KLIB_ASSERT(size > 0); return Random::in_range(0uz, size - 1); } +// Returns a random coordinate on the specified radius +[[nodiscard]] inline auto get_random_location_on_radius(float radius) -> glm::vec2 { + float const angle = random_range(0.0f, 2.0f * std::numbers::pi_v); + return {radius * std::cos(angle), radius * std::sin(angle)}; +} } // namespace miracle::util