mirror of https://github.com/auygun/kaliber.git
477 lines
14 KiB
C++
477 lines
14 KiB
C++
#include "enemy.h"
|
|
|
|
#include <cassert>
|
|
#include <functional>
|
|
#include <limits>
|
|
|
|
#include "../base/collusion_test.h"
|
|
#include "../base/interpolation.h"
|
|
#include "../base/log.h"
|
|
#include "../engine/engine.h"
|
|
#include "../engine/font.h"
|
|
#include "../engine/image.h"
|
|
#include "../engine/renderer/texture.h"
|
|
#include "../engine/sound.h"
|
|
#include "demo.h"
|
|
|
|
using namespace base;
|
|
using namespace eng;
|
|
|
|
namespace {
|
|
|
|
constexpr int enemy_frame_start[][3] = {{0, 50, -1},
|
|
{13, 33, -1},
|
|
{-1, -1, 100}};
|
|
constexpr int enemy_frame_count[][3] = {{7, 7, -1}, {6, 6, -1}, {-1, -1, 7}};
|
|
constexpr int enemy_frame_speed = 12;
|
|
|
|
constexpr int enemy_scores[] = {100, 150, 300};
|
|
|
|
constexpr float kSpawnPeriod[kEnemyType_Max][2] = {{2, 5},
|
|
{15, 25},
|
|
{110, 130}};
|
|
|
|
void SetupFadeOutAnim(Animator& animator, float delay) {
|
|
animator.SetEndCallback(Animator::kTimer, [&]() -> void {
|
|
animator.SetBlending({1, 1, 1, 0}, 0.5f,
|
|
std::bind(Acceleration, std::placeholders::_1, -1));
|
|
animator.Play(Animator::kBlending, false);
|
|
});
|
|
animator.SetEndCallback(Animator::kBlending,
|
|
[&]() -> void { animator.SetVisible(false); });
|
|
animator.SetTimer(delay);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Enemy::Enemy()
|
|
: skull_tex_(Engine::Get().CreateRenderResource<Texture>()),
|
|
bug_tex_(Engine::Get().CreateRenderResource<Texture>()),
|
|
target_tex_(Engine::Get().CreateRenderResource<Texture>()),
|
|
blast_tex_(Engine::Get().CreateRenderResource<Texture>()),
|
|
score_tex_{Engine::Get().CreateRenderResource<Texture>(),
|
|
Engine::Get().CreateRenderResource<Texture>(),
|
|
Engine::Get().CreateRenderResource<Texture>()} {}
|
|
|
|
Enemy::~Enemy() = default;
|
|
|
|
bool Enemy::Initialize() {
|
|
explosion_sound_ = std::make_shared<Sound>();
|
|
if (!explosion_sound_->Load("explosion.mp3"))
|
|
return false;
|
|
|
|
return CreateRenderResources();
|
|
}
|
|
|
|
void Enemy::ContextLost() {
|
|
CreateRenderResources();
|
|
}
|
|
|
|
void Enemy::Update(float delta_time) {
|
|
if (!waiting_for_next_wave_) {
|
|
if (spawn_factor_interpolator_ < 1) {
|
|
spawn_factor_interpolator_ += delta_time * 0.1f;
|
|
if (spawn_factor_interpolator_ > 1)
|
|
spawn_factor_interpolator_ = 1;
|
|
}
|
|
|
|
for (int i = 0; i < kEnemyType_Max; ++i)
|
|
seconds_since_last_spawn_[i] += delta_time;
|
|
|
|
SpawnNextEnemy();
|
|
}
|
|
|
|
for (auto it = enemies_.begin(); it != enemies_.end(); ++it) {
|
|
if (it->marked_for_removal) {
|
|
it = enemies_.erase(it);
|
|
continue;
|
|
}
|
|
it->sprite_animator.Update(delta_time);
|
|
it->target_animator.Update(delta_time);
|
|
it->blast_animator.Update(delta_time);
|
|
it->health_animator.Update(delta_time);
|
|
it->score_animator.Update(delta_time);
|
|
it->movement_animator.Update(delta_time);
|
|
}
|
|
}
|
|
|
|
void Enemy::Draw(float frame_frac) {
|
|
for (auto& e : enemies_) {
|
|
e.sprite.Draw();
|
|
e.target.Draw();
|
|
e.blast.Draw();
|
|
e.health_base.Draw();
|
|
e.health_bar.Draw();
|
|
e.score.Draw();
|
|
}
|
|
}
|
|
|
|
bool Enemy::HasTarget(DamageType damage_type) {
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
return GetTarget(damage_type) ? true : false;
|
|
}
|
|
|
|
Vector2 Enemy::GetTargetPos(DamageType damage_type) {
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
if (target)
|
|
return target->sprite.GetOffset() -
|
|
Vector2(0, target->sprite.GetScale().y / 2.5f);
|
|
return {0, 0};
|
|
}
|
|
|
|
void Enemy::SelectTarget(DamageType damage_type,
|
|
const Vector2& origin,
|
|
const Vector2& dir,
|
|
float snap_factor) {
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
if (waiting_for_next_wave_)
|
|
return;
|
|
|
|
EnemyUnit* best_enemy = nullptr;
|
|
|
|
float closest_dist = std::numeric_limits<float>::max();
|
|
for (auto& e : enemies_) {
|
|
if (e.hit_points <= 0 || e.marked_for_removal)
|
|
continue;
|
|
|
|
if (e.targetted_by_weapon_ == damage_type) {
|
|
e.targetted_by_weapon_ = kDamageType_Invalid;
|
|
e.target.SetVisible(false);
|
|
e.target_animator.Stop(Animator::kAllAnimations);
|
|
}
|
|
|
|
if (!base::Intersection(e.sprite.GetOffset(),
|
|
e.sprite.GetScale() * snap_factor, origin, dir))
|
|
continue;
|
|
|
|
Vector2 weapon_enemy_dir = e.sprite.GetOffset() - origin;
|
|
float enemy_weapon_dist = weapon_enemy_dir.Magnitude();
|
|
if (closest_dist > enemy_weapon_dist) {
|
|
closest_dist = enemy_weapon_dist;
|
|
best_enemy = &e;
|
|
}
|
|
}
|
|
|
|
if (best_enemy) {
|
|
best_enemy->targetted_by_weapon_ = damage_type;
|
|
best_enemy->target.SetVisible(true);
|
|
if (damage_type == kDamageType_Green) {
|
|
best_enemy->target.SetFrame(0);
|
|
best_enemy->target_animator.SetFrames(6, 28);
|
|
} else {
|
|
best_enemy->target.SetFrame(6);
|
|
best_enemy->target_animator.SetFrames(6, 28);
|
|
}
|
|
best_enemy->target_animator.Play(Animator::kFrames, false);
|
|
}
|
|
}
|
|
|
|
void Enemy::DeselectTarget(DamageType damage_type) {
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
if (target) {
|
|
target->targetted_by_weapon_ = kDamageType_Invalid;
|
|
target->target.SetVisible(false);
|
|
target->target_animator.Stop(Animator::kAllAnimations);
|
|
}
|
|
}
|
|
|
|
void Enemy::HitTarget(DamageType damage_type) {
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
if (waiting_for_next_wave_)
|
|
return;
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
|
|
if (target) {
|
|
target->target.SetVisible(false);
|
|
target->target_animator.Stop(Animator::kAllAnimations);
|
|
}
|
|
|
|
if (!target || (target->damage_type != kDamageType_Any &&
|
|
target->damage_type != damage_type))
|
|
return;
|
|
|
|
TakeDamage(target, 1);
|
|
}
|
|
|
|
void Enemy::OnWaveFinished() {
|
|
for (auto& e : enemies_) {
|
|
if (!e.marked_for_removal && e.hit_points > 0)
|
|
e.movement_animator.Pause(Animator::kMovement);
|
|
}
|
|
waiting_for_next_wave_ = true;
|
|
}
|
|
|
|
void Enemy::OnWaveStarted(int wave) {
|
|
for (auto& e : enemies_) {
|
|
if (!e.marked_for_removal && e.hit_points > 0) {
|
|
if (wave == 1)
|
|
e.marked_for_removal = true;
|
|
else
|
|
TakeDamage(&e, 100);
|
|
}
|
|
}
|
|
num_enemies_killed_in_current_wave_ = 0;
|
|
seconds_since_last_spawn_ = {0, 0, 0};
|
|
seconds_to_next_spawn_ = {0, 0, 0};
|
|
spawn_factor_ = 1 / (log10(0.25f * (wave + 4) + 1.468f) * 6);
|
|
spawn_factor_interpolator_ = 0;
|
|
waiting_for_next_wave_ = false;
|
|
}
|
|
|
|
void Enemy::TakeDamage(EnemyUnit* target, int damage) {
|
|
assert(!target->marked_for_removal);
|
|
assert(target->hit_points > 0);
|
|
|
|
target->blast.SetVisible(true);
|
|
target->blast_animator.Play(Animator::kFrames, false);
|
|
|
|
target->hit_points -= damage;
|
|
if (target->hit_points <= 0) {
|
|
if (!waiting_for_next_wave_)
|
|
++num_enemies_killed_in_current_wave_;
|
|
|
|
target->sprite.SetVisible(false);
|
|
target->health_base.SetVisible(false);
|
|
target->health_bar.SetVisible(false);
|
|
target->score.SetVisible(true);
|
|
|
|
target->score_animator.Play(Animator::kTimer | Animator::kMovement, false);
|
|
target->movement_animator.Pause(Animator::kMovement);
|
|
|
|
target->explosion_.Play(false);
|
|
|
|
Engine& engine = Engine::Get();
|
|
Demo* game = static_cast<Demo*>(engine.GetGame());
|
|
game->AddScore(GetScore(target->enemy_type));
|
|
} else {
|
|
target->targetted_by_weapon_ = kDamageType_Invalid;
|
|
|
|
Vector2 s = target->sprite.GetScale() * Vector2(0.6f, 0.01f);
|
|
s.x *= (float)target->hit_points / (float)target->total_health;
|
|
float t = (s.x - target->health_bar.GetScale().x) / 2;
|
|
target->health_bar.SetScale(s);
|
|
target->health_bar.Translate({t, 0});
|
|
|
|
target->health_base.SetVisible(true);
|
|
target->health_bar.SetVisible(true);
|
|
|
|
target->health_animator.Stop(Animator::kTimer | Animator::kBlending);
|
|
target->health_animator.Play(Animator::kTimer, false);
|
|
}
|
|
}
|
|
|
|
void Enemy::SpawnNextEnemy() {
|
|
Engine& engine = Engine::Get();
|
|
Random& rnd = engine.GetRandomGenerator();
|
|
|
|
float factor = Lerp(1.0f, spawn_factor_, spawn_factor_interpolator_);
|
|
EnemyType enemy_type = kEnemyType_Invalid;
|
|
|
|
for (int i = 0; i < kEnemyType_Max; ++i) {
|
|
if (seconds_since_last_spawn_[i] >= seconds_to_next_spawn_[i]) {
|
|
if (seconds_to_next_spawn_[i] > 0)
|
|
enemy_type = (EnemyType)i;
|
|
|
|
seconds_since_last_spawn_[i] = 0;
|
|
seconds_to_next_spawn_[i] =
|
|
Lerp(kSpawnPeriod[i][0] * factor, kSpawnPeriod[i][1] * factor,
|
|
rnd.GetFloat());
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (enemy_type == kEnemyType_Invalid)
|
|
return;
|
|
|
|
DamageType damage_type = enemy_type == kEnemyType_Tank
|
|
? kDamageType_Any
|
|
: (DamageType)(rnd.Roll(2) - 1);
|
|
|
|
Vector2 s = engine.GetScreenSize();
|
|
int col;
|
|
col = rnd.Roll(4) - 1;
|
|
if (col == last_spawn_col_)
|
|
col = (col + 1) % 4;
|
|
last_spawn_col_ = col;
|
|
float x = (s.x / 4) / 2 + (s.x / 4) * col - s.x / 2;
|
|
Vector2 pos = {x, s.y / 2};
|
|
float speed =
|
|
enemy_type == kEnemyType_Tank ? 36.0f : (rnd.Roll(4) == 4 ? 6.0f : 10.0f);
|
|
|
|
Spawn(enemy_type, damage_type, pos, speed);
|
|
}
|
|
|
|
void Enemy::Spawn(EnemyType enemy_type,
|
|
DamageType damage_type,
|
|
const Vector2& pos,
|
|
float speed) {
|
|
assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max);
|
|
assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Max);
|
|
|
|
Engine& engine = Engine::Get();
|
|
Demo* game = static_cast<Demo*>(engine.GetGame());
|
|
|
|
auto& e = enemies_.emplace_back();
|
|
e.enemy_type = enemy_type;
|
|
e.damage_type = damage_type;
|
|
if (enemy_type == kEnemyType_Skull) {
|
|
e.total_health = e.hit_points = 1;
|
|
e.sprite.Create(skull_tex_, {10, 13}, 100, 100);
|
|
} else if (enemy_type == kEnemyType_Bug) {
|
|
e.total_health = e.hit_points = 2;
|
|
e.sprite.Create(bug_tex_, {10, 4});
|
|
} else { // kEnemyType_Tank
|
|
e.total_health = e.hit_points = 6;
|
|
e.sprite.Create(skull_tex_, {10, 13}, 100, 100);
|
|
}
|
|
e.sprite.AutoScale();
|
|
e.sprite.SetVisible(true);
|
|
Vector2 spawn_pos = pos + Vector2(0, e.sprite.GetScale().y / 2);
|
|
e.sprite.SetOffset(spawn_pos);
|
|
|
|
e.sprite.SetFrame(enemy_frame_start[enemy_type][damage_type]);
|
|
e.sprite_animator.SetFrames(enemy_frame_count[enemy_type][damage_type],
|
|
enemy_frame_speed);
|
|
|
|
e.sprite_animator.Attach(&e.sprite);
|
|
e.sprite_animator.Play(Animator::kFrames, true);
|
|
|
|
e.target.Create(target_tex_, {6, 2});
|
|
e.target.AutoScale();
|
|
e.target.SetOffset(spawn_pos);
|
|
|
|
e.blast.Create(blast_tex_, {6, 2});
|
|
e.blast.AutoScale();
|
|
e.blast.SetOffset(spawn_pos);
|
|
|
|
e.health_base.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f));
|
|
e.health_base.SetOffset(spawn_pos);
|
|
e.health_base.PlaceToBottomOf(e.sprite);
|
|
e.health_base.SetColor({0.5f, 0.5f, 0.5f, 1});
|
|
|
|
e.health_bar.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f));
|
|
e.health_bar.SetOffset(spawn_pos);
|
|
e.health_bar.PlaceToBottomOf(e.sprite);
|
|
e.health_bar.SetColor({0.161f, 0.89f, 0.322f, 1});
|
|
|
|
e.score.Create(score_tex_[e.enemy_type]);
|
|
e.score.AutoScale();
|
|
e.score.SetColor({1, 1, 1, 1});
|
|
e.score.SetOffset(spawn_pos);
|
|
|
|
e.target_animator.Attach(&e.target);
|
|
|
|
e.blast_animator.SetEndCallback(Animator::kFrames,
|
|
[&]() -> void { e.blast.SetVisible(false); });
|
|
if (damage_type == kDamageType_Green) {
|
|
e.blast.SetFrame(0);
|
|
e.blast_animator.SetFrames(6, 28);
|
|
} else {
|
|
e.blast.SetFrame(6);
|
|
e.blast_animator.SetFrames(6, 28);
|
|
}
|
|
e.blast_animator.Attach(&e.blast);
|
|
|
|
SetupFadeOutAnim(e.health_animator, 1);
|
|
e.health_animator.Attach(&e.health_base);
|
|
e.health_animator.Attach(&e.health_bar);
|
|
|
|
SetupFadeOutAnim(e.score_animator, 0.2f);
|
|
e.score_animator.SetMovement({0, engine.GetScreenSize().y / 2}, 2.0f);
|
|
e.score_animator.SetEndCallback(
|
|
Animator::kMovement, [&]() -> void { e.marked_for_removal = true; });
|
|
e.score_animator.Attach(&e.score);
|
|
|
|
float max_distance =
|
|
engine.GetScreenSize().y - game->GetPlayer().GetWeaponScale().y / 2;
|
|
|
|
e.movement_animator.SetMovement(
|
|
{0, -max_distance}, speed,
|
|
std::bind(Acceleration, std::placeholders::_1, -0.15f));
|
|
e.movement_animator.SetEndCallback(Animator::kMovement, [&]() -> void {
|
|
e.sprite.SetVisible(false);
|
|
e.target.SetVisible(false);
|
|
e.blast.SetVisible(false);
|
|
e.marked_for_removal = true;
|
|
});
|
|
e.movement_animator.Attach(&e.sprite);
|
|
e.movement_animator.Attach(&e.target);
|
|
e.movement_animator.Attach(&e.blast);
|
|
e.movement_animator.Attach(&e.health_base);
|
|
e.movement_animator.Attach(&e.health_bar);
|
|
e.movement_animator.Attach(&e.score);
|
|
e.movement_animator.Play(Animator::kMovement, false);
|
|
|
|
e.explosion_.SetSound(explosion_sound_);
|
|
e.explosion_.SetVariate(true);
|
|
e.explosion_.SetSimulateStereo(true);
|
|
}
|
|
|
|
Enemy::EnemyUnit* Enemy::GetTarget(DamageType damage_type) {
|
|
for (auto& e : enemies_) {
|
|
if (e.targetted_by_weapon_ == damage_type && e.hit_points > 0 &&
|
|
!e.marked_for_removal)
|
|
return &e;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int Enemy::GetScore(EnemyType enemy_type) {
|
|
assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max);
|
|
return enemy_scores[enemy_type];
|
|
}
|
|
|
|
std::unique_ptr<Image> Enemy::GetScoreImage(int score) {
|
|
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
|
|
|
|
std::string text = std::to_string(score);
|
|
int width, height;
|
|
font.CalculateBoundingBox(text.c_str(), width, height);
|
|
|
|
auto image = std::make_unique<Image>();
|
|
image->Create(width, height);
|
|
image->Clear({1, 1, 1, 0});
|
|
|
|
font.Print(0, 0, text.c_str(), image->GetBuffer(), image->GetWidth());
|
|
|
|
return image;
|
|
}
|
|
|
|
bool Enemy::CreateRenderResources() {
|
|
auto skull_image = std::make_unique<Image>();
|
|
if (!skull_image->Load("enemy_anims_01_frames_ok.png"))
|
|
return false;
|
|
auto bug_image = std::make_unique<Image>();
|
|
if (!bug_image->Load("enemy_anims_02_frames_ok.png"))
|
|
return false;
|
|
auto target_image = std::make_unique<Image>();
|
|
if (!target_image->Load("enemy_target_single_ok.png"))
|
|
return false;
|
|
auto blast_image = std::make_unique<Image>();
|
|
if (!blast_image->Load("enemy_anims_blast_ok.png"))
|
|
return false;
|
|
|
|
skull_image->Compress();
|
|
bug_image->Compress();
|
|
target_image->Compress();
|
|
blast_image->Compress();
|
|
|
|
skull_tex_->Update(std::move(skull_image));
|
|
bug_tex_->Update(std::move(bug_image));
|
|
target_tex_->Update(std::move(target_image));
|
|
blast_tex_->Update(std::move(blast_image));
|
|
|
|
for (int i = 0; i < kEnemyType_Max; ++i)
|
|
score_tex_[i]->Update(GetScoreImage(GetScore((EnemyType)i)));
|
|
|
|
return true;
|
|
}
|