Skip to content

Commit 145da27

Browse files
authored
Add Enemy wave system, Lighthouse, and Light mechanics
- Implement enemy wave spawning with increasing intervals - Add enemy health and targeted movement toward the Lighthouse - Add basic collision detection and enemy removal - Introduce Lighthouse: - Takes damage upon enemy contact - Tracks and displays health - Stops the game when health reaches 0 - Rotates to face the cursor - Add Light: - Reveals enemies within a radius - Applies damage over time to exposed enemies - Display score on screen and update based on enemy kills
1 parent d220ac7 commit 145da27

File tree

15 files changed

+327
-34
lines changed

15 files changed

+327
-34
lines changed

assets/fonts/specialElite.ttf

148 KB
Binary file not shown.

assets/images/lighthouse.png

41.2 KB
Loading

ext/src.zip

586 Bytes
Binary file not shown.

src/EnemyColliderInfo.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
#include <glm/vec2.hpp>
3+
4+
namespace miracle {
5+
struct CollisionParams {
6+
glm::vec2 pos;
7+
float diameter;
8+
};
9+
} // namespace miracle

src/app.cpp

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,17 @@
44
#include <game.hpp>
55
#include <klib/visitor.hpp>
66
#include <log.hpp>
7-
#include <util/random.hpp>
87

98
namespace miracle {
109
namespace {
10+
constexpr auto window_size = glm::ivec2{800, 800};
11+
constexpr auto window_flags = le::default_window_flags_v & ~le::WindowFlag::Resizeable;
1112
constexpr auto context_ci = le::Context::CreateInfo{
12-
.window = le::WindowInfo{.size = {1280, 720}, .title = "miracle"},
13+
.window = le::WindowInfo{.size = window_size, .title = "miracle", .flags = window_flags},
1314
};
1415
} // namespace
1516

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

2519
void App::run() {
2620
auto game = Game{&m_services};

src/enemy.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include <enemy.hpp>
2+
#include <algorithm>
3+
#include "enemy_params.hpp"
4+
#include "glm/geometric.hpp"
5+
#include "glm/vec2.hpp"
6+
#include "kvf/color.hpp"
7+
#include "util/random.hpp"
8+
9+
namespace miracle {
10+
Enemy::Enemy(gsl::not_null<le::ServiceLocator const*> services, EnemyParams const& params)
11+
: m_services(services), m_target_pos(params.target_pos), m_move_speed(params.move_speed), m_diameter(util::random_range(40.0f, 60.0f)) {
12+
m_sprite.create(m_diameter, kvf::red_v);
13+
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
14+
auto const radius = static_cast<float>(std::max(framebuffer_size.x, framebuffer_size.y)) / 2.0f;
15+
16+
m_sprite.transform.position = util::get_random_location_on_radius(radius);
17+
// TODO: add proper textures
18+
}
19+
20+
void Enemy::render(le::Renderer& renderer) const {
21+
if (can_render) { m_sprite.draw(renderer); }
22+
}
23+
24+
void Enemy::translate(kvf::Seconds const dt) {
25+
glm::vec2 const direction = glm::normalize(m_target_pos - m_sprite.transform.position);
26+
glm::vec2 const movement = direction * m_move_speed * dt.count();
27+
m_sprite.transform.position += movement;
28+
}
29+
30+
CollisionParams Enemy::get_collision_params() const { return {.pos = m_sprite.transform.position, .diameter = m_diameter}; }
31+
void Enemy::take_damage(std::size_t dmg) {
32+
can_render = true;
33+
m_health = (dmg >= m_health) ? 0 : (m_health - dmg);
34+
}
35+
36+
} // namespace miracle

src/enemy.hpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#pragma once
2+
#include <glm/gtx/norm.hpp>
3+
#include <kvf/time.hpp>
4+
#include <le2d/context.hpp>
5+
#include <le2d/drawable/shape.hpp>
6+
#include <le2d/event.hpp>
7+
#include <le2d/renderer.hpp>
8+
#include <le2d/service_locator.hpp>
9+
#include <cstddef>
10+
#include <optional>
11+
#include "EnemyColliderInfo.hpp"
12+
#include "enemy_params.hpp"
13+
#include "glm/vec2.hpp"
14+
#include "le2d/texture.hpp"
15+
16+
namespace miracle {
17+
class Enemy {
18+
public:
19+
explicit Enemy(gsl::not_null<le::ServiceLocator const*> services, EnemyParams const& params);
20+
21+
void render(le::Renderer& renderer) const;
22+
void translate(kvf::Seconds dt);
23+
void take_damage(std::size_t dmg);
24+
[[nodiscard]] std::size_t get_health() const { return m_health; }
25+
[[nodiscard]] CollisionParams get_collision_params() const;
26+
bool can_render{false};
27+
28+
private:
29+
gsl::not_null<le::ServiceLocator const*> m_services;
30+
std::optional<le::Texture> m_texture;
31+
le::drawable::Circle m_sprite{};
32+
glm::vec2 m_target_pos{};
33+
float m_move_speed{};
34+
float m_diameter{};
35+
std::size_t m_health{100};
36+
};
37+
} // namespace miracle

src/enemy_params.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
#include "glm/vec2.hpp"
3+
4+
struct EnemyParams {
5+
glm::vec2 target_pos{};
6+
float move_speed{};
7+
};

src/game.cpp

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
#include <game.hpp>
22
#include <glm/gtx/norm.hpp>
33
#include <le2d/context.hpp>
4-
#include <cmath>
4+
#include <cstddef>
5+
#include <format>
6+
#include <iterator>
7+
#include <string>
8+
#include <vector>
9+
#include "enemy.hpp"
10+
#include "enemy_params.hpp"
11+
#include "glm/ext/vector_float2.hpp"
12+
#include "kvf/time.hpp"
13+
#include "le2d/asset_loader.hpp"
14+
#include "le2d/data_loader.hpp"
15+
#include "le2d/drawable/text.hpp"
16+
#include "lighhouse.hpp"
17+
#include "util/random.hpp"
518

619
namespace miracle {
7-
Game::Game(gsl::not_null<le::ServiceLocator const*> services) : m_services(services) {
8-
m_triangle.vertices = {
9-
le::Vertex{.position = {-50.0f, -50.0f}},
10-
le::Vertex{.position = {+50.0f, -50.0f}},
11-
le::Vertex{.position = {+0.0f, +75.0f}},
12-
};
13-
m_circle.create(50.0f);
20+
Game::Game(gsl::not_null<le::ServiceLocator const*> services) : m_services(services), m_lighthouse(services), m_light(services) {
21+
spawn_wave();
22+
auto const& data_loader = services->get<le::IDataLoader>();
23+
auto const& context = services->get<le::Context>();
24+
auto const asset_loader = le::AssetLoader{&data_loader, &context};
25+
m_font = asset_loader.load_font("fonts/specialElite.ttf");
1426
}
1527

1628
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) {
1931
}
2032

2133
void Game::tick([[maybe_unused]] kvf::Seconds const dt) {
22-
m_circle.transform.position = m_cursor_pos;
23-
24-
auto const dist_sq = glm::length2(m_cursor_pos);
25-
if (dist_sq > 0.1f) {
26-
auto const dist = std::sqrt(dist_sq);
27-
auto const normalized = m_cursor_pos / dist;
28-
static constexpr auto up_v = glm::vec2{0.0f, 1.0f};
29-
auto const dot = glm::dot(normalized, up_v);
30-
auto const angle = glm::degrees(std::acos(dot));
31-
m_triangle.transform.orientation = m_cursor_pos.x > 0.0f ? -angle : angle;
34+
m_running = m_lighthouse.get_health() > 0;
35+
if (!m_running) { return; }
36+
37+
m_time_since_last_wave_spawn += dt;
38+
if (m_time_since_last_wave_spawn >= m_wave_interval) {
39+
spawn_wave();
40+
m_time_since_last_wave_spawn = kvf::Seconds{};
41+
}
42+
for (auto& enemy : m_enemies) {
43+
m_light.check_enemy_collision(enemy);
44+
m_lighthouse.check_visibility_range(enemy);
45+
update_health_text();
46+
enemy.translate(dt);
3247
}
48+
// Keep track of how many enemies were defeated and calculate score
49+
auto res = std::erase_if(m_enemies, [](Enemy const& enemy) { return !enemy.get_health(); });
50+
update_score(static_cast<int>(res * 10));
51+
m_light.set_position(m_cursor_pos);
52+
m_lighthouse.rotate_towards_cursor(m_cursor_pos);
3353
}
3454

3555
void Game::render(le::Renderer& renderer) const {
36-
m_triangle.draw(renderer);
37-
m_circle.draw(renderer);
56+
m_light.render(renderer);
57+
m_lighthouse.render(renderer);
58+
for (auto const& enemy : m_enemies) { enemy.render(renderer); }
59+
m_score_text.draw(renderer);
60+
m_health_text.draw(renderer);
61+
}
62+
63+
void Game::spawn_wave() {
64+
++m_wave_count;
65+
m_wave_interval += kvf::Seconds{5};
66+
std::vector<Enemy> new_wave;
67+
std::size_t const wave_size = m_wave_count * 3;
68+
new_wave.reserve(wave_size);
69+
for (std::size_t i = 0; i < wave_size; ++i) {
70+
new_wave.emplace_back(m_services, EnemyParams{.target_pos = glm::vec2{0.0f, 0.0f}, .move_speed = util::random_range(35.0f, 65.0f)});
71+
}
72+
m_enemies.insert(m_enemies.end(), std::make_move_iterator(new_wave.begin()), std::make_move_iterator(new_wave.end()));
73+
}
74+
75+
void Game::update_score(int points) {
76+
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
77+
m_score_text.transform.position.y = static_cast<float>(framebuffer_size.y) / 2.0f - 50.0f;
78+
m_score += points;
79+
m_score_str.clear();
80+
std::format_to(std::back_inserter(m_score_str), "Score: {}", m_score);
81+
m_score_text.set_string(m_font, m_score_str);
3882
}
83+
84+
void Game::update_health_text() {
85+
auto const framebuffer_size = m_services->get<le::Context>().framebuffer_size();
86+
float const x = (static_cast<float>(framebuffer_size.x) * 0.5f) - 150.0f;
87+
float const y = (static_cast<float>(framebuffer_size.y) * 0.5f) - 50.0f;
88+
m_health_text.transform.position = {x, y};
89+
90+
m_health_str.clear();
91+
if (m_lighthouse.get_health() <= 0.0f) {
92+
std::format_to(std::back_inserter(m_health_str), "Game Over");
93+
} else {
94+
std::format_to(std::back_inserter(m_health_str), "Health: {:.1f}", m_lighthouse.get_health());
95+
}
96+
97+
m_health_text.set_string(m_font, m_health_str);
98+
}
99+
39100
} // namespace miracle

src/game.hpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
#include <le2d/event.hpp>
55
#include <le2d/renderer.hpp>
66
#include <le2d/service_locator.hpp>
7+
#include <cstddef>
8+
#include "enemy.hpp"
9+
#include "le2d/drawable/text.hpp"
10+
#include "le2d/font.hpp"
11+
#include "lighhouse.hpp"
12+
#include "light.hpp"
713

814
namespace miracle {
915
class Game {
@@ -14,13 +20,26 @@ class Game {
1420

1521
void tick(kvf::Seconds dt);
1622
void render(le::Renderer& renderer) const;
23+
void update_score(int points);
24+
void update_health_text();
25+
void spawn_wave();
1726

1827
private:
1928
gsl::not_null<le::ServiceLocator const*> m_services;
29+
Lighthouse m_lighthouse;
30+
Light m_light;
2031

21-
le::drawable::Triangle m_triangle{};
22-
le::drawable::Circle m_circle{};
23-
32+
le::Font m_font{};
33+
le::drawable::Text m_score_text{};
34+
le::drawable::Text m_health_text{};
35+
int m_score{};
36+
std::string m_score_str;
37+
std::string m_health_str;
2438
glm::vec2 m_cursor_pos{};
39+
std::size_t m_wave_count{};
40+
bool m_running{true};
41+
kvf::Seconds m_wave_interval{};
42+
kvf::Seconds m_time_since_last_wave_spawn{};
43+
std::vector<Enemy> m_enemies{};
2544
};
2645
} // namespace miracle

0 commit comments

Comments
 (0)