Skip to content

Commit 325b770

Browse files
authored
Add powerups and their probabilistic spawning. (#27)
* Setup powerup framework. * Add `EnemyDeathListener`. This consolidates death events from multiple sources (`EnemySpawner`s) into a single sink (`World`), which can then decide whether to spawn a powerup there. * Cleanup.
1 parent 73e7024 commit 325b770

18 files changed

+187
-27
lines changed

src/spaced/spaced/game/enemies/basic_creep_factory.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ using bave::ParticleEmitter;
1212
using bave::random_in_range;
1313
using bave::Seconds;
1414

15-
BasicCreepFactory::BasicCreepFactory(NotNull<Services const*> services, NotNull<IScorer*> scorer, dj::Json const& json)
16-
: m_services(services), m_scorer(scorer) {
15+
BasicCreepFactory::BasicCreepFactory(NotNull<Services const*> services, NotNull<IEnemyDeathListener*> listener, dj::Json const& json)
16+
: m_services(services), m_listener(listener) {
1717
for (auto const& tint : json["tints"].array_view()) { tints.push_back(tint.as<std::string>()); }
1818
if (auto const in_death_emitter = services->get<Resources>().get<ParticleEmitter>(json["death_emitter"].as_string())) { death_emitter = *in_death_emitter; }
1919
spawn_rate = Seconds{json["spawn_rate"].as<float>(spawn_rate.count())};
2020
}
2121

2222
auto BasicCreepFactory::spawn_enemy() -> std::unique_ptr<Enemy> {
23-
auto ret = std::make_unique<Creep>(*m_services, m_scorer);
23+
auto ret = std::make_unique<Creep>(*m_services, m_listener);
2424
if (!tints.empty()) {
2525
auto const& rgbas = m_services->get<Styles>().rgbas;
2626
auto const tint_index = random_in_range(std::size_t{}, tints.size() - 1);

src/spaced/spaced/game/enemies/basic_creep_factory.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class BasicCreepFactory : public IEnemyFactory {
77
public:
88
static constexpr std::string_view type_v{"BasicCreepFactory"};
99

10-
explicit BasicCreepFactory(bave::NotNull<Services const*> services, bave::NotNull<IScorer*> scorer, dj::Json const& json);
10+
explicit BasicCreepFactory(bave::NotNull<Services const*> services, bave::NotNull<IEnemyDeathListener*> listener, dj::Json const& json);
1111

1212
[[nodiscard]] auto get_type_name() const -> std::string_view final { return type_v; }
1313
[[nodiscard]] auto spawn_enemy() -> std::unique_ptr<Enemy> final;
@@ -23,7 +23,7 @@ class BasicCreepFactory : public IEnemyFactory {
2323
void do_inspect() final;
2424

2525
bave::NotNull<Services const*> m_services;
26-
bave::NotNull<IScorer*> m_scorer;
26+
bave::NotNull<IEnemyDeathListener*> m_listener;
2727
bave::Seconds m_elapsed{};
2828
};
2929
} // namespace spaced

src/spaced/spaced/game/enemies/creep.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace spaced {
55
class Creep : public Enemy {
66
public:
7-
explicit Creep(Services const& services, bave::NotNull<IScorer*> scorer) : Enemy(services, scorer, "Creep") {}
7+
explicit Creep(Services const& services, bave::NotNull<IEnemyDeathListener*> listener) : Enemy(services, listener, "Creep") {}
88

99
void tick(bave::Seconds dt) override;
1010

src/spaced/spaced/game/enemy.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ using bave::im_text;
77
using bave::random_in_range;
88
using bave::RoundedQuad;
99

10-
Enemy::Enemy(Services const& services, bave::NotNull<IScorer*> scorer, std::string_view const type)
11-
: m_layout(&services.get<ILayout>()), m_scorer(scorer), m_type(type) {
10+
Enemy::Enemy(Services const& services, bave::NotNull<IEnemyDeathListener*> listener, std::string_view const type)
11+
: m_layout(&services.get<ILayout>()), m_listener(listener), m_type(type) {
1212
static constexpr auto init_size_v = glm::vec2{100.0f};
1313
auto const play_area = m_layout->get_play_area();
1414
auto const y_min = play_area.rb.y + 0.5f * init_size_v.y;
@@ -19,7 +19,7 @@ Enemy::Enemy(Services const& services, bave::NotNull<IScorer*> scorer, std::stri
1919
auto Enemy::take_damage(float const damage) -> bool {
2020
if (is_destroyed()) { return false; }
2121
health.inflict_damage(damage);
22-
if (health.is_dead()) { m_scorer->add_score(points); }
22+
if (health.is_dead()) { m_listener->on_death(EnemyDeath{.position = shape.transform.position, .points = points}); }
2323
return true;
2424
}
2525

src/spaced/spaced/game/enemy.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
#include <bave/graphics/shape.hpp>
44
#include <bave/platform.hpp>
55
#include <spaced/game/damageable.hpp>
6+
#include <spaced/game/enemy_death.hpp>
67
#include <spaced/game/health.hpp>
7-
#include <spaced/game/scorer.hpp>
88
#include <spaced/services/layout.hpp>
99
#include <spaced/services/services.hpp>
1010

1111
namespace spaced {
1212
class Enemy : public IDamageable, public bave::IDrawable {
1313
public:
14-
explicit Enemy(Services const& services, bave::NotNull<IScorer*> scorer, std::string_view type);
14+
explicit Enemy(Services const& services, bave::NotNull<IEnemyDeathListener*> listener, std::string_view type);
1515

1616
[[nodiscard]] auto get_bounds() const -> bave::Rect<> override { return shape.get_bounds(); }
1717
auto take_damage(float damage) -> bool override;
@@ -26,7 +26,7 @@ class Enemy : public IDamageable, public bave::IDrawable {
2626
void setup(glm::vec2 max_size, float y_position);
2727

2828
[[nodiscard]] auto get_layout() const -> ILayout const& { return *m_layout; }
29-
[[nodiscard]] auto get_scorer() const -> IScorer& { return *m_scorer; }
29+
[[nodiscard]] auto get_death_listener() const -> IEnemyDeathListener& { return *m_listener; }
3030

3131
void inspect() {
3232
if constexpr (bave::debug_v) { do_inspect(); }
@@ -40,7 +40,7 @@ class Enemy : public IDamageable, public bave::IDrawable {
4040
virtual void do_inspect();
4141

4242
bave::NotNull<ILayout const*> m_layout;
43-
bave::NotNull<IScorer*> m_scorer;
43+
bave::NotNull<IEnemyDeathListener*> m_listener;
4444
std::string_view m_type{};
4545
bool m_destroyed{};
4646
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
#include <bave/core/polymorphic.hpp>
3+
#include <glm/vec2.hpp>
4+
#include <cstdint>
5+
6+
namespace spaced {
7+
struct EnemyDeath {
8+
glm::vec2 position{};
9+
std::int64_t points{};
10+
};
11+
12+
class IEnemyDeathListener : public bave::Polymorphic {
13+
public:
14+
virtual void on_death(EnemyDeath const& death) = 0;
15+
};
16+
} // namespace spaced

src/spaced/spaced/game/enemy_factory_builder.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
namespace spaced {
55
using bave::NotNull;
66

7-
EnemyFactoryBuilder::EnemyFactoryBuilder(NotNull<Services const*> services, NotNull<IScorer*> scorer) : m_services(services), m_scorer(scorer) {}
7+
EnemyFactoryBuilder::EnemyFactoryBuilder(NotNull<Services const*> services, NotNull<IEnemyDeathListener*> listener)
8+
: m_services(services), m_listener(listener) {}
89

910
auto EnemyFactoryBuilder::build(dj::Json const& json) const -> std::unique_ptr<IEnemyFactory> {
1011
auto const type_name = json["type_name"].as_string();
1112

12-
if (type_name == BasicCreepFactory::type_v) { return std::make_unique<BasicCreepFactory>(m_services, m_scorer, json); }
13+
if (type_name == BasicCreepFactory::type_v) { return std::make_unique<BasicCreepFactory>(m_services, m_listener, json); }
1314

1415
return build_default();
1516
}
1617

1718
auto EnemyFactoryBuilder::build_default() const -> std::unique_ptr<IEnemyFactory> {
18-
auto ret = std::make_unique<BasicCreepFactory>(m_services, m_scorer, dj::Json{});
19+
auto ret = std::make_unique<BasicCreepFactory>(m_services, m_listener, dj::Json{});
1920
ret->tints = {"orange", "milk"};
2021
return ret;
2122
}

src/spaced/spaced/game/enemy_factory_builder.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
namespace spaced {
66
class EnemyFactoryBuilder {
77
public:
8-
explicit EnemyFactoryBuilder(bave::NotNull<Services const*> services, bave::NotNull<IScorer*> scorer);
8+
explicit EnemyFactoryBuilder(bave::NotNull<Services const*> services, bave::NotNull<IEnemyDeathListener*> listener);
99

1010
[[nodiscard]] auto build(dj::Json const& json) const -> std::unique_ptr<IEnemyFactory>;
1111

1212
[[nodiscard]] auto build_default() const -> std::unique_ptr<IEnemyFactory>;
1313

1414
private:
1515
bave::NotNull<Services const*> m_services;
16-
bave::NotNull<IScorer*> m_scorer;
16+
bave::NotNull<IEnemyDeathListener*> m_listener;
1717
};
1818
} // namespace spaced

src/spaced/spaced/game/player.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
#include <spaced/game/weapons/gun_beam.hpp>
1010

1111
namespace spaced {
12-
using bave::im_text;
13-
using bave::NotNull;
1412
using bave::ParticleEmitter;
1513
using bave::PointerMove;
1614
using bave::PointerTap;
@@ -26,15 +24,19 @@ void Player::on_move(PointerMove const& pointer_move) { m_controller->on_move(po
2624

2725
void Player::on_tap(PointerTap const& pointer_tap) { m_controller->on_tap(pointer_tap); }
2826

29-
void Player::tick(std::span<NotNull<IDamageable*> const> targets, Seconds const dt) {
27+
void Player::tick(State const& state, Seconds const dt) {
3028
auto const y_position = m_controller->tick(dt);
3129
set_y(y_position);
3230

33-
auto const round_state = IWeaponRound::State{.targets = targets, .muzzle_position = get_muzzle_position()};
31+
auto const round_state = IWeaponRound::State{.targets = state.targets, .muzzle_position = get_muzzle_position()};
3432
m_arsenal.tick(round_state, m_controller->is_firing(), dt);
3533

3634
m_exhaust.set_position(get_exhaust_position());
3735
m_exhaust.tick(dt);
36+
37+
for (auto const& powerup : state.powerups) {
38+
if (is_intersecting(powerup->get_bounds(), ship.get_bounds())) { powerup->activate(*this); }
39+
}
3840
}
3941

4042
void Player::draw(Shader& shader) const {

src/spaced/spaced/game/player.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
#include <spaced/game/arsenal.hpp>
66
#include <spaced/game/controller.hpp>
77
#include <spaced/game/health.hpp>
8+
#include <spaced/game/powerup.hpp>
89
#include <spaced/game/world_spec.hpp>
910

1011
namespace spaced {
1112
class Player : public bave::IDrawable {
1213
public:
14+
struct State {
15+
std::span<bave::NotNull<IDamageable*> const> targets{};
16+
std::span<bave::NotNull<IPowerup*> const> powerups{};
17+
};
18+
1319
explicit Player(Services const& services, std::unique_ptr<IController> controller);
1420

1521
void on_focus(bave::FocusChange const& focus_change);
1622
void on_move(bave::PointerMove const& pointer_move);
1723
void on_tap(bave::PointerTap const& pointer_tap);
1824

19-
void tick(std::span<bave::NotNull<IDamageable*> const> targets, bave::Seconds dt);
25+
void tick(State const& state, bave::Seconds dt);
2026
void draw(bave::Shader& shader) const final;
2127

2228
void setup(WorldSpec::Player const& spec);

src/spaced/spaced/game/powerup.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
#include <bave/core/polymorphic.hpp>
3+
#include <bave/core/time.hpp>
4+
#include <bave/graphics/drawable.hpp>
5+
#include <bave/graphics/rect.hpp>
6+
7+
namespace spaced {
8+
class Player;
9+
10+
class IPowerup : public bave::IDrawable {
11+
public:
12+
[[nodiscard]] virtual auto get_bounds() const -> bave::Rect<> = 0;
13+
virtual void activate(Player& player) = 0;
14+
15+
[[nodiscard]] virtual auto is_destroyed() const -> bool = 0;
16+
17+
virtual void tick(bave::Seconds dt) = 0;
18+
};
19+
} // namespace spaced
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <spaced/game/powerups/pu_base.hpp>
2+
3+
namespace spaced {
4+
using bave::Circle;
5+
using bave::Seconds;
6+
using bave::Shader;
7+
8+
PUBase::PUBase(Services const& services, std::string_view const name) : m_services(&services), m_layout(&services.get<ILayout>()), m_name(name) {
9+
auto circle = Circle{.diameter = 50.0f};
10+
shape.set_shape(circle);
11+
}
12+
13+
void PUBase::tick(Seconds const dt) {
14+
shape.transform.position.x -= speed * dt.count();
15+
if (shape.transform.position.x < m_layout->get_play_area().lt.x - 0.5f * shape.get_shape().diameter) { m_destroyed = true; }
16+
}
17+
18+
void PUBase::draw(Shader& shader) const { shape.draw(shader); }
19+
20+
void PUBase::activate(Player& player) {
21+
do_activate(player);
22+
m_destroyed = true;
23+
}
24+
} // namespace spaced
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#pragma once
2+
#include <bave/core/time.hpp>
3+
#include <bave/graphics/shape.hpp>
4+
#include <spaced/game/powerup.hpp>
5+
#include <spaced/services/layout.hpp>
6+
#include <spaced/services/services.hpp>
7+
8+
namespace spaced {
9+
class PUBase : public IPowerup {
10+
public:
11+
explicit PUBase(Services const& services, std::string_view name);
12+
13+
void tick(bave::Seconds dt) final;
14+
void draw(bave::Shader& shader) const final;
15+
16+
[[nodiscard]] auto get_bounds() const -> bave::Rect<> final { return shape.get_bounds(); }
17+
void activate(Player& player) final;
18+
19+
[[nodiscard]] auto is_destroyed() const -> bool final { return m_destroyed; }
20+
21+
float speed{300.0f};
22+
bave::CircleShape shape{};
23+
24+
protected:
25+
virtual void do_activate(Player& player) = 0;
26+
27+
bave::NotNull<Services const*> m_services;
28+
bave::NotNull<ILayout const*> m_layout;
29+
std::string_view m_name{};
30+
bool m_destroyed{};
31+
};
32+
} // namespace spaced
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <spaced/game/player.hpp>
2+
#include <spaced/game/powerups/pu_beam.hpp>
3+
#include <spaced/game/weapons/gun_beam.hpp>
4+
#include <spaced/services/styles.hpp>
5+
6+
namespace spaced {
7+
PUBeam::PUBeam(Services const& services, int rounds) : PUBase(services, "Beam"), m_rounds(rounds) { shape.tint = services.get<Styles>().rgbas["gun_beam"]; }
8+
9+
void PUBeam::do_activate(Player& player) {
10+
auto beam = std::make_unique<GunBeam>(*m_services);
11+
beam->rounds = m_rounds;
12+
player.set_special_weapon(std::move(beam));
13+
}
14+
} // namespace spaced
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#pragma once
2+
#include <spaced/game/powerups/pu_base.hpp>
3+
4+
namespace spaced {
5+
class PUBeam : public PUBase {
6+
public:
7+
explicit PUBeam(Services const& services, int rounds = 2);
8+
9+
private:
10+
void do_activate(Player& player) final;
11+
12+
int m_rounds{};
13+
};
14+
} // namespace spaced

src/spaced/spaced/game/weapons/gun_kinetic.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class GunKinetic final : public Weapon {
88
explicit GunKinetic(Services const& services);
99

1010
auto fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> final;
11-
[[nodiscard]] auto is_idle() const -> bool final { return m_reload_remain >= 0s; }
11+
[[nodiscard]] auto is_idle() const -> bool final { return m_reload_remain <= 0s; }
1212

1313
void tick(bave::Seconds dt) final;
1414

0 commit comments

Comments
 (0)