mirror of https://github.com/auygun/kaliber.git
1221 lines
40 KiB
C++
1221 lines
40 KiB
C++
#include "demo/enemy.h"
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
#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/geometry.h"
|
|
#include "engine/sound.h"
|
|
|
|
#include "demo/demo.h"
|
|
|
|
using namespace std::string_literals;
|
|
|
|
using namespace base;
|
|
using namespace eng;
|
|
|
|
namespace {
|
|
|
|
constexpr int idle1_frame_start[][3] = {{0, 50, -1}, {23, 73, -1},
|
|
{-1, -1, 100}, {13, 33, -1},
|
|
{-1, -1, 0}, {-1, -1, -1}};
|
|
constexpr int idle2_frame_start[][3] = {{7, 57, -1},
|
|
{30, 80, -1},
|
|
{-1, -1, 107},
|
|
{-1, -1, -1},
|
|
{-1, -1, -1}};
|
|
|
|
constexpr int idle1_frame_count[][3] = {{7, 7, -1},
|
|
{7, 7, -1},
|
|
{-1, -1, 7},
|
|
{6, 6, -1},
|
|
{0, 0, 8}};
|
|
constexpr int idle2_frame_count[][3] = {{16, 16, -1},
|
|
{16, 16, -1},
|
|
{-1, -1, 16},
|
|
{-1, -1, -1},
|
|
{-1, -1, -1}};
|
|
|
|
constexpr int idle_frame_speed = 12;
|
|
|
|
constexpr int enemy_scores[] = {100, 150, 300, 250, 0, 500};
|
|
|
|
// Enemy units spawn speed.
|
|
constexpr float kSpawnPeriod[kEnemyType_Unit_Last + 1][2] = {{3, 6},
|
|
{20, 30},
|
|
{60, 80},
|
|
{70, 100}};
|
|
|
|
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);
|
|
}
|
|
|
|
float SnapSpawnPosX(int col) {
|
|
Vector2f s = eng::Engine::Get().GetScreenSize();
|
|
float offset = base::Lerp(s.x * -0.02f, s.x * 0.02f,
|
|
eng::Engine::Get().GetRandomGenerator().Rand());
|
|
return (s.x / 4) / 2 + (s.x / 4) * col - s.x / 2 + offset;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Enemy::Enemy() = default;
|
|
|
|
Enemy::~Enemy() = default;
|
|
|
|
bool Enemy::Initialize() {
|
|
boss_intro_sound_ = std::make_shared<Sound>();
|
|
if (!boss_intro_sound_->Load("boss_intro.mp3", false))
|
|
return false;
|
|
|
|
boss_explosion_sound_ = std::make_shared<Sound>();
|
|
if (!boss_explosion_sound_->Load("boss_explosion.mp3", false))
|
|
return false;
|
|
|
|
explosion_sound_ = std::make_shared<Sound>();
|
|
if (!explosion_sound_->Load("explosion.mp3", false))
|
|
return false;
|
|
|
|
stealth_sound_ = std::make_shared<Sound>();
|
|
if (!stealth_sound_->Load("stealth.mp3", false))
|
|
return false;
|
|
|
|
shield_on_sound_ = std::make_shared<Sound>();
|
|
if (!shield_on_sound_->Load("shield.mp3", false))
|
|
return false;
|
|
|
|
hit_sound_ = std::make_shared<Sound>();
|
|
if (!hit_sound_->Load("hit.mp3", false))
|
|
return false;
|
|
|
|
power_up_spawn_sound_ = std::make_shared<Sound>();
|
|
if (!power_up_spawn_sound_->Load("powerup-spawn.mp3", false))
|
|
return false;
|
|
|
|
power_up_pick_sound_ = std::make_shared<Sound>();
|
|
if (!power_up_pick_sound_->Load("powerup-pick.mp3", false))
|
|
return false;
|
|
|
|
if (!CreateRenderResources())
|
|
return false;
|
|
|
|
boss_.SetZOrder(10);
|
|
boss_animator_.Attach(&boss_);
|
|
|
|
boss_intro_.SetSound(boss_intro_sound_);
|
|
boss_intro_.SetVariate(false);
|
|
boss_intro_.SetSimulateStereo(false);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Enemy::Update(float delta_time) {
|
|
if (!progress_paused_) {
|
|
if (boss_fight_)
|
|
UpdateBoss(delta_time);
|
|
else
|
|
UpdateWave(delta_time);
|
|
}
|
|
|
|
Randomf& rnd = Engine::Get().GetRandomGenerator();
|
|
|
|
chromatic_aberration_offset_ += 0.8f * delta_time;
|
|
|
|
// Update enemy units.
|
|
for (auto it = enemies_.begin(); it != enemies_.end();) {
|
|
if (it->marked_for_removal) {
|
|
it = enemies_.erase(it);
|
|
continue;
|
|
}
|
|
|
|
if (it->chromatic_aberration_active_) {
|
|
it->sprite.SetCustomUniform(
|
|
"aberration_offset",
|
|
Lerp(0.0f, 0.015f, chromatic_aberration_offset_));
|
|
}
|
|
#if defined(LOAD_TEST)
|
|
else if (it->kill_timer <= 0 &&
|
|
it->movement_animator.GetTime(Animator::kMovement) > 0.7f) {
|
|
TakeDamage(&*it, 1);
|
|
}
|
|
#endif
|
|
|
|
if (it->kill_timer > 0) {
|
|
it->kill_timer -= delta_time;
|
|
if (it->kill_timer <= 0)
|
|
TakeDamage(&*it, 100);
|
|
} else if ((it->enemy_type == kEnemyType_LightSkull ||
|
|
it->enemy_type == kEnemyType_DarkSkull ||
|
|
it->enemy_type == kEnemyType_Tank) &&
|
|
it->sprite_animator.IsPlaying(Animator::kFrames) &&
|
|
!it->idle2_anim && rnd.Roll(200) == 1) {
|
|
// Play random idle animation.
|
|
it->idle2_anim = true;
|
|
it->sprite_animator.Stop(Animator::kFrames);
|
|
it->sprite.SetFrame(idle2_frame_start[it->enemy_type][it->damage_type]);
|
|
it->sprite_animator.SetFrames(
|
|
idle2_frame_count[it->enemy_type][it->damage_type], idle_frame_speed);
|
|
auto& e = *it;
|
|
it->sprite_animator.SetEndCallback(Animator::kFrames, [&]() -> void {
|
|
e.sprite_animator.Stop(Animator::kFrames);
|
|
e.sprite.SetFrame(idle1_frame_start[e.enemy_type][e.damage_type]);
|
|
e.sprite_animator.SetFrames(
|
|
idle1_frame_count[e.enemy_type][e.damage_type], idle_frame_speed);
|
|
e.sprite_animator.Play(Animator::kFrames, true);
|
|
});
|
|
it->sprite_animator.Play(Animator::kFrames, false);
|
|
}
|
|
it++;
|
|
}
|
|
|
|
#if defined(LOAD_TEST)
|
|
if (boss_fight_ && IsBossAlive() && boss_spawn_time_ > 40)
|
|
KillBoss();
|
|
#endif
|
|
}
|
|
|
|
void Enemy::Pause(bool pause) {
|
|
for (auto& e : enemies_) {
|
|
e.sprite_animator.PauseOrResumeAll(pause);
|
|
e.target_animator.PauseOrResumeAll(pause);
|
|
e.blast_animator.PauseOrResumeAll(pause);
|
|
e.shield_animator.PauseOrResumeAll(pause);
|
|
e.health_animator.PauseOrResumeAll(pause);
|
|
e.score_animator.PauseOrResumeAll(pause);
|
|
e.movement_animator.PauseOrResumeAll(pause);
|
|
}
|
|
boss_animator_.PauseOrResumeAll(pause);
|
|
}
|
|
|
|
bool Enemy::HasTarget(DamageType damage_type) {
|
|
DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
return GetTarget(damage_type) ? true : false;
|
|
}
|
|
|
|
Vector2f Enemy::GetTargetPos(DamageType damage_type) {
|
|
DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
if (target)
|
|
return target->sprite.GetPosition() -
|
|
Vector2f(0, target->sprite.GetSize().y / 2.5f);
|
|
return {0, 0};
|
|
}
|
|
|
|
void Enemy::SelectTarget(DamageType damage_type,
|
|
const Vector2f& origin,
|
|
const Vector2f& dir) {
|
|
DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
if (progress_paused_)
|
|
return;
|
|
|
|
std::vector<std::tuple<EnemyUnit*, float, float, Vector2f>> candidates;
|
|
|
|
for (auto& e : enemies_) {
|
|
if (e.hit_points <= 0 || e.marked_for_removal || e.stealth_active)
|
|
continue;
|
|
|
|
if (e.targetted_by_weapon_[damage_type]) {
|
|
e.targetted_by_weapon_[damage_type] = false;
|
|
e.target.SetVisible(false);
|
|
e.target_animator.Stop(Animator::kAllAnimations);
|
|
}
|
|
|
|
Vector2f weapon_enemy_dir = e.sprite.GetPosition() - origin;
|
|
float weapon_enemy_dist = weapon_enemy_dir.Length();
|
|
weapon_enemy_dir.Normalize();
|
|
float cos_theta = weapon_enemy_dir.DotProduct(dir);
|
|
|
|
candidates.push_back(
|
|
std::make_tuple(&e, cos_theta, weapon_enemy_dist, weapon_enemy_dir));
|
|
}
|
|
|
|
if (candidates.empty())
|
|
return;
|
|
|
|
auto all_candidates = candidates;
|
|
|
|
for (auto it = candidates.begin(); it != candidates.end();) {
|
|
auto [cand_enemy, cand_cos_theta, cand_dist, cand_dir] = *it;
|
|
|
|
auto oit = candidates.begin();
|
|
for (; oit != candidates.end(); ++oit) {
|
|
auto [other_enemy, other_cos_theta, other_dist, orther_dir] = *oit;
|
|
|
|
if (cand_enemy == other_enemy || cand_dist < other_dist)
|
|
continue;
|
|
|
|
// Remove obstructed units.
|
|
if (base::Intersection(other_enemy->sprite.GetPosition(),
|
|
other_enemy->sprite.GetSize(), origin, cand_dir)) {
|
|
break;
|
|
}
|
|
}
|
|
if (oit != candidates.end())
|
|
it = candidates.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
|
|
if (candidates.empty())
|
|
return;
|
|
|
|
// Sort by cos-theta.
|
|
std::sort(candidates.begin(), candidates.end(),
|
|
[](auto& a, auto& b) { return std::get<1>(a) > std::get<1>(b); });
|
|
|
|
constexpr float threshold = 0.95f;
|
|
|
|
EnemyUnit* best_enemy = nullptr;
|
|
for (auto& cand : candidates) {
|
|
auto [cand_enemy, cos_theta, cand_dist, cand_dir] = cand;
|
|
if ((cand_enemy->damage_type == damage_type ||
|
|
cand_enemy->damage_type == kDamageType_Any) &&
|
|
cos_theta > threshold) {
|
|
best_enemy = cand_enemy;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!best_enemy && std::get<1>(candidates[0]) > threshold)
|
|
best_enemy = std::get<0>(candidates[0]);
|
|
|
|
if (!best_enemy) {
|
|
// Sort by distance.
|
|
std::sort(all_candidates.begin(), all_candidates.end(),
|
|
[](auto& a, auto& b) { return std::get<2>(a) > std::get<2>(b); });
|
|
|
|
if (base::Intersection(std::get<0>(all_candidates[0])->sprite.GetPosition(),
|
|
std::get<0>(all_candidates[0])->sprite.GetSize(),
|
|
origin, dir))
|
|
best_enemy = std::get<0>(all_candidates[0]);
|
|
}
|
|
|
|
if (best_enemy) {
|
|
best_enemy->targetted_by_weapon_[damage_type] = true;
|
|
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) {
|
|
DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
if (target) {
|
|
target->targetted_by_weapon_[damage_type] = false;
|
|
target->target.SetVisible(false);
|
|
target->target_animator.Stop(Animator::kAllAnimations);
|
|
}
|
|
}
|
|
|
|
void Enemy::HitTarget(DamageType damage_type) {
|
|
DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any);
|
|
|
|
if (progress_paused_)
|
|
return;
|
|
|
|
EnemyUnit* target = GetTarget(damage_type);
|
|
|
|
if (!target)
|
|
return;
|
|
|
|
target->target.SetVisible(false);
|
|
target->target_animator.Stop(Animator::kAllAnimations);
|
|
target->targetted_by_weapon_[damage_type] = false;
|
|
|
|
if (target->damage_type != kDamageType_Any &&
|
|
target->damage_type != damage_type) {
|
|
// No shield until wave 8.
|
|
if (wave_ <= 8)
|
|
return;
|
|
|
|
if (!target->shield_active) {
|
|
target->shield_active = true;
|
|
target->shield.SetVisible(true);
|
|
|
|
// Play intro anim and start shield timer.
|
|
target->shield_animator.Stop(Animator::kAllAnimations);
|
|
target->shield.SetFrame(0);
|
|
target->shield_animator.SetFrames(4, 12);
|
|
target->shield_animator.SetTimer(1.0f);
|
|
target->shield_animator.Play(Animator::kFrames | Animator::kTimer, false);
|
|
target->shield_animator.SetEndCallback(
|
|
Animator::kFrames, [&, target]() -> void {
|
|
// Play loop anim.
|
|
target->shield.SetFrame(4);
|
|
target->shield_animator.SetFrames(4, 12);
|
|
target->shield_animator.Play(Animator::kFrames, true);
|
|
target->shield_animator.SetEndCallback(Animator::kFrames, nullptr);
|
|
});
|
|
target->shield_animator.SetEndCallback(
|
|
Animator::kTimer, [&, target]() -> void {
|
|
// Timeout. Remove shield if sill active.
|
|
if (target->shield_active) {
|
|
target->shield_active = false;
|
|
target->shield_animator.Stop(Animator::kFrames);
|
|
target->shield_animator.Play(Animator::kBlending, false);
|
|
}
|
|
});
|
|
|
|
target->shield_on.Play(false);
|
|
} else {
|
|
// Restart shield timer.
|
|
target->shield_animator.Stop(Animator::kTimer);
|
|
target->shield_animator.SetTimer(0.6f);
|
|
target->shield_animator.Play(Animator::kTimer, false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
TakeDamage(target, 1);
|
|
}
|
|
|
|
bool Enemy::IsBossAlive() const {
|
|
return boss_fight_ && boss_.GetFrame() < 9;
|
|
}
|
|
|
|
void Enemy::PauseProgress() {
|
|
DeselectTarget(kDamageType_Green);
|
|
DeselectTarget(kDamageType_Blue);
|
|
progress_paused_ = true;
|
|
}
|
|
|
|
void Enemy::ResumeProgress() {
|
|
progress_paused_ = false;
|
|
}
|
|
|
|
void Enemy::StopAllEnemyUnits(bool chromatic_aberration_effect) {
|
|
for (auto& e : enemies_) {
|
|
if (e.enemy_type > kEnemyType_Unit_Last || e.marked_for_removal ||
|
|
e.hit_points == 0)
|
|
continue;
|
|
|
|
if (chromatic_aberration_effect) {
|
|
e.sprite.SetCustomShader("chromatic_aberration");
|
|
e.chromatic_aberration_active_ = true;
|
|
}
|
|
|
|
e.movement_animator.Pause(Animator::kMovement);
|
|
e.freeze_ = true;
|
|
|
|
if (e.stealth_active) {
|
|
e.sprite_animator.Stop(Animator::kAllAnimations | Animator::kTimer);
|
|
e.sprite_animator.SetBlending({1, 1, 1, 1}, 0.5f);
|
|
e.sprite_animator.SetEndCallback(Animator::kBlending, nullptr);
|
|
e.sprite_animator.Play(Animator::kBlending, false);
|
|
e.sprite_animator.Play(Animator::kFrames, true);
|
|
}
|
|
}
|
|
|
|
if (chromatic_aberration_effect)
|
|
chromatic_aberration_offset_ = 0.0f;
|
|
}
|
|
|
|
void Enemy::KillAllEnemyUnits(bool randomize_order) {
|
|
Engine& engine = Engine::Get();
|
|
Demo* game = static_cast<Demo*>(engine.GetGame());
|
|
|
|
for (auto& e : enemies_) {
|
|
if (!e.marked_for_removal && e.hit_points > 0 &&
|
|
e.enemy_type <= kEnemyType_Unit_Last) {
|
|
if (randomize_order) {
|
|
e.kill_timer = Lerp(0.0f, engine.GetScreenSize().y * 0.5f * 0.15f,
|
|
engine.GetRandomGenerator().Rand());
|
|
} else {
|
|
float dist = e.sprite.GetPosition().y -
|
|
game->GetPlayer().GetWeaponPos(kDamageType_Green).y;
|
|
e.kill_timer = dist * 0.15f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enemy::RemoveAll() {
|
|
for (auto& e : enemies_) {
|
|
if (e.enemy_type == kEnemyType_Boss) {
|
|
e.marked_for_removal = true;
|
|
} else if (!e.marked_for_removal && e.hit_points > 0) {
|
|
e.sprite_animator.SetEndCallback(
|
|
Animator::kBlending, [&]() -> void { e.marked_for_removal = true; });
|
|
e.sprite_animator.SetBlending({1, 1, 1, 0}, 0.3f);
|
|
e.sprite_animator.Play(Animator::kBlending, false);
|
|
}
|
|
}
|
|
|
|
// Hide boss if not already hiding.
|
|
if (boss_.IsVisible() &&
|
|
!boss_animator_.IsPlaying(Animator::kTimer | Animator::kMovement)) {
|
|
boss_animator_.SetEndCallback(Animator::kMovement, [&]() -> void {
|
|
boss_animator_.SetVisible(false);
|
|
});
|
|
boss_animator_.SetMovement({0, boss_.GetSize().y * 0.99f}, 1);
|
|
boss_animator_.Play(Animator::kMovement, false);
|
|
}
|
|
}
|
|
|
|
void Enemy::KillBoss() {
|
|
for (auto& e : enemies_) {
|
|
if (e.enemy_type == kEnemyType_Boss) {
|
|
TakeDamage(&e, 1000);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enemy::Reset() {
|
|
seconds_since_last_power_up_ = 0;
|
|
seconds_to_next_power_up_ = 0;
|
|
}
|
|
|
|
void Enemy::OnWaveStarted(int wave, bool boss_fight) {
|
|
num_enemies_killed_in_current_wave_ = 0;
|
|
seconds_since_last_spawn_ = {0, 0, 0, 0};
|
|
seconds_to_next_spawn_ = {0, 0, 0, 0};
|
|
spawn_factor_ = 0.3077f - (0.0538f * log((float)wave));
|
|
last_spawn_col_ = 0;
|
|
progress_paused_ = false;
|
|
wave_ = wave;
|
|
boss_fight_ = boss_fight;
|
|
|
|
if (boss_fight) {
|
|
boss_spawn_time_ = 0;
|
|
boss_spawn_time_factor_ = [wave]() -> float {
|
|
if (wave <= 6)
|
|
return 0.4f;
|
|
if (wave <= 9)
|
|
return 0.6f;
|
|
if (wave <= 12)
|
|
return 1.0f;
|
|
return 1.6f;
|
|
}();
|
|
DLOG << "boss_spawn_time_factor_: " << boss_spawn_time_factor_;
|
|
SpawnBoss();
|
|
}
|
|
}
|
|
|
|
bool Enemy::CheckSpawnPos(Vector2f pos, SpeedType speed_type) {
|
|
for (auto& e : enemies_) {
|
|
if (e.hit_points <= 0 || e.marked_for_removal || e.stealth_active ||
|
|
e.enemy_type > kEnemyType_Unit_Last || e.speed_type != speed_type)
|
|
continue;
|
|
|
|
// Check for collision.
|
|
float sy = e.sprite.GetSize().y;
|
|
Vector2f spawn_pos = pos + Vector2f(0, sy);
|
|
|
|
bool gc = (spawn_pos - e.sprite.GetPosition()).Length() < sy * 0.8f;
|
|
bool tc = e.movement_animator.GetTime(Animator::kMovement) <= 0.06f;
|
|
|
|
if (gc && tc)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Enemy::CheckTeleportPos(EnemyUnit* enemy) {
|
|
Vector2f pos = enemy->sprite.GetPosition();
|
|
float t = enemy->movement_animator.GetTime(Animator::kMovement);
|
|
|
|
for (auto& e : enemies_) {
|
|
if (&e == enemy || e.hit_points <= 0 || e.marked_for_removal ||
|
|
e.stealth_active || e.enemy_type > kEnemyType_Unit_Last ||
|
|
e.speed_type != enemy->speed_type)
|
|
continue;
|
|
|
|
if (e.enemy_type == kEnemyType_Bug &&
|
|
!e.movement_animator.IsPlaying(Animator::kMovement))
|
|
continue;
|
|
|
|
bool gc =
|
|
(pos - e.sprite.GetPosition()).Length() < e.sprite.GetSize().y * 0.8f;
|
|
bool tc =
|
|
fabs(t - e.movement_animator.GetTime(Animator::kMovement)) <= 0.04f;
|
|
|
|
if (gc && tc)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Enemy::SpawnUnit(EnemyType enemy_type,
|
|
DamageType damage_type,
|
|
const Vector2f& pos,
|
|
float speed,
|
|
SpeedType speed_type) {
|
|
DCHECK(
|
|
(enemy_type > kEnemyType_Invalid && enemy_type <= kEnemyType_Unit_Last) ||
|
|
enemy_type == kEnemyType_PowerUp);
|
|
DCHECK(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;
|
|
e.speed_type = speed_type;
|
|
|
|
switch (enemy_type) {
|
|
case kEnemyType_LightSkull:
|
|
e.total_health = e.hit_points = 1;
|
|
e.sprite.Create("skull_tex", {10, 13}, 100, 100);
|
|
break;
|
|
case kEnemyType_DarkSkull:
|
|
e.total_health = e.hit_points = 2;
|
|
e.sprite.Create("skull_tex", {10, 13}, 100, 100);
|
|
break;
|
|
case kEnemyType_Tank:
|
|
e.total_health = e.hit_points = 5;
|
|
e.sprite.Create("skull_tex", {10, 13}, 100, 100);
|
|
break;
|
|
case kEnemyType_Bug:
|
|
e.total_health = e.hit_points = 3;
|
|
e.sprite.Create("bug_tex", {10, 4});
|
|
break;
|
|
case kEnemyType_PowerUp:
|
|
e.total_health = e.hit_points = 1;
|
|
e.sprite.Create("crate_tex", {8, 3});
|
|
break;
|
|
default:
|
|
NOTREACHED << "- Unkown enemy type: " << enemy_type;
|
|
}
|
|
|
|
e.sprite.SetZOrder(11);
|
|
e.sprite.SetVisible(true);
|
|
Vector2f spawn_pos = pos + Vector2f(0, e.sprite.GetSize().y / 2);
|
|
e.sprite.SetPosition(spawn_pos);
|
|
|
|
e.sprite.SetFrame(idle1_frame_start[enemy_type][damage_type]);
|
|
e.sprite_animator.SetFrames(idle1_frame_count[enemy_type][damage_type],
|
|
idle_frame_speed);
|
|
|
|
e.sprite.SetColor({1, 1, 1, 0});
|
|
e.sprite_animator.SetBlending({1, 1, 1, 1}, 0.3f);
|
|
|
|
e.sprite_animator.Attach(&e.sprite);
|
|
e.sprite_animator.Play(Animator::kBlending, false);
|
|
e.sprite_animator.Play(Animator::kFrames, true);
|
|
|
|
e.target.Create("target_tex", {6, 2});
|
|
e.target.SetZOrder(12);
|
|
e.target.SetPosition(spawn_pos);
|
|
|
|
if (enemy_type == kEnemyType_PowerUp)
|
|
e.blast.Create("crate_tex", {8, 3});
|
|
else
|
|
e.blast.Create("blast_tex", {6, 2});
|
|
e.blast.SetZOrder(12);
|
|
e.blast.SetPosition(spawn_pos);
|
|
|
|
e.shield.Create("shield_tex", {4, 2});
|
|
e.shield.SetZOrder(11);
|
|
e.shield.SetPosition(spawn_pos);
|
|
|
|
e.health_base.SetZOrder(11);
|
|
e.health_base.SetSize(e.sprite.GetSize() * Vector2f(0.6f, 0.01f));
|
|
e.health_base.SetPosition(spawn_pos);
|
|
e.health_base.PlaceToBottomOf(e.sprite);
|
|
e.health_base.SetColor({0.5f, 0.5f, 0.5f, 1});
|
|
|
|
e.health_bar.SetZOrder(11);
|
|
e.health_bar.SetSize(e.sprite.GetSize() * Vector2f(0.6f, 0.01f));
|
|
e.health_bar.SetPosition(spawn_pos);
|
|
e.health_bar.PlaceToBottomOf(e.sprite);
|
|
e.health_bar.SetColor({0.161f, 0.89f, 0.322f, 1});
|
|
|
|
e.score.Create("score_tex"s + std::to_string(e.enemy_type));
|
|
e.score.SetZOrder(12);
|
|
e.score.Scale(1.1f);
|
|
e.score.SetColor({1, 1, 1, 1});
|
|
e.score.SetPosition(spawn_pos);
|
|
|
|
e.target_animator.Attach(&e.target);
|
|
|
|
e.blast_animator.SetEndCallback(Animator::kFrames, [&]() -> void {
|
|
e.blast.SetVisible(false);
|
|
if (e.enemy_type == kEnemyType_PowerUp)
|
|
e.marked_for_removal = true;
|
|
});
|
|
if (enemy_type == kEnemyType_PowerUp) {
|
|
e.blast.SetFrame(8);
|
|
e.blast_animator.SetFrames(13, 18);
|
|
} else if (damage_type == kDamageType_Green) {
|
|
e.blast.SetFrame(0);
|
|
e.blast_animator.SetFrames(6, 18);
|
|
} else {
|
|
e.blast.SetFrame(6);
|
|
e.blast_animator.SetFrames(6, 18);
|
|
}
|
|
e.blast_animator.Attach(&e.blast);
|
|
|
|
e.shield_animator.Attach(&e.shield);
|
|
e.shield_animator.SetBlending({1, 1, 1, 0}, 0.15f, nullptr);
|
|
e.shield_animator.SetEndCallback(Animator::kBlending, [&]() -> void {
|
|
e.shield_animator.Stop(Animator::kAllAnimations | Animator::kTimer);
|
|
e.shield.SetVisible(false);
|
|
});
|
|
|
|
SetupFadeOutAnim(e.health_animator, 1);
|
|
e.health_animator.Attach(&e.health_base);
|
|
e.health_animator.Attach(&e.health_bar);
|
|
|
|
SetupFadeOutAnim(e.score_animator, 0.5f);
|
|
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 =
|
|
spawn_pos.y - game->GetPlayer().GetWeaponPos(kDamageType_Green).y;
|
|
if (enemy_type == kEnemyType_PowerUp)
|
|
max_distance /= 2;
|
|
|
|
Animator::Interpolator interpolator;
|
|
if (boss_fight_)
|
|
interpolator = std::bind(CatmullRom, std::placeholders::_1, 2.5f, 1.5f);
|
|
else if (enemy_type == kEnemyType_PowerUp)
|
|
interpolator = std::bind(CatmullRom, std::placeholders::_1, -9.0, 1.35f);
|
|
else
|
|
interpolator = std::bind(Acceleration, std::placeholders::_1, -0.15f);
|
|
e.movement_animator.SetMovement({0, -max_distance}, speed, interpolator);
|
|
e.movement_animator.SetEndCallback(Animator::kMovement, [&]() -> void {
|
|
// Enemy has reached the player.
|
|
e.hit_points = 0;
|
|
e.target.SetVisible(false);
|
|
e.blast.SetVisible(false);
|
|
if (e.enemy_type == kEnemyType_PowerUp)
|
|
seconds_to_next_power_up_ *= 0.5f;
|
|
else
|
|
static_cast<Demo*>(engine.GetGame())->GetPlayer().TakeDamage(1);
|
|
e.sprite_animator.SetEndCallback(
|
|
Animator::kBlending, [&]() -> void { e.marked_for_removal = true; });
|
|
e.sprite_animator.SetBlending({1, 1, 1, 0}, 0.3f);
|
|
e.sprite_animator.Play(Animator::kBlending, false);
|
|
});
|
|
e.movement_animator.Attach(&e.sprite);
|
|
e.movement_animator.Attach(&e.target);
|
|
e.movement_animator.Attach(&e.blast);
|
|
e.movement_animator.Attach(&e.shield);
|
|
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);
|
|
|
|
if (e.enemy_type == kEnemyType_PowerUp) {
|
|
e.explosion.SetSound(power_up_pick_sound_);
|
|
|
|
e.spawn.SetSound(power_up_spawn_sound_);
|
|
e.spawn.SetMaxAplitude(2.0f);
|
|
e.spawn.Play(false);
|
|
} else {
|
|
e.explosion.SetSound(explosion_sound_);
|
|
e.explosion.SetVariate(true);
|
|
e.explosion.SetSimulateStereo(true);
|
|
e.explosion.SetMaxAplitude(0.9f);
|
|
}
|
|
|
|
e.stealth.SetSound(stealth_sound_);
|
|
e.stealth.SetVariate(false);
|
|
e.stealth.SetSimulateStereo(false);
|
|
e.stealth.SetMaxAplitude(0.7f);
|
|
|
|
e.shield_on.SetSound(shield_on_sound_);
|
|
e.shield_on.SetVariate(false);
|
|
e.shield_on.SetSimulateStereo(false);
|
|
e.shield_on.SetMaxAplitude(0.5f);
|
|
|
|
e.hit.SetSound(hit_sound_);
|
|
e.hit.SetVariate(true);
|
|
e.hit.SetSimulateStereo(false);
|
|
e.hit.SetMaxAplitude(0.5f);
|
|
}
|
|
|
|
void Enemy::SpawnBoss() {
|
|
// Setup visual sprite of the boss.
|
|
int boss_id = (wave_ / 3) % 3;
|
|
if (boss_id == 0)
|
|
boss_id = 3;
|
|
boss_.Create("boss_tex"s + std::to_string(boss_id), {4, 3});
|
|
boss_.SetVisible(true);
|
|
boss_.SetPosition(Engine::Get().GetScreenSize() * Vector2f(0, 0.5f) +
|
|
boss_.GetSize() * Vector2f(0, 2.0f));
|
|
boss_animator_.SetMovement(
|
|
{0, boss_.GetSize().y * -2.4f}, 5,
|
|
std::bind(Acceleration, std::placeholders::_1, -1));
|
|
boss_.SetFrame(0);
|
|
boss_animator_.SetFrames(8, 16);
|
|
|
|
boss_animator_.SetEndCallback(Animator::kMovement, [&]() -> void {
|
|
Engine& engine = Engine::Get();
|
|
Demo* game = static_cast<Demo*>(engine.GetGame());
|
|
|
|
// Spwawn a stationary enemy unit for the boss.
|
|
auto& e = enemies_.emplace_front();
|
|
e.enemy_type = kEnemyType_Boss;
|
|
e.damage_type = kDamageType_Any;
|
|
e.total_health = e.hit_points = 41.1283f * log((float)game->wave()) - 20.0f;
|
|
DLOG << " Boss health: " << e.total_health;
|
|
|
|
Vector2f hit_box_pos =
|
|
boss_.GetPosition() - boss_.GetSize() * Vector2f(0, 0.2f);
|
|
|
|
// Just a hit box, no visual sprite.
|
|
e.sprite.SetPosition(hit_box_pos);
|
|
e.sprite.SetSize(boss_.GetSize() * 0.3f);
|
|
|
|
e.target.Create("target_tex", {6, 2});
|
|
e.target.SetZOrder(12);
|
|
e.target.SetPosition(hit_box_pos);
|
|
|
|
Vector2f health_bar_offset = boss_.GetSize() * Vector2f(0, 0.2f);
|
|
|
|
// A thicker and always visible health bar.
|
|
e.health_base.SetZOrder(10);
|
|
e.health_base.SetSize(e.sprite.GetSize() * Vector2f(0.7f, 0.08f));
|
|
e.health_base.SetPosition(hit_box_pos + health_bar_offset);
|
|
e.health_base.PlaceToBottomOf(boss_);
|
|
e.health_base.SetColor({0.5f, 0.5f, 0.5f, 1});
|
|
e.health_base.SetVisible(true);
|
|
|
|
e.health_bar.SetZOrder(10);
|
|
e.health_bar.SetSize(e.sprite.GetSize() * Vector2f(0.7f, 0.08f));
|
|
e.health_bar.SetPosition(hit_box_pos + health_bar_offset);
|
|
e.health_bar.PlaceToBottomOf(boss_);
|
|
e.health_bar.SetColor({0.161f, 0.89f, 0.322f, 1});
|
|
e.health_bar.SetVisible(true);
|
|
|
|
e.score.Create("score_tex"s + std::to_string(e.enemy_type));
|
|
e.score.SetZOrder(12);
|
|
e.score.SetColor({1, 1, 1, 1});
|
|
e.score.SetPosition(hit_box_pos);
|
|
|
|
e.target_animator.Attach(&e.target);
|
|
|
|
SetupFadeOutAnim(e.score_animator, 0.5f);
|
|
e.score_animator.SetMovement({0, Engine::Get().GetScreenSize().y / 2},
|
|
2.0f);
|
|
e.score_animator.SetEndCallback(
|
|
Animator::kMovement, [&]() -> void { e.marked_for_removal = true; });
|
|
e.score_animator.Attach(&e.score);
|
|
|
|
e.explosion.SetSound(boss_explosion_sound_);
|
|
e.explosion.SetVariate(false);
|
|
e.explosion.SetSimulateStereo(false);
|
|
|
|
e.hit.SetSound(hit_sound_);
|
|
e.hit.SetVariate(true);
|
|
e.hit.SetSimulateStereo(false);
|
|
e.hit.SetMaxAplitude(0.5f);
|
|
});
|
|
boss_animator_.Play(Animator::kFrames, true);
|
|
boss_animator_.Play(Animator::kMovement, false);
|
|
|
|
boss_intro_.Play(false);
|
|
}
|
|
|
|
void Enemy::TakeDamage(EnemyUnit* target, int damage) {
|
|
DCHECK(!target->marked_for_removal);
|
|
|
|
if (target->hit_points <= 0)
|
|
return;
|
|
|
|
Engine::Get().Vibrate(30);
|
|
|
|
if (target->shield_active) {
|
|
// Remove shield.
|
|
target->shield_active = false;
|
|
target->shield_animator.Stop(Animator::kFrames | Animator::kTimer);
|
|
target->shield_animator.Play(Animator::kBlending, false);
|
|
|
|
if (--damage == 0)
|
|
return;
|
|
}
|
|
|
|
target->hit_points -= damage;
|
|
|
|
target->blast.SetVisible(true);
|
|
target->blast_animator.Play(Animator::kFrames, false);
|
|
|
|
if (target->hit_points <= 0) {
|
|
Engine& engine = Engine::Get();
|
|
Demo* game = static_cast<Demo*>(engine.GetGame());
|
|
|
|
if (target->enemy_type != kEnemyType_PowerUp)
|
|
++num_enemies_killed_in_current_wave_;
|
|
|
|
target->sprite.SetVisible(false);
|
|
target->health_base.SetVisible(false);
|
|
target->health_bar.SetVisible(false);
|
|
target->target.SetVisible(false);
|
|
|
|
target->spawn.Stop(0.5f);
|
|
|
|
if (target->enemy_type == kEnemyType_PowerUp) {
|
|
// Move power-up sprite towards player.
|
|
float distance = target->sprite.GetPosition().y -
|
|
game->GetPlayer().GetWeaponPos(kDamageType_Green).y;
|
|
target->movement_animator.SetMovement(
|
|
{0, -distance}, 0.7f,
|
|
std::bind(Acceleration, std::placeholders::_1, 1));
|
|
target->movement_animator.Stop(Animator::kMovement);
|
|
target->movement_animator.SetEndCallback(Animator::kMovement, nullptr);
|
|
target->movement_animator.Play(Animator::kMovement, false);
|
|
} else {
|
|
// Stop enemy sprite and play score animation.
|
|
target->score.SetVisible(true);
|
|
target->score_animator.Play(Animator::kTimer | Animator::kMovement,
|
|
false);
|
|
target->movement_animator.Pause(Animator::kMovement);
|
|
}
|
|
|
|
int score = GetScore(target->enemy_type);
|
|
if (score)
|
|
game->AddScore(score);
|
|
|
|
target->explosion.Play(false);
|
|
|
|
if (target->enemy_type == kEnemyType_PowerUp) {
|
|
if (damage == 1)
|
|
game->GetPlayer().AddNuke(1);
|
|
} else if (target->enemy_type == kEnemyType_Boss) {
|
|
// Play dead animation and move away the boss.
|
|
boss_animator_.Stop(Animator::kFrames | Animator::kTimer);
|
|
boss_animator_.SetEndCallback(Animator::kMovement, [&]() -> void {
|
|
boss_animator_.SetVisible(false);
|
|
});
|
|
boss_animator_.SetMovement({0, boss_.GetSize().y * 0.99f}, 4);
|
|
boss_.SetFrame(9);
|
|
boss_animator_.SetFrames(2, 12);
|
|
boss_animator_.SetEndCallback(Animator::kTimer, [&]() -> void {
|
|
boss_animator_.Stop(Animator::kFrames);
|
|
boss_animator_.Play(Animator::kMovement, false);
|
|
boss_.SetFrame(11);
|
|
});
|
|
boss_animator_.SetTimer(1.25f);
|
|
boss_animator_.Play(Animator::kFrames, true);
|
|
boss_animator_.Play(Animator::kTimer, false);
|
|
}
|
|
} else {
|
|
Vector2f s = target->health_base.GetSize();
|
|
s.x *= (float)target->hit_points / (float)target->total_health;
|
|
float t = (s.x - target->health_bar.GetSize().x) / 2;
|
|
target->health_bar.SetSize(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);
|
|
|
|
if (target->enemy_type == kEnemyType_Bug &&
|
|
target->hit_points == target->total_health - 1) {
|
|
// Stealth and teleport.
|
|
target->stealth_active = true;
|
|
target->movement_animator.Pause(Animator::kMovement);
|
|
target->sprite_animator.Pause(Animator::kFrames);
|
|
|
|
Randomf& rnd = Engine::Get().GetRandomGenerator();
|
|
float stealth_timer = Lerp(2.0f, 5.0f, rnd.Rand());
|
|
target->sprite_animator.SetEndCallback(
|
|
Animator::kTimer, [&, target]() -> void {
|
|
// No horizontal teleport in boss fight.
|
|
if (!boss_fight_) {
|
|
float x = SnapSpawnPosX(rnd.Roll(4) - 1);
|
|
TranslateEnemyUnit(*target,
|
|
{x - target->sprite.GetPosition().x, 0});
|
|
}
|
|
|
|
// Vertical teleport (wave 6+).
|
|
float ct = target->movement_animator.GetTime(Animator::kMovement);
|
|
if (wave_ >= 6 && ct < 0.6f) {
|
|
float t = Lerp(0.0f, 0.5f, rnd.Rand());
|
|
float nt = std::min(ct + t, 0.6f);
|
|
target->movement_animator.SetTime(Animator::kMovement, nt, true);
|
|
}
|
|
|
|
target->stealth_active = false;
|
|
target->sprite_animator.SetBlending({1, 1, 1, 1}, 1.0f);
|
|
target->sprite_animator.Play(Animator::kBlending, false);
|
|
target->sprite_animator.SetEndCallback(
|
|
Animator::kBlending, [&]() -> void {
|
|
if (target->freeze_) {
|
|
target->sprite_animator.Play(Animator::kFrames, true);
|
|
} else if (CheckTeleportPos(target)) {
|
|
target->movement_animator.Play(Animator::kMovement, false);
|
|
target->sprite_animator.Play(Animator::kFrames, true);
|
|
} else {
|
|
// Try again soon.
|
|
target->sprite_animator.SetBlending({1, 1, 1, 1}, 0.001f);
|
|
target->sprite_animator.Play(Animator::kBlending, false);
|
|
}
|
|
});
|
|
});
|
|
|
|
target->sprite_animator.SetTimer(stealth_timer);
|
|
target->sprite_animator.SetBlending({1, 1, 1, 0}, 1.5f);
|
|
target->sprite_animator.Play(Animator::kBlending | Animator::kTimer,
|
|
false);
|
|
|
|
target->stealth.Play(false);
|
|
} else {
|
|
target->hit.Play(false);
|
|
}
|
|
|
|
if (target->enemy_type == kEnemyType_Boss) {
|
|
// Play damage animation.
|
|
boss_animator_.Stop(Animator::kFrames | Animator::kTimer);
|
|
boss_.SetFrame(8);
|
|
boss_animator_.SetFrames(1, 1);
|
|
boss_animator_.SetEndCallback(Animator::kTimer, [&]() -> void {
|
|
boss_animator_.Stop(Animator::kFrames);
|
|
boss_.SetFrame(0);
|
|
boss_animator_.SetFrames(8, 12);
|
|
boss_animator_.Play(Animator::kFrames, true);
|
|
});
|
|
boss_animator_.SetTimer(0.2f);
|
|
boss_animator_.Play(Animator::kFrames, true);
|
|
boss_animator_.Play(Animator::kTimer, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enemy::UpdateWave(float delta_time) {
|
|
for (int i = 0; i < kEnemyType_Unit_Last + 1; ++i)
|
|
seconds_since_last_spawn_[i] += delta_time;
|
|
|
|
Engine& engine = Engine::Get();
|
|
Randomf& rnd = engine.GetRandomGenerator();
|
|
|
|
EnemyType enemy_type = kEnemyType_Invalid;
|
|
|
|
for (int i = 0; i < kEnemyType_Unit_Last + 1; ++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] * spawn_factor_,
|
|
kSpawnPeriod[i][1] * spawn_factor_, rnd.Rand());
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (enemy_type != kEnemyType_Invalid) {
|
|
// Spawn only light enemies during the first 4 waves. Then gradually
|
|
// introduce harder enemy types.
|
|
if (enemy_type != kEnemyType_LightSkull && wave_ <= 3)
|
|
enemy_type = wave_ == 1 ? kEnemyType_Invalid : kEnemyType_LightSkull;
|
|
else if (enemy_type > kEnemyType_DarkSkull && wave_ == 4)
|
|
enemy_type = kEnemyType_LightSkull;
|
|
else if (enemy_type == kEnemyType_Tank && wave_ <= 6)
|
|
enemy_type = kEnemyType_LightSkull;
|
|
}
|
|
|
|
if (enemy_type != kEnemyType_Invalid) {
|
|
DamageType damage_type = enemy_type == kEnemyType_Tank
|
|
? kDamageType_Any
|
|
: (DamageType)(rnd.Roll(2) - 1);
|
|
|
|
int col = rnd.Roll(4) - 1;
|
|
if (col == last_spawn_col_)
|
|
col = (col + 1) % 4;
|
|
last_spawn_col_ = col;
|
|
|
|
Vector2f s = engine.GetScreenSize();
|
|
float x = SnapSpawnPosX(col);
|
|
Vector2f pos = {x, s.y / 2};
|
|
|
|
SpeedType speed_type =
|
|
enemy_type == kEnemyType_Tank
|
|
? kSpeedType_Slow
|
|
: (rnd.Roll(3) == 1 ? kSpeedType_Fast : kSpeedType_Slow);
|
|
float speed = speed_type == kSpeedType_Slow ? 10.0f : 6.0f;
|
|
|
|
if (CheckSpawnPos(pos, speed_type))
|
|
SpawnUnit(enemy_type, damage_type, pos, speed, speed_type);
|
|
else
|
|
seconds_to_next_spawn_[enemy_type] = 0.001f;
|
|
}
|
|
|
|
seconds_since_last_power_up_ += delta_time;
|
|
if (seconds_since_last_power_up_ >= seconds_to_next_power_up_) {
|
|
if (seconds_to_next_power_up_ > 0 &&
|
|
static_cast<Demo*>(engine.GetGame())->GetPlayer().nuke_count() < 3) {
|
|
Vector2f s = engine.GetScreenSize();
|
|
Vector2f pos = {0, s.y / 2};
|
|
SpawnUnit(kEnemyType_PowerUp, kDamageType_Any, pos, 6);
|
|
}
|
|
seconds_since_last_power_up_ = 0;
|
|
seconds_to_next_power_up_ = Lerp(60.0f, 80.0f, rnd.Rand());
|
|
}
|
|
}
|
|
|
|
void Enemy::UpdateBoss(float delta_time) {
|
|
if (boss_animator_.IsPlaying(Animator::kMovement) &&
|
|
boss_animator_.GetTime(Animator::kMovement) < 0.5f)
|
|
return;
|
|
|
|
for (int i = 0; i < kEnemyType_Unit_Last + 1; ++i)
|
|
seconds_since_last_spawn_[i] += delta_time;
|
|
|
|
Randomf& rnd = Engine::Get().GetRandomGenerator();
|
|
|
|
boss_spawn_time_ += delta_time;
|
|
float boss_spawn_factor =
|
|
0.4f - (0.0684f * log(boss_spawn_time_ * boss_spawn_time_factor_));
|
|
if (boss_spawn_factor < 0.1f)
|
|
boss_spawn_factor = 0.1f;
|
|
|
|
EnemyType enemy_type = kEnemyType_Invalid;
|
|
|
|
for (int i = 0; i < kEnemyType_Unit_Last + 1; ++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] * boss_spawn_factor,
|
|
kSpawnPeriod[i][1] * boss_spawn_factor, rnd.Rand());
|
|
break;
|
|
} else if (seconds_to_next_spawn_[i] >
|
|
kSpawnPeriod[i][1] * boss_spawn_factor) {
|
|
seconds_to_next_spawn_[i] =
|
|
Lerp(kSpawnPeriod[i][0] * boss_spawn_factor,
|
|
kSpawnPeriod[i][1] * boss_spawn_factor, rnd.Rand());
|
|
}
|
|
}
|
|
|
|
if (enemy_type != kEnemyType_Invalid && boss_spawn_factor > 0.11f) {
|
|
// Spawn only light enemies during the first boss fight. Then gradually
|
|
// introduce harder enemy types.
|
|
if (enemy_type != kEnemyType_LightSkull && wave_ == 3)
|
|
enemy_type = enemy_type == kEnemyType_DarkSkull ? kEnemyType_LightSkull
|
|
: kEnemyType_Invalid;
|
|
else if (enemy_type == kEnemyType_Tank && wave_ == 6)
|
|
enemy_type = kEnemyType_Invalid;
|
|
}
|
|
|
|
if (enemy_type == kEnemyType_Invalid)
|
|
return;
|
|
|
|
DamageType damage_type = enemy_type == kEnemyType_Tank
|
|
? kDamageType_Any
|
|
: (DamageType)(rnd.Roll(2) - 1);
|
|
|
|
int col = (last_spawn_col_++) % 2;
|
|
float offset =
|
|
Lerp(boss_.GetSize().x * -0.12f, boss_.GetSize().x * 0.12f, rnd.Rand());
|
|
float x = (boss_.GetSize().x / 3) * (col ? 1 : -1) + offset;
|
|
Vector2f pos = {x, boss_.GetPosition().y - boss_.GetSize().y / 2};
|
|
|
|
SpeedType speed_type =
|
|
enemy_type == kEnemyType_Tank
|
|
? kSpeedType_Slow
|
|
: (rnd.Roll(3) == 1 ? kSpeedType_Fast : kSpeedType_Slow);
|
|
float speed = speed_type == kSpeedType_Slow ? 10.0f : 6.0f;
|
|
|
|
if (CheckSpawnPos(pos, speed_type))
|
|
SpawnUnit(enemy_type, damage_type, pos, speed, speed_type);
|
|
else
|
|
seconds_to_next_spawn_[enemy_type] = 0.001f;
|
|
}
|
|
|
|
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) {
|
|
DCHECK(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max);
|
|
return enemy_scores[enemy_type];
|
|
}
|
|
|
|
std::unique_ptr<Image> Enemy::GetScoreImage(EnemyType enemy_type) {
|
|
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
|
|
|
|
int score = GetScore(enemy_type);
|
|
if (!score)
|
|
return nullptr;
|
|
|
|
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());
|
|
|
|
image->Compress();
|
|
return image;
|
|
}
|
|
|
|
bool Enemy::CreateRenderResources() {
|
|
Engine::Get().SetImageSource("skull_tex", "enemy_anims_01_frames_ok.png",
|
|
true);
|
|
Engine::Get().SetImageSource("bug_tex", "enemy_anims_02_frames_ok.png", true);
|
|
Engine::Get().SetImageSource("boss_tex1", "Boss_ok.png", true);
|
|
Engine::Get().SetImageSource("boss_tex2", "Boss_ok_lvl2.png", true);
|
|
Engine::Get().SetImageSource("boss_tex3", "Boss_ok_lvl3.png", true);
|
|
Engine::Get().SetImageSource("target_tex", "enemy_target_single_ok.png",
|
|
true);
|
|
Engine::Get().SetImageSource("blast_tex", "enemy_anims_blast_ok.png", true);
|
|
Engine::Get().SetImageSource("shield_tex", "woom_enemy_shield.png", true);
|
|
Engine::Get().SetImageSource("crate_tex", "nuke_pack_OK.png", true);
|
|
|
|
for (int i = 0; i < kEnemyType_Max; ++i)
|
|
Engine::Get().SetImageSource(
|
|
"score_tex"s + std::to_string(i),
|
|
std::bind(&Enemy::GetScoreImage, this, (EnemyType)i), true);
|
|
|
|
Engine::Get().LoadCustomShader("chromatic_aberration",
|
|
"chromatic_aberration.glsl");
|
|
|
|
return true;
|
|
}
|
|
|
|
void Enemy::TranslateEnemyUnit(EnemyUnit& e, const Vector2f& delta) {
|
|
e.sprite.Translate(delta);
|
|
e.target.Translate(delta);
|
|
e.blast.Translate(delta);
|
|
e.shield.Translate(delta);
|
|
e.health_base.Translate(delta);
|
|
e.health_bar.Translate(delta);
|
|
e.score.Translate(delta);
|
|
}
|