#include "demo/enemy.h" #include #include #include #include #include #include "base/collusion_test.h" #include "base/interpolation.h" #include "base/log.h" #include "engine/asset/font.h" #include "engine/asset/image.h" #include "engine/engine.h" #include "engine/renderer/geometry.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::PreInitialize() { 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) { if (i == kEnemyType_PowerUp) continue; Engine::Get().SetImageSource( "score_tex"s + std::to_string(i), std::bind(&Enemy::GetScoreImage, this, (EnemyType)i), true); } Engine::Get().SetShaderSource("chromatic_aberration", "chromatic_aberration.glsl"); Engine::Get().AsyncLoadSound("boss_intro", "boss_intro.mp3"); Engine::Get().AsyncLoadSound("boss_explosion", "boss_explosion.mp3"); Engine::Get().AsyncLoadSound("explosion", "explosion.mp3"); Engine::Get().AsyncLoadSound("stealth", "stealth.mp3"); Engine::Get().AsyncLoadSound("shield", "shield.mp3"); Engine::Get().AsyncLoadSound("hit", "hit.mp3"); Engine::Get().AsyncLoadSound("powerup-spawn", "powerup-spawn.mp3"); Engine::Get().AsyncLoadSound("powerup-pick", "powerup-pick.mp3"); return true; } bool Enemy::Initialize() { boss_.SetZOrder(10); boss_animator_.Attach(&boss_); boss_intro_.SetSound("boss_intro"); 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> 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(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(0) << "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(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}); if (enemy_type != kEnemyType_PowerUp) { 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(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("powerup-pick"); e.spawn.SetSound("powerup-spawn"); e.spawn.SetMaxAmplitude(2.0f); e.spawn.Play(false); } else { e.explosion.SetSound("explosion"); e.explosion.SetVariate(true); e.explosion.SetSimulateStereo(true); e.explosion.SetMaxAmplitude(0.9f); } e.stealth.SetSound("stealth"); e.stealth.SetVariate(false); e.stealth.SetSimulateStereo(false); e.stealth.SetMaxAmplitude(0.7f); e.shield_on.SetSound("shield"); e.shield_on.SetVariate(false); e.shield_on.SetSimulateStereo(false); e.shield_on.SetMaxAmplitude(0.5f); e.hit.SetSound("hit"); e.hit.SetVariate(true); e.hit.SetSimulateStereo(false); e.hit.SetMaxAmplitude(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(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(0) << " 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"); e.explosion.SetVariate(false); e.explosion.SetSimulateStereo(false); e.hit.SetSound("hit"); e.hit.SetVariate(true); e.hit.SetSimulateStereo(false); e.hit.SetMaxAmplitude(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(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(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 Enemy::GetScoreImage(EnemyType enemy_type) { const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); std::string text = std::to_string(GetScore(enemy_type)); int width, height; font.CalculateBoundingBox(text.c_str(), width, height); auto image = std::make_unique(); 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; } 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); }