diff --git a/.gitignore b/.gitignore index c12247a..5fcbfb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.vscode build/android/.gradle build/android/app/.cxx build/android/app/build diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b508809 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/linux/gltest_x86_64_debug", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/build/linux", + "environment": [], + "console": "externalTerminal", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 0b54d93..5fea6ac 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ A simple, cross-platform 2D game engine with OpenGL and Vulkan renderers. Supports Linux and Android (lolipop+) platforms. This is a personal hobby project. I've published a little game on [Google Play](https://play.google.com/store/apps/details?id=com.woom.game) -based on this engine. The demo included in this repository is an early prototype -of the game. +based on this engine. Full game code and assets are included in this repository. #### Building the demo Linux: ```text diff --git a/assets/Boss_ok.png b/assets/Boss_ok.png new file mode 100644 index 0000000..290ea5e Binary files /dev/null and b/assets/Boss_ok.png differ diff --git a/assets/Boss_ok_lvl2.png b/assets/Boss_ok_lvl2.png new file mode 100644 index 0000000..a4ef6b6 Binary files /dev/null and b/assets/Boss_ok_lvl2.png differ diff --git a/assets/Boss_ok_lvl3.png b/assets/Boss_ok_lvl3.png new file mode 100644 index 0000000..867024c Binary files /dev/null and b/assets/Boss_ok_lvl3.png differ diff --git a/assets/Game_2_Boss.mp3 b/assets/Game_2_Boss.mp3 new file mode 100644 index 0000000..7cdb3e3 Binary files /dev/null and b/assets/Game_2_Boss.mp3 differ diff --git a/assets/Game_2_Main.mp3 b/assets/Game_2_Main.mp3 new file mode 100644 index 0000000..14d47e9 Binary files /dev/null and b/assets/Game_2_Main.mp3 differ diff --git a/assets/bead.png b/assets/bead.png new file mode 100644 index 0000000..62cc63e Binary files /dev/null and b/assets/bead.png differ diff --git a/assets/boss_explosion.mp3 b/assets/boss_explosion.mp3 new file mode 100644 index 0000000..faf2b35 Binary files /dev/null and b/assets/boss_explosion.mp3 differ diff --git a/assets/boss_intro.mp3 b/assets/boss_intro.mp3 new file mode 100644 index 0000000..383286a Binary files /dev/null and b/assets/boss_intro.mp3 differ diff --git a/assets/chromatic_aberration.glsl_fragment b/assets/chromatic_aberration.glsl_fragment new file mode 100644 index 0000000..8b28e1e --- /dev/null +++ b/assets/chromatic_aberration.glsl_fragment @@ -0,0 +1,27 @@ +#ifdef GL_ES +precision mediump float; +#endif + +IN(0) vec2 tex_coord_0; + +UNIFORM_BEGIN + UNIFORM_V(vec2 scale) + UNIFORM_V(vec2 offset) + UNIFORM_V(vec2 rotation) + UNIFORM_V(vec2 tex_offset) + UNIFORM_V(vec2 tex_scale) + UNIFORM_F(float aberration_offset) + UNIFORM_V(mat4 projection) + UNIFORM_F(vec4 color) +UNIFORM_END + +SAMPLER(0, sampler2D texture_0) + +FRAG_COLOR_OUT(frag_color) + +void main() { + vec4 r = TEXTURE(texture_0, tex_coord_0 - vec2(PARAM(aberration_offset), 0)); + vec4 g = TEXTURE(texture_0, tex_coord_0 - vec2(-PARAM(aberration_offset), 0)); + vec4 b = TEXTURE(texture_0, tex_coord_0 - vec2(0, PARAM(aberration_offset))); + FRAG_COLOR(frag_color) = vec4(r.x, g.y, b.z, (r.w + g.w + b.w) / 3.0) * PARAM(color); +} diff --git a/assets/chromatic_aberration.glsl_vertex b/assets/chromatic_aberration.glsl_vertex new file mode 100644 index 0000000..418e0b4 --- /dev/null +++ b/assets/chromatic_aberration.glsl_vertex @@ -0,0 +1,28 @@ +IN(0) vec2 in_position; +IN(1) vec2 in_tex_coord_0; + +UNIFORM_BEGIN + UNIFORM_V(vec2 scale) + UNIFORM_V(vec2 offset) + UNIFORM_V(vec2 rotation) + UNIFORM_V(vec2 tex_offset) + UNIFORM_V(vec2 tex_scale) + UNIFORM_F(float aberration_offset) + UNIFORM_V(mat4 projection) + UNIFORM_F(vec4 color) +UNIFORM_END + +OUT(0) vec2 tex_coord_0; + +void main() { + // Simple 2d transform. + vec2 position = in_position; + position *= PARAM(scale); + position = vec2(position.x * PARAM(rotation).y + position.y * PARAM(rotation).x, + position.y * PARAM(rotation).y - position.x * PARAM(rotation).x); + position += PARAM(offset); + + tex_coord_0 = (in_tex_coord_0 + PARAM(tex_offset)) * PARAM(tex_scale); + + gl_Position = PARAM(projection) * vec4(position, 0.0, 1.0); +} diff --git a/assets/crate.png b/assets/crate.png new file mode 100644 index 0000000..96ad59e Binary files /dev/null and b/assets/crate.png differ diff --git a/assets/hit.mp3 b/assets/hit.mp3 new file mode 100644 index 0000000..bc52ac5 Binary files /dev/null and b/assets/hit.mp3 differ diff --git a/assets/laser.mp3 b/assets/laser.mp3 new file mode 100644 index 0000000..321e71a Binary files /dev/null and b/assets/laser.mp3 differ diff --git a/assets/menu_click.mp3 b/assets/menu_click.mp3 new file mode 100644 index 0000000..fc496eb Binary files /dev/null and b/assets/menu_click.mp3 differ diff --git a/assets/menu_icons.png b/assets/menu_icons.png new file mode 100644 index 0000000..18a496b Binary files /dev/null and b/assets/menu_icons.png differ diff --git a/assets/no_nuke.mp3 b/assets/no_nuke.mp3 new file mode 100644 index 0000000..773fbbc Binary files /dev/null and b/assets/no_nuke.mp3 differ diff --git a/assets/nuke.mp3 b/assets/nuke.mp3 new file mode 100644 index 0000000..7dc88e1 Binary files /dev/null and b/assets/nuke.mp3 differ diff --git a/assets/nuke_frames.png b/assets/nuke_frames.png new file mode 100644 index 0000000..f30db2d Binary files /dev/null and b/assets/nuke_frames.png differ diff --git a/assets/nuke_pack_OK.png b/assets/nuke_pack_OK.png new file mode 100644 index 0000000..e8c56d7 Binary files /dev/null and b/assets/nuke_pack_OK.png differ diff --git a/assets/powerup-pick.mp3 b/assets/powerup-pick.mp3 new file mode 100644 index 0000000..03e2fc9 Binary files /dev/null and b/assets/powerup-pick.mp3 differ diff --git a/assets/powerup-spawn.mp3 b/assets/powerup-spawn.mp3 new file mode 100644 index 0000000..7b159ad Binary files /dev/null and b/assets/powerup-spawn.mp3 differ diff --git a/assets/shield.mp3 b/assets/shield.mp3 new file mode 100644 index 0000000..2ff0f4a Binary files /dev/null and b/assets/shield.mp3 differ diff --git a/assets/sky_without_nebula.glsl_fragment b/assets/sky_without_nebula.glsl_fragment new file mode 100644 index 0000000..46441ba --- /dev/null +++ b/assets/sky_without_nebula.glsl_fragment @@ -0,0 +1,47 @@ +#ifdef GL_ES +precision highp float; +#else +#define lowp +#define mediump +#define highp +#endif + +IN(0) vec2 tex_coord_0; + +UNIFORM_BEGIN + UNIFORM_V(vec2 scale) + UNIFORM_V(mat4 projection) + UNIFORM_F(vec2 sky_offset) +UNIFORM_END + +FRAG_COLOR_OUT(frag_color) + +float random(vec2 p) { + float sd = sin(dot(p, vec2(54.90898, 18.233))); + return fract(sd * 2671.6182); +} + +float stars(in vec2 p, float num_cells, float size) { + vec2 n = p * num_cells; + vec2 i = floor(n); + + vec2 a = n - i - random(i); + a /= num_cells * size; + float e = dot(a, a); + + return smoothstep(0.94, 1.0, (1.0 - e * 35.0)); +} + +void main() { + vec2 layer1_coord = tex_coord_0 + PARAM(sky_offset); + vec2 layer2_coord = tex_coord_0 + PARAM(sky_offset) * 0.7; + mediump vec3 result = vec3(0.); + + float c = stars(layer1_coord, 8.0, 0.05); + result += vec3(0.97, 0.74, 0.74) * c; + + c = stars(layer2_coord, 16.0, 0.025) * 0.5; + result += vec3(0.9, 0.9, 0.95) * c; + + FRAG_COLOR(frag_color) = vec4(result, 1.0); +} diff --git a/assets/sky_without_nebula.glsl_vertex b/assets/sky_without_nebula.glsl_vertex new file mode 100644 index 0000000..c9b8d5d --- /dev/null +++ b/assets/sky_without_nebula.glsl_vertex @@ -0,0 +1,20 @@ +IN(0) vec2 in_position; +IN(1) vec2 in_tex_coord_0; + +UNIFORM_BEGIN + UNIFORM_V(vec2 scale) + UNIFORM_V(mat4 projection) + UNIFORM_F(vec2 sky_offset) +UNIFORM_END + +OUT(0) vec2 tex_coord_0; + +void main() { + // Simple 2d transform. + vec2 position = in_position; + position *= PARAM(scale); + + tex_coord_0 = in_tex_coord_0; + + gl_Position = PARAM(projection) * vec4(position, 0.0, 1.0); +} diff --git a/assets/stealth.mp3 b/assets/stealth.mp3 new file mode 100644 index 0000000..8b34013 Binary files /dev/null and b/assets/stealth.mp3 differ diff --git a/assets/woom_enemy_shield.png b/assets/woom_enemy_shield.png new file mode 100644 index 0000000..d819279 Binary files /dev/null and b/assets/woom_enemy_shield.png differ diff --git a/assets/woom_logo_start_frames_01.png b/assets/woom_logo_start_frames_01.png new file mode 100644 index 0000000..ac284df Binary files /dev/null and b/assets/woom_logo_start_frames_01.png differ diff --git a/assets/woom_logo_start_frames_02-03.png b/assets/woom_logo_start_frames_02-03.png new file mode 100644 index 0000000..e78943d Binary files /dev/null and b/assets/woom_logo_start_frames_02-03.png differ diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle index 5cd8399..c0ecd4d 100644 --- a/build/android/app/build.gradle +++ b/build/android/app/build.gradle @@ -5,7 +5,7 @@ android { ndkVersion '21.3.6528147' defaultConfig { - applicationId = 'com.kaliber.demo' + applicationId = 'com.woom.game' minSdkVersion 21 targetSdkVersion 29 externalNativeBuild { diff --git a/build/android/app/src/main/AndroidManifest.xml b/build/android/app/src/main/AndroidManifest.xml index bd36cb6..3555ac8 100644 --- a/build/android/app/src/main/AndroidManifest.xml +++ b/build/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + package="com.woom.game" + android:versionCode="16" + android:versionName="1.0.1"> - demo - ca-app-pub-3940256099942544/1033173712 + woom + ca-app-pub-1321063817979967/8373182022 diff --git a/build/android/local.properties b/build/android/local.properties deleted file mode 100644 index 9ee0358..0000000 --- a/build/android/local.properties +++ /dev/null @@ -1,9 +0,0 @@ -## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. -#Fri Oct 16 12:48:26 CEST 2020 -ndk.dir=/home/auygun/spinning/Android/Sdk/ndk/21.3.6528147 -sdk.dir=/home/auygun/spinning/Android/Sdk diff --git a/build/android/sign b/build/android/sign new file mode 100755 index 0000000..f809c9e --- /dev/null +++ b/build/android/sign @@ -0,0 +1,2 @@ +jarsigner -keystore ~/key_store/apk_key_store.jks -storepass xxx -keypass xxx -signedjar app-release.apk ./app/build/outputs/apk/release/app-release-unsigned.apk upload +zipalign -f 4 ./app-release.apk ./app-release-aligned.apk diff --git a/src/demo/credits.cc b/src/demo/credits.cc index 04774ba..deb6b5d 100644 --- a/src/demo/credits.cc +++ b/src/demo/credits.cc @@ -13,10 +13,15 @@ using namespace eng; namespace { -constexpr char kCreditsLines[Credits::kNumLines][15] = { - "Credits", "Code:", "Attila Uygun", "Graphics:", "Erkan Ertürk"}; +constexpr char kCreditsLines[Credits::kNumLines][40] = { + "Credits", "Code", + "Attila Uygun", "Graphics", + "Erkan Ertürk", "Music", + "Patrik Häggblad", "Special thanks", + "Peter Pettersson", "github.com/auygun/kaliber"}; -constexpr float kLineSpaces[Credits::kNumLines - 1] = {1.5f, 0.5f, 1.5f, 0.5f}; +constexpr float kLineSpaces[Credits::kNumLines - 1] = { + 1.5f, 0.5f, 1.5f, 0.5f, 1.5f, 0.5f, 1.5f, 0.5f, 1.5f}; const Vector4f kTextColor = {0.80f, 0.87f, 0.93f, 1}; constexpr float kFadeSpeed = 0.2f; diff --git a/src/demo/credits.h b/src/demo/credits.h index afa83c7..d0fb7a3 100644 --- a/src/demo/credits.h +++ b/src/demo/credits.h @@ -14,7 +14,7 @@ class InputEvent; class Credits { public: - static constexpr int kNumLines = 5; + static constexpr int kNumLines = 10; Credits(); ~Credits(); diff --git a/src/demo/damage_type.h b/src/demo/damage_type.h index 6757427..aa7f6b7 100644 --- a/src/demo/damage_type.h +++ b/src/demo/damage_type.h @@ -11,10 +11,23 @@ enum DamageType { enum EnemyType { kEnemyType_Invalid = -1, - kEnemyType_Skull, - kEnemyType_Bug, + // Enemy units (waves and boss adds). + kEnemyType_LightSkull, + kEnemyType_DarkSkull, kEnemyType_Tank, + kEnemyType_Bug, + kEnemyType_Unit_Last = kEnemyType_Bug, + // Boss. + kEnemyType_PowerUp, + kEnemyType_Boss, kEnemyType_Max }; +enum SpeedType { + kSpeedType_Invalid = -1, + kSpeedType_Slow, + kSpeedType_Fast, + kSpeedType_Max +}; + #endif // DAMAGE_TYPE_H diff --git a/src/demo/demo.cc b/src/demo/demo.cc index 781826e..e90299f 100644 --- a/src/demo/demo.cc +++ b/src/demo/demo.cc @@ -1,27 +1,59 @@ #include "demo.h" #include +#include +#include #include +#include "../base/file.h" #include "../base/interpolation.h" #include "../base/log.h" #include "../base/random.h" +#include "../base/timer.h" #include "../engine/engine.h" #include "../engine/game_factory.h" #include "../engine/input_event.h" +#include "../engine/sound.h" DECLARE_GAME_BEGIN DECLARE_GAME(Demo) DECLARE_GAME_END +// #define RECORD 15 +// #define REPLAY + +using namespace std::string_literals; + using namespace base; using namespace eng; +namespace { + +const Vector4f kBgColor = {0, 0, 0, 0.8f}; +constexpr float kFadeSpeed = 0.2f; + +constexpr int kLaunchCountBeforeAd = 2; + +const char kSaveFileName[] = "woom"; +const char kHightScore[] = "high_score"; +const char kLastWave[] = "last_wave"; +const char kLaunchCount[] = "launch_count"; + +} // namespace + +Demo::Demo() = default; + +Demo::~Demo() { + saved_data_.Save(); +} + bool Demo::Initialize() { + saved_data_.Load(kSaveFileName); + if (!font_.Load("PixelCaps!.ttf")) return false; - if (!sky_.Create()) { + if (!sky_.Create(false)) { LOG << "Could not create the sky."; return false; } @@ -51,6 +83,39 @@ bool Demo::Initialize() { return false; } + auto sound = std::make_unique(); + if (!sound->Load("Game_2_Main.mp3", true)) + return false; + + auto boss_sound = std::make_unique(); + if (!boss_sound->Load("Game_2_Boss.mp3", true)) + return false; + + music_.SetSound(std::move(sound)); + music_.SetMaxAplitude(0.5f); + + boss_music_.SetSound(std::move(boss_sound)); + boss_music_.SetMaxAplitude(0.5f); + + if (!saved_data_.root().get("audio", Json::Value(true)).asBool()) + Engine::Get().SetEnableAudio(false); + else if (saved_data_.root().get("music", Json::Value(true)).asBool()) + music_.Play(true); + + if (!saved_data_.root().get("vibration", Json::Value(true)).asBool()) { + Engine::Get().SetEnableVibration(false); + } + + dimmer_.SetSize(Engine::Get().GetScreenSize()); + dimmer_.SetZOrder(40); + dimmer_.SetColor(kBgColor); + dimmer_.SetVisible(true); + dimmer_active_ = true; + dimmer_animator_.Attach(&dimmer_); + + saved_data_.root()[kLaunchCount] = + saved_data_.root().get(kLaunchCount, Json::Value(0)).asInt() + 1; + EnterMenuState(); return true; @@ -59,68 +124,101 @@ bool Demo::Initialize() { void Demo::Update(float delta_time) { Engine& engine = Engine::Get(); + if (do_benchmark_) { + benchmark_time_ += delta_time; + if (benchmark_time_ > 1) { + avarage_fps_ += Engine::Get().fps(); + ++num_benchmark_samples_; + } + if (benchmark_time_ > 6) { + avarage_fps_ /= num_benchmark_samples_; + do_benchmark_ = false; + BenchmarkResult(avarage_fps_); + } + } + + stage_time_ += delta_time; + while (std::unique_ptr event = engine.GetNextInputEvent()) { +#if 0 + if (event->GetType() == InputEvent::kDragEnd && + ((engine.GetScreenSize() / 2) * 0.9f - + event->GetVector() * Vector2f(-1, 1)) + .Length() <= 0.25f) + Win(); +#endif + if (state_ == kMenu) menu_.OnInputEvent(std::move(event)); else if (state_ == kCredits) credits_.OnInputEvent(std::move(event)); - else + else if (state_ != kGameOver) player_.OnInputEvent(std::move(event)); } - if (delayed_work_timer_ > 0) { - delayed_work_timer_ -= delta_time; - if (delayed_work_timer_ <= 0) { - base::Closure cb = std::move(delayed_work_cb_); - delayed_work_cb_ = nullptr; - cb(); - } - } - - if (add_score_ > 0) { - score_ += add_score_; - add_score_ = 0; - hud_.SetScore(score_, true); - } - - sky_.Update(delta_time); - player_.Update(delta_time); - enemy_.Update(delta_time); - if (state_ == kMenu) UpdateMenuState(delta_time); - else if (state_ == kGame) + else if (state_ == kGame || state_ == kGameOver) UpdateGameState(delta_time); } void Demo::ContextLost() { sky_.ContextLost(); + enemy_.ContextLost(); } -void Demo::LostFocus() { - if (state_ == kGame) - EnterMenuState(); +void Demo::LostFocus() {} + +void Demo::GainedFocus(bool from_interstitial_ad) { + DLOG << __func__ << " from_interstitial_ad: " << from_interstitial_ad; + if (!from_interstitial_ad) { + if (saved_data_.root().get(kLaunchCount, Json::Value(0)).asInt() > + kLaunchCountBeforeAd) + Engine::Get().ShowInterstitialAd(); + if (state_ == kGame) + EnterMenuState(); + } } -void Demo::GainedFocus(bool from_interstitial_ad) {} +void Demo::AddScore(size_t score) { + delta_score_ += score; + wave_score_ += score; +} -void Demo::AddScore(int score) { - add_score_ += score; +void Demo::SetEnableMusic(bool enable) { + if (enable) { + if (boss_fight_) + boss_music_.Resume(1); + else + music_.Resume(1); + } else { + music_.Stop(1); + boss_music_.Stop(1); + } } void Demo::EnterMenuState() { + saved_data_.Save(); + if (state_ == kMenu) return; + player_.OnInputEvent( + std::make_unique(InputEvent::kDragCancel, (size_t)0)); + player_.OnInputEvent( + std::make_unique(InputEvent::kDragCancel, (size_t)1)); + + Dimmer(true); if (state_ == kState_Invalid || state_ == kGame) { - sky_.SetSpeed(0); + hud_.Pause(true); player_.Pause(true); enemy_.Pause(true); } - if (wave_ == 0) { + if (state_ == kState_Invalid || state_ == kGameOver) { menu_.SetOptionEnabled(Menu::kContinue, false); - } else { + menu_.SetOptionEnabled(Menu::kNewGame, true); + } else if (state_ == kGame) { menu_.SetOptionEnabled(Menu::kContinue, true); menu_.SetOptionEnabled(Menu::kNewGame, false); } @@ -131,6 +229,7 @@ void Demo::EnterMenuState() { void Demo::EnterCreditsState() { if (state_ == kCredits) return; + credits_.Show(); state_ = kCredits; } @@ -139,13 +238,50 @@ void Demo::EnterGameState() { if (state_ == kGame) return; + Dimmer(false); sky_.SetSpeed(0.04f); + hud_.Show(); + hud_.Pause(false); player_.Pause(false); enemy_.Pause(false); + if (boss_fight_) + hud_.HideProgress(); state_ = kGame; } +void Demo::EnterGameOverState() { + if (state_ == kGameOver) + return; + + saved_data_.Save(); + + enemy_.PauseProgress(); + enemy_.StopAllEnemyUnits(); + sky_.SwitchColor({0, 0, 0, 1}); + hud_.ShowMessage("Game Over", 3); + state_ = kGameOver; + + if (boss_fight_) { + music_.Resume(10); + boss_music_.Stop(10); + } + + SetDelayedWork(1, [&]() -> void { + enemy_.RemoveAll(); + // hud_.Hide(); + SetDelayedWork(3, [&]() -> void { + wave_ = 0; + boss_fight_ = false; + EnterMenuState(); + }); + }); + +#if defined(RECORD) + Engine::Get().EndRecording("replay"); +#endif +} + void Demo::UpdateMenuState(float delta_time) { switch (menu_.selected_option()) { case Menu::kOption_Invalid: @@ -155,8 +291,12 @@ void Demo::UpdateMenuState(float delta_time) { Continue(); break; case Menu::kNewGame: - menu_.Hide(); - StartNewGame(); + menu_.Hide([&]() { + if (saved_data_.root().get(kLaunchCount, Json::Value(0)).asInt() > + kLaunchCountBeforeAd) + Engine::Get().ShowInterstitialAd(); + StartNewGame(); + }); break; case Menu::kCredits: menu_.Hide(); @@ -171,20 +311,131 @@ void Demo::UpdateMenuState(float delta_time) { } void Demo::UpdateGameState(float delta_time) { + if (delayed_work_timer_ > 0) { + delayed_work_timer_ -= delta_time; + if (delayed_work_timer_ <= 0) { + base::Closure cb = std::move(delayed_work_cb_); + delayed_work_cb_ = nullptr; + cb(); + } + } + + if (delta_score_ > 0) { + total_score_ += delta_score_; + delta_score_ = 0; + hud_.SetScore(total_score_, true); + + if (total_score_ > GetHighScore()) + saved_data_.root()[kHightScore] = total_score_; + } + + if (wave_ > saved_data_.root().get(kLastWave, Json::Value(0)).asInt()) + saved_data_.root()[kLastWave] = wave_; + + sky_.Update(delta_time); + player_.Update(delta_time); + enemy_.Update(delta_time); + if (waiting_for_next_wave_) return; - if (enemy_.num_enemies_killed_in_current_wave() != last_num_enemies_killed_) { - last_num_enemies_killed_ = enemy_.num_enemies_killed_in_current_wave(); + if (boss_fight_) { + if (!enemy_.IsBossAlive()) + StartNextStage(false); + } else if (enemy_.num_enemies_killed_in_current_wave() != + last_num_enemies_killed_) { + bool no_boss = (last_num_enemies_killed_ == -1); + if (last_num_enemies_killed_ < enemy_.num_enemies_killed_in_current_wave()) + last_num_enemies_killed_ = enemy_.num_enemies_killed_in_current_wave(); int enemies_remaining = total_enemies_ - last_num_enemies_killed_; - if (enemies_remaining <= 0) { - waiting_for_next_wave_ = true; - hud_.SetProgress(wave_ > 0 ? 0 : 1); + if (enemies_remaining <= 0) + StartNextStage(wave_ && !(wave_ % 3) && !no_boss); + else + hud_.SetProgress((float)enemies_remaining / (float)total_enemies_); + } +} - enemy_.OnWaveFinished(); +void Demo::Continue() { + EnterGameState(); +} + +void Demo::StartNewGame() { +#if defined(RECORD) + Json::Value game_data; + game_data["wave"] = RECORD; + wave_ = RECORD - 1; + Engine::Get().StartRecording(game_data); +#elif defined(REPLAY) + Json::Value game_data; + Engine::Get().Replay("replay", game_data); + wave_ = game_data["wave"].asInt() - 1; +#else + wave_ = menu_.start_from_wave() - 1; +#endif + + wave_score_ = total_score_ = delta_score_ = 0; + last_num_enemies_killed_ = -1; + total_enemies_ = 0; + waiting_for_next_wave_ = false; + boss_fight_ = false; + delayed_work_timer_ = 0; + delayed_work_cb_ = nullptr; + player_.Reset(); + enemy_.Reset(); + EnterGameState(); +} + +void Demo::StartNextStage(bool boss) { + waiting_for_next_wave_ = true; + hud_.SetProgress(wave_ > 0 ? 0 : 1); + + DLOG_IF(wave_ > 0 && stage_time_ > 0) + << "wave: " << wave_ << " time: " << stage_time_ / 60.0f; + stage_time_ = 0; + + enemy_.PauseProgress(); + enemy_.StopAllEnemyUnits(); + + if (boss) { + boss_music_.Play(true, 10); + music_.Stop(10); + } else if (boss_fight_) { + music_.Resume(10); + boss_music_.Stop(10); + } + + SetDelayedWork(1.25f, [&, boss]() -> void { + enemy_.KillAllEnemyUnits(); + + SetDelayedWork(boss_fight_ ? 4 : 0.5f, [&, boss]() -> void { + if (boss) { + sky_.SwitchColor(sky_.nebula_color() * 0.5f); + + hud_.HideProgress(); + + boss_fight_ = true; + DLOG << "Boss fight."; + } else { + size_t bonus_factor = [&]() -> size_t { + if (wave_ <= 3) + return 2; + if (wave_ <= 6) + return 5; + if (wave_ <= 9) + return 15; + return 100; + }(); + size_t bonus_score = wave_score_ * (bonus_factor - 1); + DLOG << "total_score_" << total_score_ << " wave " << wave_ + << " score: " << wave_score_ << " bonus: " << bonus_score; + + if (bonus_score > 0) { + delta_score_ += bonus_score; + hud_.ShowBonus(bonus_score); + wave_score_ = 0; + } - SetDelayedWork(1, [&]() -> void { Random& rnd = Engine::Get().GetRandomGenerator(); int dominant_channel = rnd.Roll(3) - 1; if (dominant_channel == last_dominant_channel_) @@ -202,39 +453,56 @@ void Demo::UpdateGameState(float delta_time) { sky_.SwitchColor(c); ++wave_; - hud_.SetScore(score_, true); - hud_.SetWave(wave_, true); + hud_.Show(); hud_.SetProgress(1); - float factor = 3 * (log10(5 * (float)wave_) / log10(1.2f)) - 25; - total_enemies_ = (int)(6 * factor); + if (boss_fight_) + player_.TakeDamage(-3); + + total_enemies_ = 20.0f + 23.0897f * log((float)wave_); last_num_enemies_killed_ = 0; + boss_fight_ = false; DLOG << "wave: " << wave_ << " total_enemies_: " << total_enemies_; + } - enemy_.OnWaveStarted(wave_); + hud_.SetScore(total_score_, true); + hud_.SetWave(wave_, true); - waiting_for_next_wave_ = false; - }); - } else { - hud_.SetProgress((float)enemies_remaining / (float)total_enemies_); - } + enemy_.OnWaveStarted(wave_, boss); + + waiting_for_next_wave_ = false; + }); + }); +} + +void Demo::Win() { + // Satisfy win conditions. + if (boss_fight_) + enemy_.KillBoss(); + else + last_num_enemies_killed_ = total_enemies_; +} + +void Demo::Dimmer(bool enable) { + if (enable && !dimmer_active_) { + dimmer_active_ = true; + dimmer_.SetColor(kBgColor * Vector4f(0, 0, 0, 0)); + dimmer_animator_.SetBlending(kBgColor, kFadeSpeed); + dimmer_animator_.Play(Animator::kBlending, false); + dimmer_animator_.SetVisible(true); + } else if (!enable && dimmer_active_) { + dimmer_active_ = false; + dimmer_animator_.SetBlending(kBgColor * Vector4f(0, 0, 0, 0), kFadeSpeed); + dimmer_animator_.Play(Animator::kBlending, false); + dimmer_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + dimmer_animator_.SetEndCallback(Animator::kBlending, nullptr); + dimmer_animator_.SetVisible(false); + }); } } -void Demo::Continue() { - EnterGameState(); -} - -void Demo::StartNewGame() { - score_ = 0; - add_score_ = 0; - wave_ = 0; - last_num_enemies_killed_ = -1; - total_enemies_ = 0; - waiting_for_next_wave_ = false; - delayed_work_timer_ = 0; - delayed_work_cb_ = nullptr; - EnterGameState(); +size_t Demo::GetHighScore() const { + return saved_data_.root().get(kHightScore, Json::Value(0)).asUInt64(); } void Demo::SetDelayedWork(float seconds, base::Closure cb) { @@ -242,3 +510,9 @@ void Demo::SetDelayedWork(float seconds, base::Closure cb) { delayed_work_cb_ = std::move(cb); delayed_work_timer_ = seconds; } + +void Demo::BenchmarkResult(int avarage_fps) { + LOG << __func__ << " avarage_fps: " << avarage_fps; + if (avarage_fps < 30) + sky_.Create(true); +} diff --git a/src/demo/demo.h b/src/demo/demo.h index dddc60a..651e0dd 100644 --- a/src/demo/demo.h +++ b/src/demo/demo.h @@ -2,8 +2,12 @@ #define DEMO_H #include "../base/closure.h" +#include "../engine/animator.h" #include "../engine/font.h" #include "../engine/game.h" +#include "../engine/persistent_data.h" +#include "../engine/solid_quad.h" +#include "../engine/sound_player.h" #include "credits.h" #include "enemy.h" #include "hud.h" @@ -11,10 +15,12 @@ #include "player.h" #include "sky_quad.h" +// #define LOAD_TEST + class Demo : public eng::Game { public: - Demo() = default; - ~Demo() override = default; + Demo(); + ~Demo() override; bool Initialize() override; @@ -26,21 +32,38 @@ class Demo : public eng::Game { void GainedFocus(bool from_interstitial_ad) override; - void AddScore(int score); + void AddScore(size_t score); + + void SetEnableMusic(bool enable); void EnterMenuState(); void EnterCreditsState(); void EnterGameState(); + void EnterGameOverState(); const eng::Font& GetFont() { return font_; } Player& GetPlayer() { return player_; } Enemy& GetEnemy() { return enemy_; } - int wave() { return wave_; } + int wave() const { return wave_; } + + size_t GetHighScore() const; + + float stage_time() const { return stage_time_; } + + eng::PersistentData& saved_data() { return saved_data_; } + const eng::PersistentData& saved_data() const { return saved_data_; } private: - enum State { kState_Invalid = -1, kMenu, kGame, kCredits, kState_Max }; + enum State { + kState_Invalid = -1, + kMenu, + kGame, + kCredits, + kGameOver, + kState_Max + }; State state_ = kState_Invalid; @@ -55,8 +78,9 @@ class Demo : public eng::Game { eng::Font font_; - int score_ = 0; - int add_score_ = 0; + size_t wave_score_ = 0; + size_t total_score_ = 0; + size_t delta_score_ = 0; int wave_ = 0; @@ -65,16 +89,42 @@ class Demo : public eng::Game { int waiting_for_next_wave_ = false; + bool boss_fight_ = false; + + float stage_time_ = 0; + + eng::SoundPlayer music_; + eng::SoundPlayer boss_music_; + + eng::SolidQuad dimmer_; + eng::Animator dimmer_animator_; + bool dimmer_active_ = false; + float delayed_work_timer_ = 0; base::Closure delayed_work_cb_; + eng::PersistentData saved_data_; + + bool do_benchmark_ = true; + float benchmark_time_ = 0; + int num_benchmark_samples_ = 0; + int avarage_fps_ = 0; + void UpdateMenuState(float delta_time); void UpdateGameState(float delta_time); void Continue(); void StartNewGame(); + void StartNextStage(bool boss); + + void Win(); + + void Dimmer(bool enable); + void SetDelayedWork(float seconds, base::Closure cb); + + void BenchmarkResult(int avarage_fps); }; #endif // DEMO_H diff --git a/src/demo/enemy.cc b/src/demo/enemy.cc index a8d90cb..0d9d8a8 100644 --- a/src/demo/enemy.cc +++ b/src/demo/enemy.cc @@ -1,7 +1,10 @@ #include "enemy.h" +#include #include #include +#include +#include #include "../base/collusion_test.h" #include "../base/interpolation.h" @@ -9,6 +12,9 @@ #include "../engine/engine.h" #include "../engine/font.h" #include "../engine/image.h" +#include "../engine/renderer/geometry.h" +#include "../engine/renderer/shader.h" +#include "../engine/shader_source.h" #include "../engine/sound.h" #include "demo.h" @@ -19,17 +25,35 @@ 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 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 enemy_scores[] = {100, 150, 300}; +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 float kSpawnPeriod[kEnemyType_Max][2] = {{2, 5}, - {15, 25}, - {110, 130}}; +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 { @@ -42,52 +66,145 @@ void SetupFadeOutAnim(Animator& animator, float delay) { 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().GetFloat()); + return (s.x / 4) / 2 + (s.x / 4) * col - s.x / 2 + offset; +} + } // namespace -Enemy::Enemy() = default; +Enemy::Enemy() + : chromatic_aberration_(Engine::Get().CreateRenderResource()) {} Enemy::~Enemy() = default; bool Enemy::Initialize() { + boss_intro_sound_ = std::make_shared(); + if (!boss_intro_sound_->Load("boss_intro.mp3", false)) + return false; + + boss_explosion_sound_ = std::make_shared(); + if (!boss_explosion_sound_->Load("boss_explosion.mp3", false)) + return false; + explosion_sound_ = std::make_shared(); if (!explosion_sound_->Load("explosion.mp3", false)) return false; - return CreateRenderResources(); + stealth_sound_ = std::make_shared(); + if (!stealth_sound_->Load("stealth.mp3", false)) + return false; + + shield_on_sound_ = std::make_shared(); + if (!shield_on_sound_->Load("shield.mp3", false)) + return false; + + hit_sound_ = std::make_shared(); + if (!hit_sound_->Load("hit.mp3", false)) + return false; + + power_up_spawn_sound_ = std::make_shared(); + if (!power_up_spawn_sound_->Load("powerup-spawn.mp3", false)) + return false; + + power_up_pick_sound_ = std::make_shared(); + 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 (!waiting_for_next_wave_ && !paused_) { - if (spawn_factor_interpolator_ < 1) { - spawn_factor_interpolator_ += delta_time * 0.1f; - if (spawn_factor_interpolator_ > 1) - spawn_factor_interpolator_ = 1; + if (!progress_paused_) { + if (boss_fight_) + UpdateBoss(delta_time); + else + UpdateWave(delta_time); + } + + Random& 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; } - for (int i = 0; i < kEnemyType_Max; ++i) - seconds_since_last_spawn_[i] += delta_time; + if (it->chromatic_aberration_active_) { + it->sprite.SetCustomUniform( + "aberration_offset", Lerp(0.0f, 0.01f, 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 - SpawnNextEnemy(); + 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++; } - for (auto it = enemies_.begin(); it != enemies_.end();) { - if (it->marked_for_removal) - it = enemies_.erase(it); - else - ++it; - } +#if defined(LOAD_TEST) + if (boss_fight_ && IsBossAlive() && boss_spawn_time_ > 40) + KillBoss(); +#endif } void Enemy::Pause(bool pause) { - paused_ = pause; for (auto& e : enemies_) { - e.movement_animator.PauseOrResumeAll(pause); 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); +} + +void Enemy::ContextLost() { + CreateShaders(); } bool Enemy::HasTarget(DamageType damage_type) { @@ -108,40 +225,96 @@ Vector2f Enemy::GetTargetPos(DamageType damage_type) { void Enemy::SelectTarget(DamageType damage_type, const Vector2f& origin, - const Vector2f& dir, - float snap_factor) { + const Vector2f& dir) { DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); - if (waiting_for_next_wave_) + if (progress_paused_) return; - EnemyUnit* best_enemy = nullptr; + std::vector> candidates; - float closest_dist = std::numeric_limits::max(); for (auto& e : enemies_) { - if (e.hit_points <= 0 || e.marked_for_removal) + if (e.hit_points <= 0 || e.marked_for_removal || e.stealth_active) continue; - if (e.targetted_by_weapon_ == damage_type) { - e.targetted_by_weapon_ = kDamageType_Invalid; + if (e.targetted_by_weapon_[damage_type]) { + e.targetted_by_weapon_[damage_type] = false; e.target.SetVisible(false); e.target_animator.Stop(Animator::kAllAnimations); } - if (!base::Intersection(e.sprite.GetPosition(), - e.sprite.GetSize() * snap_factor, origin, dir)) - continue; - Vector2f weapon_enemy_dir = e.sprite.GetPosition() - origin; - float enemy_weapon_dist = weapon_enemy_dir.Length(); - if (closest_dist > enemy_weapon_dist) { - closest_dist = enemy_weapon_dist; - best_enemy = &e; + 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; + best_enemy->targetted_by_weapon_[damage_type] = true; best_enemy->target.SetVisible(true); if (damage_type == kDamageType_Green) { best_enemy->target.SetFrame(0); @@ -159,7 +332,7 @@ void Enemy::DeselectTarget(DamageType damage_type) { EnemyUnit* target = GetTarget(damage_type); if (target) { - target->targetted_by_weapon_ = kDamageType_Invalid; + target->targetted_by_weapon_[damage_type] = false; target->target.SetVisible(false); target->target_animator.Stop(Animator::kAllAnimations); } @@ -168,136 +341,240 @@ void Enemy::DeselectTarget(DamageType damage_type) { void Enemy::HitTarget(DamageType damage_type) { DCHECK(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); - if (waiting_for_next_wave_) + if (progress_paused_) 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)) + 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 4. + if (wave_ <= 3) + 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); } -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; +bool Enemy::IsBossAlive() const { + return boss_fight_ && boss_.GetFrame() < 9; } -void Enemy::OnWaveStarted(int wave) { +void Enemy::PauseProgress() { + progress_paused_ = true; +} + +void Enemy::ResumeProgress() { + progress_paused_ = false; +} + +void Enemy::StopAllEnemyUnits(bool chromatic_aberration_effect) { 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); + 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); } } - 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; + + if (chromatic_aberration_effect) + chromatic_aberration_offset_ = 0.0f; } -void Enemy::TakeDamage(EnemyUnit* target, int damage) { - DCHECK(!target->marked_for_removal); - DCHECK(target->hit_points > 0); +void Enemy::KillAllEnemyUnits(bool randomize_order) { + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); - 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(engine.GetGame()); - game->AddScore(GetScore(target->enemy_type)); - } else { - target->targetted_by_weapon_ = kDamageType_Invalid; - - Vector2f s = target->sprite.GetSize() * Vector2f(0.6f, 0.01f); - 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); + 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().GetFloat()); + } else { + float dist = e.sprite.GetPosition().y - + game->GetPlayer().GetWeaponPos(kDamageType_Green).y; + e.kill_timer = dist * 0.15f; + } + } } } -void Enemy::SpawnNextEnemy() { - Engine& engine = Engine::Get(); - Random& rnd = engine.GetRandomGenerator(); +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); + } + } - float factor = Lerp(1.0f, spawn_factor_, spawn_factor_interpolator_); - EnemyType enemy_type = kEnemyType_Invalid; + // 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); + } +} - 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()); +void Enemy::KillBoss() { + for (auto& e : enemies_) { + if (e.enemy_type == kEnemyType_Boss) { + TakeDamage(&e, 1000); break; } } - - if (enemy_type == kEnemyType_Invalid) - return; - - DamageType damage_type = enemy_type == kEnemyType_Tank - ? kDamageType_Any - : (DamageType)(rnd.Roll(2) - 1); - - Vector2f 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; - Vector2f 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 Vector2f& pos, - float speed) { - DCHECK(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max); +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(); @@ -306,36 +583,64 @@ void Enemy::Spawn(EnemyType enemy_type, 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.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(enemy_frame_start[enemy_type][damage_type]); - e.sprite_animator.SetFrames(enemy_frame_count[enemy_type][damage_type], - enemy_frame_speed); + 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); - e.blast.Create("blast_tex", {6, 2}); + 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); @@ -350,60 +655,510 @@ void Enemy::Spawn(EnemyType enemy_type, 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 (damage_type == kDamageType_Green) { + 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, 28); + e.blast_animator.SetFrames(6, 18); } else { e.blast.SetFrame(6); - e.blast_animator.SetFrames(6, 28); + 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.2f); + 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 = - engine.GetScreenSize().y - game->GetPlayer().GetWeaponScale().y / 2; + spawn_pos.y - game->GetPlayer().GetWeaponPos(kDamageType_Green).y; + if (enemy_type == kEnemyType_PowerUp) + max_distance /= 2; - e.movement_animator.SetMovement( - {0, -max_distance}, speed, - std::bind(Acceleration, std::placeholders::_1, -0.15f)); + 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 { - e.sprite.SetVisible(false); + // Enemy has reached the player. + e.hit_points = 0; e.target.SetVisible(false); e.blast.SetVisible(false); - e.marked_for_removal = true; + 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); - e.explosion_.SetSound(explosion_sound_); - e.explosion_.SetVariate(true); - e.explosion_.SetSimulateStereo(true); + 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(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 = + -15.0845f + 41.1283f * log((float)game->wave()); + 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(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 | Animator::kTimer, true); + } + } 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); + + Random& rnd = Engine::Get().GetRandomGenerator(); + float stealth_timer = Lerp(2.0f, 5.0f, rnd.GetFloat()); + 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.GetFloat()); + 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 | Animator::kTimer, true); + } + } +} + +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(); + Random& 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.GetFloat()); + 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(1.3f * 60.0f, 1.8f * 60.0f, rnd.GetFloat()); + } +} + +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; + + Random& 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; + + DLOG << "boss_spawn_time_: " << boss_spawn_time_ + << " boss_spawn_factor: " << boss_spawn_factor; + + 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.GetFloat()); + 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.GetFloat()); + } + } + + 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.GetFloat()); + 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 && + if (e.targetted_by_weapon_[damage_type] && e.hit_points > 0 && !e.marked_for_removal) return &e; } @@ -419,6 +1174,9 @@ std::unique_ptr Enemy::GetScoreImage(EnemyType enemy_type) { const Font& font = static_cast(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); @@ -429,16 +1187,25 @@ std::unique_ptr Enemy::GetScoreImage(EnemyType enemy_type) { font.Print(0, 0, text.c_str(), image->GetBuffer(), image->GetWidth()); + image->Compress(); return image; } bool Enemy::CreateRenderResources() { + if (!CreateShaders()) + return false; + 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( @@ -447,3 +1214,23 @@ bool Enemy::CreateRenderResources() { return true; } + +bool Enemy::CreateShaders() { + auto source = std::make_unique(); + if (!source->Load("chromatic_aberration.glsl")) + return false; + chromatic_aberration_->Create(std::move(source), + Engine::Get().GetQuad()->vertex_description(), + Engine::Get().GetQuad()->primitive(), false); + 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); +} diff --git a/src/demo/enemy.h b/src/demo/enemy.h index 50295ba..95e8742 100644 --- a/src/demo/enemy.h +++ b/src/demo/enemy.h @@ -14,6 +14,7 @@ namespace eng { class Image; +class Shader; class Sound; } // namespace eng @@ -28,21 +29,34 @@ class Enemy { void Pause(bool pause); + void ContextLost(); + bool HasTarget(DamageType damage_type); base::Vector2f GetTargetPos(DamageType damage_type); void SelectTarget(DamageType damage_type, const base::Vector2f& origin, - const base::Vector2f& dir, - float snap_factor); + const base::Vector2f& dir); void DeselectTarget(DamageType damage_type); void HitTarget(DamageType damage_type); - void OnWaveFinished(); - void OnWaveStarted(int wave); + bool IsBossAlive() const; - int num_enemies_killed_in_current_wave() { + void PauseProgress(); + void ResumeProgress(); + + void OnWaveStarted(int wave, bool boss_figt); + + void StopAllEnemyUnits(bool chromatic_aberration_effect = false); + void KillAllEnemyUnits(bool randomize_order = true); + void RemoveAll(); + + void KillBoss(); + + void Reset(); + + int num_enemies_killed_in_current_wave() const { return num_enemies_killed_in_current_wave_; } @@ -50,15 +64,28 @@ class Enemy { struct EnemyUnit { EnemyType enemy_type = kEnemyType_Invalid; DamageType damage_type = kDamageType_Invalid; + SpeedType speed_type = kSpeedType_Invalid; bool marked_for_removal = false; - DamageType targetted_by_weapon_ = kDamageType_Invalid; + bool targetted_by_weapon_[2] = {false, false}; int total_health = 0; int hit_points = 0; + float kill_timer = 0; + + bool idle2_anim = false; + bool stealth_active = false; + + bool shield_active = false; + + bool freeze_ = false; + + bool chromatic_aberration_active_ = false; + eng::ImageQuad sprite; eng::ImageQuad target; eng::ImageQuad blast; + eng::ImageQuad shield; eng::ImageQuad score; eng::SolidQuad health_base; eng::SolidQuad health_bar; @@ -67,38 +94,72 @@ class Enemy { eng::Animator sprite_animator; eng::Animator target_animator; eng::Animator blast_animator; + eng::Animator shield_animator; eng::Animator health_animator; eng::Animator score_animator; - eng::SoundPlayer explosion_; + eng::SoundPlayer spawn; + eng::SoundPlayer explosion; + eng::SoundPlayer stealth; + eng::SoundPlayer shield_on; + eng::SoundPlayer hit; }; + std::shared_ptr chromatic_aberration_; + float chromatic_aberration_offset_ = 0; + + eng::ImageQuad boss_; + eng::Animator boss_animator_; + eng::SoundPlayer boss_intro_; + + std::shared_ptr boss_intro_sound_; + std::shared_ptr boss_explosion_sound_; std::shared_ptr explosion_sound_; + std::shared_ptr stealth_sound_; + std::shared_ptr shield_on_sound_; + std::shared_ptr hit_sound_; + std::shared_ptr power_up_spawn_sound_; + std::shared_ptr power_up_pick_sound_; std::list enemies_; int num_enemies_killed_in_current_wave_ = 0; - std::array seconds_since_last_spawn_ = {0, 0, 0}; - std::array seconds_to_next_spawn_ = {0, 0, 0}; + std::array seconds_since_last_spawn_ = { + 0, 0, 0, 0}; + std::array seconds_to_next_spawn_ = {0, 0, 0, + 0}; float spawn_factor_ = 0; - float spawn_factor_interpolator_ = 0; - bool waiting_for_next_wave_ = false; + float boss_spawn_time_ = 0; + float boss_spawn_time_factor_ = 0; + + float seconds_since_last_power_up_ = 0; + float seconds_to_next_power_up_ = 0; + + bool progress_paused_ = true; int last_spawn_col_ = 0; - bool paused_ = false; + int wave_ = 0; + bool boss_fight_ = false; + + bool CheckSpawnPos(base::Vector2f pos, SpeedType speed_type); + bool CheckTeleportPos(EnemyUnit* enemy); + + void SpawnUnit(EnemyType enemy_type, + DamageType damage_type, + const base::Vector2f& pos, + float speed, + SpeedType speed_type = kSpeedType_Invalid); + + void SpawnBoss(); void TakeDamage(EnemyUnit* target, int damage); - void SpawnNextEnemy(); - - void Spawn(EnemyType enemy_type, - DamageType damage_type, - const base::Vector2f& pos, - float speed); + void UpdateWave(float delta_time); + void UpdateBoss(float delta_time); EnemyUnit* GetTarget(DamageType damage_type); @@ -107,6 +168,9 @@ class Enemy { std::unique_ptr GetScoreImage(EnemyType enemy_type); bool CreateRenderResources(); + bool CreateShaders(); + + void TranslateEnemyUnit(EnemyUnit& e, const base::Vector2f& delta); }; #endif // ENEMY_H diff --git a/src/demo/hud.cc b/src/demo/hud.cc index e5f1da9..1d8e16a 100644 --- a/src/demo/hud.cc +++ b/src/demo/hud.cc @@ -22,6 +22,17 @@ const Vector4f kPprogressBarColor[2] = {{0.256f, 0.434f, 0.72f, 1}, {0.905f, 0.493f, 0.194f, 1}}; const Vector4f kTextColor = {0.895f, 0.692f, 0.24f, 1}; +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 Hud::Hud() = default; @@ -38,6 +49,10 @@ bool Hud::Initialize() { Engine::Get().SetImageSource("text0", std::bind(&Hud::CreateScoreImage, this)); Engine::Get().SetImageSource("text1", std::bind(&Hud::CreateWaveImage, this)); + Engine::Get().SetImageSource("message", + std::bind(&Hud::CreateMessageImage, this)); + Engine::Get().SetImageSource("bonus_tex", + std::bind(&Hud::CreateBonusImage, this)); for (int i = 0; i < 2; ++i) { text_[i].Create("text"s + std::to_string(i)); @@ -71,22 +86,81 @@ bool Hud::Initialize() { text_animator_[i].Attach(&text_[i]); } + message_.Create("message"); + message_.SetZOrder(30); + + message_animator_.SetEndCallback(Animator::kTimer, [&]() -> void { + message_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + message_animator_.SetVisible(false); + }); + message_animator_.SetBlending({1, 1, 1, 0}, 0.5f); + message_animator_.Play(Animator::kBlending, false); + }); + message_animator_.Attach(&message_); + + bonus_.Create("bonus_tex"); + bonus_.SetZOrder(30); + + SetupFadeOutAnim(bonus_animator_, 1.0f); + bonus_animator_.SetMovement({0, Engine::Get().GetScreenSize().y / 2}, 2.0f); + bonus_animator_.Attach(&bonus_); + return true; } +void Hud::Pause(bool pause) { + message_animator_.PauseOrResumeAll(pause); + bonus_animator_.PauseOrResumeAll(pause); +} + void Hud::Show() { - if (text_[0].IsVisible()) + if (text_[0].IsVisible() && text_[1].IsVisible() && + progress_bar_[0].IsVisible() && progress_bar_[1].IsVisible()) return; for (int i = 0; i < 2; ++i) { progress_bar_[i].SetVisible(true); text_[i].SetVisible(true); - progress_bar_animator_[i].SetBlending(kPprogressBarColor[i], 0.3f); + progress_bar_animator_[i].SetBlending(kPprogressBarColor[i], 0.5f); progress_bar_animator_[i].Play(Animator::kBlending, false); } } -void Hud::SetScore(int score, bool flash) { +void Hud::Hide() { + if (!text_[0].IsVisible() && !text_[1].IsVisible() && + !progress_bar_[0].IsVisible() && !progress_bar_[1].IsVisible()) + return; + + for (int i = 0; i < 2; ++i) { + text_animator_[i].SetEndCallback(Animator::kBlending, [&, i]() -> void { + text_animator_[i].SetEndCallback(Animator::kBlending, nullptr); + text_[i].SetVisible(false); + }); + text_animator_[i].SetBlending(kTextColor * Vector4f(1, 1, 1, 0), 0.5f); + text_animator_[i].Play(Animator::kBlending, false); + } + + HideProgress(); +} + +void Hud::HideProgress() { + if (!progress_bar_[0].IsVisible()) + return; + + for (int i = 0; i < 2; ++i) { + progress_bar_animator_[i].SetEndCallback( + Animator::kBlending, [&, i]() -> void { + progress_bar_animator_[i].SetEndCallback(Animator::kBlending, + nullptr); + progress_bar_animator_[1].SetVisible(false); + }); + progress_bar_animator_[i].SetBlending( + kPprogressBarColor[i] * Vector4f(1, 1, 1, 0), 0.5f); + progress_bar_animator_[i].Play(Animator::kBlending, false); + } +} + +void Hud::SetScore(size_t score, bool flash) { last_score_ = score; Engine::Get().RefreshImage("text0"); @@ -104,7 +178,8 @@ void Hud::SetWave(int wave, bool flash) { if (flash) { text_animator_[1].SetEndCallback(Animator::kBlending, text_animator_cb_[1]); - text_animator_[1].SetBlending({1, 1, 1, 1}, 0.08f); + text_animator_[1].SetBlending( + {1, 1, 1, 1}, 0.1f, std::bind(Acceleration, std::placeholders::_1, 1)); text_animator_[1].Play(Animator::kBlending, false); } } @@ -118,6 +193,34 @@ void Hud::SetProgress(float progress) { progress_bar_[1].Translate({t, 0}); } +void Hud::ShowMessage(const std::string& text, float duration) { + message_text_ = text; + Engine::Get().RefreshImage("message"); + + message_.AutoScale(); + message_.Scale(1.5f); + message_.SetColor({1, 1, 1, 0}); + message_.SetVisible(true); + + message_animator_.SetEndCallback( + Animator::kBlending, [&, duration]() -> void { + message_animator_.SetTimer(duration); + message_animator_.Play(Animator::kTimer, false); + }); + message_animator_.SetBlending({1, 1, 1, 1}, 0.5f); + message_animator_.Play(Animator::kBlending, false); +} + +void Hud::ShowBonus(size_t bonus) { + bonus_score_ = bonus; + Engine::Get().RefreshImage("bonus_tex"); + bonus_.AutoScale(); + bonus_.Scale(1.3f); + bonus_.SetColor({1, 1, 1, 1}); + bonus_.SetVisible(true); + bonus_animator_.Play(Animator::kTimer | Animator::kMovement, false); +} + std::unique_ptr Hud::CreateScoreImage() { return Print(0, std::to_string(last_score_)); } @@ -126,6 +229,45 @@ std::unique_ptr Hud::CreateWaveImage() { return Print(1, "wave "s + std::to_string(last_wave_)); } +std::unique_ptr Hud::CreateMessageImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + auto image = std::make_unique(); + image->Create(max_text_width_, font.GetLineHeight()); + image->GradientV({0.80f, 0.87f, 0.93f, 0}, {0.003f, 0.91f, 0.99f, 0}, + font.GetLineHeight()); + + int w, h; + font.CalculateBoundingBox(message_text_.c_str(), w, h); + float x = (image->GetWidth() - w) / 2; + + font.Print(x, 0, message_text_.c_str(), image->GetBuffer(), + image->GetWidth()); + image->Compress(); + + return image; +} + +std::unique_ptr Hud::CreateBonusImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + if (bonus_score_ == 0) + return nullptr; + + std::string text = std::to_string(bonus_score_); + 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; +} + std::unique_ptr Hud::Print(int i, const std::string& text) { const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); diff --git a/src/demo/hud.h b/src/demo/hud.h index d425895..50269d3 100644 --- a/src/demo/hud.h +++ b/src/demo/hud.h @@ -20,28 +20,46 @@ class Hud { bool Initialize(); - void Show(); + void Pause(bool pause); - void SetScore(int score, bool flash); + void Show(); + void Hide(); + void HideProgress(); + + void SetScore(size_t score, bool flash); void SetWave(int wave, bool flash); void SetProgress(float progress); + void ShowMessage(const std::string& text, float duration); + + void ShowBonus(size_t bonus); + private: eng::SolidQuad progress_bar_[2]; eng::ImageQuad text_[2]; + eng::ImageQuad message_; + eng::ImageQuad bonus_; eng::Animator progress_bar_animator_[2]; eng::Animator text_animator_[2]; + eng::Animator message_animator_; base::Closure text_animator_cb_[2]; + eng::Animator bonus_animator_; int max_text_width_ = 0; - int last_score_ = 0; + size_t last_score_ = 0; int last_wave_ = 0; float last_progress_ = 0; + std::string message_text_; + + size_t bonus_score_ = 0; + std::unique_ptr CreateScoreImage(); std::unique_ptr CreateWaveImage(); + std::unique_ptr CreateMessageImage(); + std::unique_ptr CreateBonusImage(); std::unique_ptr Print(int i, const std::string& text); diff --git a/src/demo/menu.cc b/src/demo/menu.cc index 28aede8..ae564c5 100644 --- a/src/demo/menu.cc +++ b/src/demo/menu.cc @@ -1,6 +1,7 @@ #include "menu.h" #include +#include #include #include "../base/collusion_test.h" @@ -10,25 +11,39 @@ #include "../engine/font.h" #include "../engine/image.h" #include "../engine/input_event.h" +#include "../engine/sound.h" #include "demo.h" +using namespace std::string_literals; + using namespace base; using namespace eng; namespace { +constexpr char kVersionStr[] = "Version 1.0.1"; + constexpr char kMenuOption[Menu::kOption_Max][10] = {"continue", "start", "credits", "exit"}; constexpr float kMenuOptionSpace = 1.5f; const Vector4f kColorNormal = {1, 1, 1, 1}; -const Vector4f kColorHighlight = {5, 5, 5, 1}; +const Vector4f kColorHighlight = {20, 20, 20, 1}; constexpr float kBlendingSpeed = 0.12f; +const Vector4f kColorSwitch[2] = {{0.003f, 0.91f, 0.99f, 1}, + {0.33f, 0.47, 0.51f, 1}}; + const Vector4f kColorFadeOut = {1, 1, 1, 0}; constexpr float kFadeSpeed = 0.2f; +const Vector4f kHighScoreColor = {0.895f, 0.692f, 0.24f, 1}; + +const char kLastWave[] = "last_wave"; + +constexpr int kMaxStartWave = 10; + } // namespace Menu::Menu() = default; @@ -36,7 +51,13 @@ Menu::Menu() = default; Menu::~Menu() = default; bool Menu::Initialize() { - const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + click_sound_ = std::make_shared(); + if (!click_sound_->Load("menu_click.mp3", false)) + return false; + + Demo* game = static_cast(Engine::Get().GetGame()); + + const Font& font = game->GetFont(); max_text_width_ = -1; for (int i = 0; i < kOption_Max; ++i) { @@ -51,31 +72,166 @@ bool Menu::Initialize() { for (int i = 0; i < kOption_Max; ++i) { items_[i].text.Create("menu_tex", {1, 4}); - items_[i].text.SetZOrder(40); + items_[i].text.SetZOrder(41); items_[i].text.Scale(1.5f); items_[i].text.SetColor(kColorFadeOut); items_[i].text.SetVisible(false); items_[i].text.SetFrame(i); items_[i].select_item_cb_ = [&, i]() -> void { - items_[i].text_animator.SetEndCallback( - Animator::kBlending, [&, i]() -> void { - items_[i].text_animator.SetEndCallback(Animator::kBlending, - nullptr); - selected_option_ = (Option)i; - }); - items_[i].text_animator.SetBlending(kColorNormal, kBlendingSpeed); - items_[i].text_animator.Play(Animator::kBlending, false); + items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr); + selected_option_ = (Option)i; }; items_[i].text_animator.Attach(&items_[i].text); } // Get the item positions calculated. SetOptionEnabled(kContinue, true); + click_.SetSound(click_sound_); + click_.SetVariate(false); + click_.SetSimulateStereo(false); + click_.SetMaxAplitude(1.5f); + + logo_[0].Create("logo_tex0", {3, 8}); + logo_[0].SetZOrder(41); + logo_[0].SetPosition(Engine::Get().GetScreenSize() * Vector2f(0, 0.35f)); + + logo_[1].Create("logo_tex1", {3, 7}); + logo_[1].SetZOrder(41); + logo_[1].SetPosition(Engine::Get().GetScreenSize() * Vector2f(0, 0.35f)); + + logo_animator_[0].Attach(&logo_[0]); + logo_animator_[0].SetFrames(24, 20); + logo_animator_[0].SetEndCallback(Animator::kFrames, [&]() -> void { + logo_[0].SetVisible(false); + logo_[1].SetVisible(true); + logo_[1].SetFrame(0); + logo_animator_[1].SetFrames(12, 20); + logo_animator_[1].SetTimer( + Lerp(3.0f, 8.0f, Engine::Get().GetRandomGenerator().GetFloat())); + logo_animator_[1].Play(Animator::kFrames | Animator::kTimer, true); + }); + + logo_animator_[1].Attach(&logo_[1]); + logo_animator_[1].SetEndCallback(Animator::kTimer, [&]() -> void { + logo_animator_[1].Stop(Animator::kFrames); + logo_[1].SetFrame(12); + logo_animator_[1].SetFrames(9, 30); + logo_animator_[1].SetTimer( + Lerp(3.0f, 8.0f, Engine::Get().GetRandomGenerator().GetFloat())); + logo_animator_[1].Play(Animator::kFrames | Animator::kTimer, false); + }); + logo_animator_[1].SetEndCallback(Animator::kFrames, [&]() -> void { + logo_[1].SetFrame(0); + logo_animator_[1].SetFrames(12, 20); + logo_animator_[1].Play(Animator::kFrames, true); + }); + + toggle_audio_.Create( + "buttons_tex", {4, 2}, 2, 6, + [&] { + Engine::Get().SetEnableAudio(toggle_audio_.enabled()); + + Demo* game = static_cast(Engine::Get().GetGame()); + if (toggle_audio_.enabled()) { + if (toggle_music_.enabled()) + game->SetEnableMusic(true); + else + click_.Play(false); + } else { + game->SetEnableMusic(false); + } + game->saved_data().root()["audio"] = toggle_audio_.enabled(); + }, + true, game->saved_data().root().get("audio", Json::Value(true)).asBool()); + toggle_audio_.image().SetPosition(Engine::Get().GetScreenSize() * + Vector2f(0, -0.25f)); + toggle_audio_.image().Scale(0.7f); + + toggle_music_.Create( + "buttons_tex", {4, 2}, 1, 5, + [&] { + Demo* game = static_cast(Engine::Get().GetGame()); + game->SetEnableMusic(toggle_music_.enabled()); + game->saved_data().root()["music"] = toggle_music_.enabled(); + }, + true, game->saved_data().root().get("music", Json::Value(true)).asBool()); + toggle_music_.image().SetPosition(Engine::Get().GetScreenSize() * + Vector2f(0, -0.25f)); + toggle_music_.image().Scale(0.7f); + + toggle_vibration_.Create( + "buttons_tex", {4, 2}, 3, 7, + [&] { + Engine::Get().SetEnableVibration(toggle_vibration_.enabled()); + if (toggle_vibration_.enabled()) + Engine::Get().Vibrate(50); + Demo* game = static_cast(Engine::Get().GetGame()); + game->saved_data().root()["vibration"] = toggle_vibration_.enabled(); + }, + true, + game->saved_data().root().get("vibration", Json::Value(true)).asBool()); + toggle_vibration_.image().SetPosition(Engine::Get().GetScreenSize() * + Vector2f(0, -0.25f)); + toggle_vibration_.image().Scale(0.7f); + + toggle_audio_.image().PlaceToLeftOf(toggle_music_.image()); + toggle_audio_.image().Translate({toggle_music_.image().GetSize().x / -2, 0}); + toggle_vibration_.image().PlaceToRightOf(toggle_music_.image()); + toggle_vibration_.image().Translate( + {toggle_music_.image().GetSize().x / 2, 0}); + + high_score_value_ = game->GetHighScore(); + + high_score_.Create("high_score_tex"); + high_score_.SetZOrder(41); + high_score_.Scale(0.8f); + high_score_.SetPosition(Engine::Get().GetScreenSize() * Vector2f(0, 0.225f)); + high_score_.SetColor(kHighScoreColor * Vector4f(1, 1, 1, 0)); + high_score_.SetVisible(false); + + high_score_animator_.Attach(&high_score_); + + version_.Create("version_tex"); + version_.SetZOrder(41); + version_.Scale(0.6f); + version_.SetPosition(Engine::Get().GetScreenSize() * Vector2f(0, -0.5f) + + version_.GetSize() * Vector2f(0, 2)); + version_.SetColor(kHighScoreColor * Vector4f(1, 1, 1, 0)); + version_.SetVisible(false); + + version_animator_.Attach(&version_); + + start_from_wave_ = 1; + starting_wave_.Create("starting_wave"); + + wave_up_.Create( + "wave_up_tex", {1, 1}, 0, 0, + [&] { + Demo* game = static_cast(Engine::Get().GetGame()); + start_from_wave_ += 3; + if (start_from_wave_ > game->saved_data() + .root() + .get(kLastWave, Json::Value(1)) + .asInt() || + start_from_wave_ > kMaxStartWave) + start_from_wave_ = 1; + starting_wave_.image().SetFrame(start_from_wave_ / 3); + click_.Play(false); + }, + false, true); + wave_up_.image().Scale(1.5f); + return true; } void Menu::OnInputEvent(std::unique_ptr event) { + if (toggle_audio_.OnInputEvent(event.get()) || + toggle_music_.OnInputEvent(event.get()) || + toggle_vibration_.OnInputEvent(event.get()) || + (wave_up_.image().IsVisible() && wave_up_.OnInputEvent(event.get()))) + return; + if (event->GetType() == InputEvent::kDragStart) { tap_pos_[0] = event->GetVector(); tap_pos_[1] = event->GetVector(); @@ -102,6 +258,8 @@ void Menu::OnInputEvent(std::unique_ptr event) { items_[i].select_item_cb_); items_[i].text_animator.SetBlending(kColorHighlight, kBlendingSpeed); items_[i].text_animator.Play(Animator::kBlending, false); + + click_.Play(false); } } @@ -135,6 +293,40 @@ void Menu::SetOptionEnabled(Option o, bool enable) { } void Menu::Show() { + logo_[1].SetColor(kColorNormal); + logo_animator_[0].SetVisible(true); + logo_animator_[0].SetBlending(kColorNormal, kFadeSpeed); + logo_animator_[0].Play(Animator::kBlending | Animator::kFrames, false); + + if (high_score_value_ != + static_cast(Engine::Get().GetGame())->GetHighScore()) { + high_score_value_ = + static_cast(Engine::Get().GetGame())->GetHighScore(); + Engine::Get().RefreshImage("high_score_tex"); + + high_score_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + high_score_animator_.SetBlending(kColorFadeOut, 0.3f); + high_score_animator_.SetTimer(5); + high_score_animator_.Play(Animator::kBlending | Animator::kTimer, true); + }); + high_score_animator_.SetEndCallback(Animator::kTimer, [&]() -> void { + high_score_animator_.Play(Animator::kBlending | Animator::kTimer, false); + high_score_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + high_score_animator_.Stop(Animator::kBlending); + }); + high_score_animator_.SetEndCallback(Animator::kTimer, nullptr); + }); + } + if (high_score_value_ > 0) { + high_score_animator_.SetVisible(true); + high_score_animator_.SetBlending(kHighScoreColor, kFadeSpeed); + high_score_animator_.Play(Animator::kBlending, false); + } + + version_animator_.SetVisible(true); + version_animator_.SetBlending(kHighScoreColor, kFadeSpeed); + version_animator_.Play(Animator::kBlending, false); + for (int i = 0; i < kOption_Max; ++i) { if (items_[i].hide) continue; @@ -146,9 +338,54 @@ void Menu::Show() { items_[i].text_animator.Play(Animator::kBlending, false); items_[i].text.SetVisible(true); } + + toggle_audio_.Show(); + toggle_music_.Show(); + toggle_vibration_.Show(); + + Demo* game = static_cast(Engine::Get().GetGame()); + + if (!items_[kNewGame].hide && + game->saved_data().root().get(kLastWave, Json::Value(1)).asInt() > 3) { + wave_up_.image().SetPosition(items_[1].text.GetPosition()); + wave_up_.image().PlaceToRightOf(items_[1].text); + starting_wave_.image().SetPosition(wave_up_.image().GetPosition()); + starting_wave_.Show(); + wave_up_.Show(); + } } -void Menu::Hide() { +void Menu::Hide(Closure cb) { + for (int i = 0; i < 2; ++i) { + logo_animator_[i].Stop(Animator::kAllAnimations | Animator::kTimer); + logo_animator_[i].SetBlending(kColorFadeOut, kFadeSpeed); + logo_animator_[i].SetEndCallback(Animator::kBlending, [&, i, cb]() -> void { + logo_animator_[i].Stop(Animator::kAllAnimations | Animator::kTimer); + logo_animator_[i].SetEndCallback(Animator::kBlending, nullptr); + logo_animator_[i].SetVisible(false); + if (i == 0 && cb) + cb(); + }); + logo_animator_[i].Play(Animator::kBlending, false); + } + + high_score_animator_.Stop(Animator::kAllAnimations | Animator::kTimer); + high_score_animator_.SetEndCallback(Animator::kTimer, nullptr); + high_score_animator_.SetBlending(kColorFadeOut, kFadeSpeed); + high_score_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + high_score_animator_.SetEndCallback(Animator::kBlending, nullptr); + high_score_animator_.SetVisible(false); + }); + high_score_animator_.Play(Animator::kBlending, false); + + version_animator_.Stop(Animator::kAllAnimations); + version_animator_.SetBlending(kColorFadeOut, kFadeSpeed); + version_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + version_animator_.SetEndCallback(Animator::kBlending, nullptr); + version_animator_.SetVisible(false); + }); + version_animator_.Play(Animator::kBlending, false); + selected_option_ = kOption_Invalid; for (int i = 0; i < kOption_Max; ++i) { if (items_[i].hide) @@ -161,15 +398,64 @@ void Menu::Hide() { items_[i].text_animator.SetBlending(kColorFadeOut, kFadeSpeed); items_[i].text_animator.Play(Animator::kBlending, false); } + + toggle_audio_.Hide(); + toggle_music_.Hide(); + toggle_vibration_.Hide(); + + if (starting_wave_.image().IsVisible()) { + starting_wave_.Hide(); + wave_up_.Hide(); + } } bool Menu::CreateRenderResources() { - Engine::Get().SetImageSource("menu_tex", std::bind(&Menu::CreateImage, this)); + Engine::Get().SetImageSource("menu_tex", + std::bind(&Menu::CreateMenuImage, this)); + Engine::Get().SetImageSource("logo_tex0", "woom_logo_start_frames_01.png"); + Engine::Get().SetImageSource("logo_tex1", "woom_logo_start_frames_02-03.png"); + Engine::Get().SetImageSource("buttons_tex", "menu_icons.png"); + Engine::Get().SetImageSource("high_score_tex", + std::bind(&Menu::CreateHighScoreImage, this)); + + Engine::Get().SetImageSource("wave_up_tex", []() -> std::unique_ptr { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + constexpr char btn_text[] = "[ ]"; + + int w, h; + font.CalculateBoundingBox(btn_text, w, h); + + auto image = std::make_unique(); + image->Create(w, h); + image->Clear({1, 1, 1, 0}); + + font.Print(0, 0, btn_text, image->GetBuffer(), image->GetWidth()); + + image->Compress(); + return image; + }); + + Engine::Get().SetImageSource("version_tex", []() -> std::unique_ptr { + const Font* font = Engine::Get().GetSystemFont(); + + int w, h; + font->CalculateBoundingBox(kVersionStr, w, h); + + auto image = std::make_unique(); + image->Create(w, font->GetLineHeight()); + image->Clear({1, 1, 1, 0}); + + font->Print(0, 0, kVersionStr, image->GetBuffer(), image->GetWidth()); + + image->Compress(); + return image; + }); return true; } -std::unique_ptr Menu::CreateImage() { +std::unique_ptr Menu::CreateMenuImage() { const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); int line_height = font.GetLineHeight() + 1; @@ -177,7 +463,8 @@ std::unique_ptr Menu::CreateImage() { image->Create(max_text_width_, line_height * kOption_Max); // Fill the area of each menu item with gradient. - image->GradientV({1.0f, 1.0f, 1.0f, 0}, {.0f, .0f, 1.0f, 0}, line_height); + image->GradientV({0.80f, 0.87f, 0.93f, 0}, + kColorSwitch[0] * Vector4f(1, 1, 1, 0), line_height); for (int i = 0; i < kOption_Max; ++i) { int w, h; @@ -187,6 +474,23 @@ std::unique_ptr Menu::CreateImage() { font.Print(x, y, kMenuOption[i], image->GetBuffer(), image->GetWidth()); } + image->Compress(); + return image; +} + +std::unique_ptr Menu::CreateHighScoreImage() { + std::string text = "High Score: "s + std::to_string(high_score_value_); + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int width, height; + font.CalculateBoundingBox(text, width, height); + + auto image = std::make_unique(); + image->Create(width, height); + image->Clear({1, 1, 1, 0}); + font.Print(0, 0, text, image->GetBuffer(), image->GetWidth()); + + image->Compress(); return image; } @@ -197,3 +501,141 @@ bool Menu::IsAnimating() { } return false; } + +// +// Menu::Button implementation +// + +void Menu::Button::Create(const std::string& asset_name, + std::array num_frames, + int frame1, + int frame2, + Closure pressed_cb, + bool switch_control, + bool enabled) { + frame1_ = frame1; + frame2_ = frame2; + pressed_cb_ = std::move(pressed_cb); + switch_control_ = switch_control; + enabled_ = enabled; + + image_.Create(asset_name, num_frames); + image_.SetFrame(enabled ? frame1 : frame2); + image_.SetColor(kColorFadeOut); + image_.SetZOrder(41); + image_.SetVisible(false); + + animator_.Attach(&image_); +} + +bool Menu::Button::OnInputEvent(eng::InputEvent* event) { + if (event->GetType() == InputEvent::kDragStart) { + tap_pos_[0] = event->GetVector(); + tap_pos_[1] = event->GetVector(); + } else if (event->GetType() == InputEvent::kDrag) { + tap_pos_[1] = event->GetVector(); + } + + if (event->GetType() != InputEvent::kDragEnd) + return false; + + if (!Intersection(image_.GetPosition(), image_.GetSize() * Vector2f(1.2f, 2), + tap_pos_[0])) + return false; + if (!Intersection(image_.GetPosition(), image_.GetSize() * Vector2f(1.2f, 2), + tap_pos_[1])) + return false; + + SetEnabled(!enabled_); + pressed_cb_(); + + return true; +} + +void Menu::Button::Show() { + animator_.SetVisible(true); + animator_.SetBlending(enabled_ ? kColorSwitch[0] : kColorSwitch[1], + kBlendingSpeed); + animator_.Play(Animator::kBlending, false); + animator_.SetEndCallback(Animator::kBlending, nullptr); +} + +void Menu::Button::Hide() { + animator_.SetBlending(kColorFadeOut, kBlendingSpeed); + animator_.Play(Animator::kBlending, false); + animator_.SetEndCallback(Animator::kBlending, + [&]() -> void { animator_.SetVisible(false); }); +} + +void Menu::Button::SetEnabled(bool enable) { + if (switch_control_) { + enabled_ = enable; + image_.SetFrame(enabled_ ? frame1_ : frame2_); + image_.SetColor(enabled_ ? kColorSwitch[0] : kColorSwitch[1]); + } +} + +// +// Menu::Radio implementation +// + +void Menu::Radio::Create(const std::string& asset_name) { + Engine::Get().SetImageSource(asset_name, + std::bind(&Radio::CreateImage, this)); + + options_.Create(asset_name, {1, (kMaxStartWave + 2) / 3}); + options_.SetZOrder(41); + options_.SetColor(kColorFadeOut); + options_.SetFrame(0); + options_.SetVisible(false); + + animator_.Attach(&options_); +} + +bool Menu::Radio::OnInputEvent(eng::InputEvent* event) { + return false; +} + +void Menu::Radio::Show() { + animator_.SetVisible(true); + animator_.SetBlending(kHighScoreColor, kBlendingSpeed); + animator_.Play(Animator::kBlending, false); + animator_.SetEndCallback(Animator::kBlending, nullptr); +} + +void Menu::Radio::Hide() { + animator_.SetBlending(kColorFadeOut, kBlendingSpeed); + animator_.Play(Animator::kBlending, false); + animator_.SetEndCallback(Animator::kBlending, + [&]() -> void { animator_.SetVisible(false); }); +} + +std::unique_ptr Menu::Radio::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int max_width = 0; + for (int i = 1; i <= kMaxStartWave; i += 3) { + int w, h; + font.CalculateBoundingBox(std::to_string(i), w, h); + if (w > max_width) + max_width = w; + } + + int line_height = font.GetLineHeight() + 1; + + auto image = std::make_unique(); + image->Create(max_width, line_height * ((kMaxStartWave + 2) / 3)); + image->Clear({1, 1, 1, 0}); + + for (int i = 1, j = 0; i <= kMaxStartWave; i += 3) { + int w, h; + font.CalculateBoundingBox(std::to_string(i), w, h); + float x = (image->GetWidth() - w) / 2; + float y = line_height * j++; + font.Print(x, y, std::to_string(i), image->GetBuffer(), image->GetWidth()); + } + + image->Compress(); + + return image; +} diff --git a/src/demo/menu.h b/src/demo/menu.h index 66dbbd0..60cefa6 100644 --- a/src/demo/menu.h +++ b/src/demo/menu.h @@ -8,10 +8,12 @@ #include "../base/vecmath.h" #include "../engine/animator.h" #include "../engine/image_quad.h" +#include "../engine/sound_player.h" namespace eng { class Image; class InputEvent; +class Sound; } // namespace eng class Menu { @@ -35,11 +37,70 @@ class Menu { void SetOptionEnabled(Option o, bool enable); void Show(); - void Hide(); + void Hide(base::Closure cb = nullptr); Option selected_option() const { return selected_option_; } + int start_from_wave() { return start_from_wave_; } + private: + class Button { + public: + Button() = default; + ~Button() = default; + + void Create(const std::string& asset_name, + std::array num_frames, + int frame1, + int frame2, + base::Closure pressed_cb, + bool switch_control, + bool enabled); + + bool OnInputEvent(eng::InputEvent* event); + + void Show(); + void Hide(); + + eng::ImageQuad& image() { return image_; }; + + bool enabled() const { return enabled_; } + + private: + eng::ImageQuad image_; + eng::Animator animator_; + int frame1_ = 0; + int frame2_ = 0; + base::Closure pressed_cb_; + + bool switch_control_ = false; + bool enabled_ = false; + base::Vector2f tap_pos_[2] = {{0, 0}, {0, 0}}; + + void SetEnabled(bool enable); + }; + + class Radio { + public: + Radio() = default; + ~Radio() = default; + + void Create(const std::string& asset_name); + + bool OnInputEvent(eng::InputEvent* event); + + void Show(); + void Hide(); + + eng::ImageQuad& image() { return options_; }; + + private: + eng::ImageQuad options_; + eng::Animator animator_; + + std::unique_ptr CreateImage(); + }; + struct Item { eng::ImageQuad text; eng::Animator text_animator; @@ -47,6 +108,13 @@ class Menu { bool hide = false; }; + eng::ImageQuad logo_[2]; + eng::Animator logo_animator_[2]; + + std::shared_ptr click_sound_; + + eng::SoundPlayer click_; + Item items_[kOption_Max]; int max_text_width_ = 0; @@ -55,9 +123,28 @@ class Menu { base::Vector2f tap_pos_[2] = {{0, 0}, {0, 0}}; + Button toggle_audio_; + Button toggle_music_; + Button toggle_vibration_; + + int high_score_value_ = 0; + + eng::ImageQuad high_score_; + eng::Animator high_score_animator_; + + eng::ImageQuad version_; + eng::Animator version_animator_; + + int start_from_wave_ = 1; + + Radio starting_wave_; + Button wave_up_; + Button wave_down_; + bool CreateRenderResources(); - std::unique_ptr CreateImage(); + std::unique_ptr CreateMenuImage(); + std::unique_ptr CreateHighScoreImage(); bool IsAnimating(); }; diff --git a/src/demo/player.cc b/src/demo/player.cc index 674bea8..c194d60 100644 --- a/src/demo/player.cc +++ b/src/demo/player.cc @@ -1,9 +1,11 @@ #include "player.h" +#include "../base/interpolation.h" #include "../base/log.h" #include "../engine/engine.h" -#include "../engine/image.h" +#include "../engine/font.h" #include "../engine/input_event.h" +#include "../engine/sound.h" #include "demo.h" using namespace base; @@ -17,6 +19,9 @@ constexpr int wepon_cooldown_frame[] = {5, 13}; constexpr int wepon_cooldown_frame_count = 3; constexpr int wepon_anim_speed = 48; +const Vector4f kNukeColor[2] = {{0.16f, 0.46f, 0.93f, 0}, + {0.93f, 0.35f, 0.15f, 1}}; + } // namespace Player::Player() = default; @@ -26,13 +31,78 @@ Player::~Player() = default; bool Player::Initialize() { if (!CreateRenderResources()) return false; + + laser_shot_sound_ = std::make_shared(); + if (!laser_shot_sound_->Load("laser.mp3", false)) + return false; + + nuke_explosion_sound_ = std::make_shared(); + if (!nuke_explosion_sound_->Load("nuke.mp3", false)) + return false; + + no_nuke_sound_ = std::make_shared(); + if (!no_nuke_sound_->Load("no_nuke.mp3", false)) + return false; + SetupWeapons(); + + Vector2f hb_pos = Engine::Get().GetScreenSize() / Vector2f(2, -2) + + Vector2f(0, weapon_[0].GetSize().y * 0.3f); + + for (int i = 0; i < 3; ++i) { + health_bead_[i].Create("health_bead", {1, 2}); + health_bead_[i].SetZOrder(25); + health_bead_[i].Translate(hb_pos * Vector2f(0, 1)); + health_bead_[i].SetVisible(true); + } + health_bead_[0].PlaceToLeftOf(health_bead_[1]); + health_bead_[0].Translate(health_bead_[1].GetSize() * Vector2f(-0.1f, 0)); + health_bead_[2].PlaceToRightOf(health_bead_[1]); + health_bead_[2].Translate(health_bead_[1].GetSize() * Vector2f(0.1f, 0)); + + nuke_symbol_.Create("nuke_symbol_tex", {5, 1}); + nuke_symbol_.SetZOrder(29); + nuke_symbol_.SetPosition({0, weapon_[0].GetPosition().y}); + nuke_symbol_.SetFrame(4); + nuke_symbol_.SetVisible(true); + + nuke_.SetZOrder(20); + nuke_.SetSize(Engine::Get().GetScreenSize()); + nuke_.SetColor(kNukeColor[0]); + + nuke_animator_.Attach(&nuke_); + + nuke_symbol_animator_.Attach(&nuke_symbol_); + + nuke_explosion_.SetSound(nuke_explosion_sound_); + nuke_explosion_.SetVariate(false); + nuke_explosion_.SetSimulateStereo(false); + nuke_explosion_.SetMaxAplitude(0.8f); + + no_nuke_.SetSound(no_nuke_sound_); + return true; } void Player::Update(float delta_time) { - if (active_weapon_ != kDamageType_Invalid) - UpdateTarget(); + for (int i = 0; i < 2; ++i) { + if (drag_weapon_[i] != kDamageType_Invalid) + UpdateTarget(drag_weapon_[i]); + } + +#if defined(LOAD_TEST) + Enemy& enemy = static_cast(Engine::Get().GetGame())->GetEnemy(); + if (enemy.num_enemies_killed_in_current_wave() == 40) + Nuke(); + + DamageType type = + (DamageType)(Engine::Get().GetRandomGenerator().Roll(2) - 1); + if (!IsFiring(type)) { + DragStart(type, GetWeaponPos(type)); + Drag(type, {0, 0}); + DragEnd(type); + } +#endif } void Player::Pause(bool pause) { @@ -42,19 +112,59 @@ void Player::Pause(bool pause) { beam_animator_[i].PauseOrResumeAll(pause); spark_animator_[i].PauseOrResumeAll(pause); } + nuke_animator_.PauseOrResumeAll(pause); + nuke_symbol_animator_.PauseOrResumeAll(pause); } void Player::OnInputEvent(std::unique_ptr event) { if (event->GetType() == InputEvent::kNavigateBack) NavigateBack(); else if (event->GetType() == InputEvent::kDragStart) - DragStart(event->GetVector()); + DragStart(event->GetPointerId(), event->GetVector()); else if (event->GetType() == InputEvent::kDrag) - Drag(event->GetVector()); + Drag(event->GetPointerId(), event->GetVector()); else if (event->GetType() == InputEvent::kDragEnd) - DragEnd(); + DragEnd(event->GetPointerId()); else if (event->GetType() == InputEvent::kDragCancel) - DragCancel(); + DragCancel(event->GetPointerId()); +} + +void Player::TakeDamage(int damage) { + if (damage > 0) + Engine::Get().Vibrate(250); + + hit_points_ = std::min(total_health_, std::max(0, hit_points_ - damage)); + + for (int i = 0; i < 3; ++i) + health_bead_[i].SetFrame(hit_points_ > i ? 0 : 1); + + if (hit_points_ == 0) + static_cast(Engine::Get().GetGame())->EnterGameOverState(); +} + +void Player::AddNuke(int n) { + int new_nuke_count = std::max(std::min(nuke_count_ + n, 3), 0); + if (new_nuke_count == nuke_count_) + return; + + nuke_count_ = new_nuke_count; + nuke_symbol_.SetFrame(4 - nuke_count_); + + if (!nuke_symbol_animator_.IsPlaying(Animator::kRotation)) { + nuke_symbol_animator_.SetRotation( + M_PI * 5, 2, std::bind(SmootherStep, std::placeholders::_1)); + nuke_symbol_animator_.Play(Animator::kRotation, false); + } +} + +void Player::Reset() { + DragCancel(0); + DragCancel(1); + + TakeDamage(-total_health_); + + nuke_count_ = 1; + nuke_symbol_.SetFrame(3); } Vector2f Player::GetWeaponPos(DamageType type) const { @@ -79,7 +189,7 @@ DamageType Player::GetWeaponType(const Vector2f& pos) { } DCHECK(closest_weapon != kDamageType_Invalid); - if (closest_dist < weapon_[closest_weapon].GetSize().x * 0.9f) + if (closest_dist < weapon_[closest_weapon].GetSize().x * 0.5f) return closest_weapon; return kDamageType_Invalid; } @@ -98,6 +208,9 @@ void Player::Fire(DamageType type, Vector2f dir) { Engine& engine = Engine::Get(); Enemy& enemy = static_cast(engine.GetGame())->GetEnemy(); + float max_beam_length = engine.GetScreenSize().y * 1.3f * 0.85f; + constexpr float max_beam_duration = 0.259198f; + if (enemy.HasTarget(type)) dir = weapon_[type].GetPosition() - enemy.GetTargetPos(type); else @@ -119,12 +232,15 @@ void Player::Fire(DamageType type, Vector2f dir) { beam_spark_[type].SetVisible(true); spark_animator_[type].Stop(Animator::kMovement); + float length = beam_[type].GetSize().x * 0.9f; Vector2f movement = dir * -length; - // Convert from units per second to duration. - float speed = 1.0f / (18.0f / length); - spark_animator_[type].SetMovement(movement, speed); + float duration = (length * max_beam_duration) / max_beam_length; + + spark_animator_[type].SetMovement(movement, duration); spark_animator_[type].Play(Animator::kMovement, false); + + laser_shot_[type].Play(false); } bool Player::IsFiring(DamageType type) { @@ -189,69 +305,121 @@ void Player::SetupWeapons() { Animator::kBlending, [&, i]() -> void { beam_[i].SetVisible(false); }); beam_animator_[i].SetBlending({1, 1, 1, 0}, 0.16f); beam_animator_[i].Attach(&beam_[i]); + + laser_shot_[i].SetSound(laser_shot_sound_); + laser_shot_[i].SetVariate(true); + laser_shot_[i].SetSimulateStereo(false); + laser_shot_[i].SetMaxAplitude(0.4f); } } -void Player::UpdateTarget() { - if (IsFiring(active_weapon_)) +void Player::UpdateTarget(DamageType weapon) { + if (IsFiring(weapon)) return; Engine& engine = Engine::Get(); Demo* game = static_cast(engine.GetGame()); - if (drag_valid_) { - Vector2f dir = (drag_end_ - drag_start_).Normalize(); - game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 1.2f); - if (!game->GetEnemy().HasTarget(active_weapon_)) - game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 2); + int i = weapon_drag_ind[weapon]; + + if (drag_valid_[i]) { + Vector2f origin = weapon_[weapon].GetPosition(); + Vector2f dir = (drag_end_[i] - drag_start_[i]).Normalize(); + game->GetEnemy().SelectTarget(weapon, origin, dir); } else { - game->GetEnemy().DeselectTarget(active_weapon_); + game->GetEnemy().DeselectTarget(weapon); } } -void Player::DragStart(const Vector2f& pos) { - active_weapon_ = GetWeaponType(pos); - if (active_weapon_ == kDamageType_Invalid) +void Player::Nuke() { + if (nuke_animator_.IsPlaying(Animator::kBlending)) return; - drag_start_ = pos; - drag_end_ = pos; + if (nuke_count_ <= 0) { + no_nuke_.Play(false); + return; + } - drag_sign_[active_weapon_].SetPosition(drag_start_); - drag_sign_[active_weapon_].SetVisible(true); + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + AddNuke(-1); + + nuke_animator_.SetEndCallback(Animator::kBlending, [&, game]() -> void { + nuke_animator_.SetEndCallback(Animator::kBlending, + [&]() -> void { nuke_.SetVisible(false); }); + nuke_animator_.SetBlending( + kNukeColor[0], 2, std::bind(Acceleration, std::placeholders::_1, -1)); + nuke_animator_.SetEndCallback(Animator::kTimer, [&, game]() -> void { + game->GetEnemy().KillAllEnemyUnits(false); + game->GetEnemy().ResumeProgress(); + }); + nuke_animator_.SetTimer(0.5f); + nuke_animator_.Play(Animator::kBlending | Animator::kTimer, false); + }); + nuke_animator_.SetBlending(kNukeColor[1], 0.1f, + std::bind(Acceleration, std::placeholders::_1, 1)); + nuke_animator_.Play(Animator::kBlending, false); + nuke_.SetVisible(true); + + game->GetEnemy().PauseProgress(); + game->GetEnemy().StopAllEnemyUnits(true); + + nuke_explosion_.Play(false); } -void Player::Drag(const Vector2f& pos) { - if (active_weapon_ == kDamageType_Invalid) +void Player::DragStart(int i, const Vector2f& pos) { + drag_weapon_[i] = GetWeaponType(pos); + if (drag_weapon_[i] == kDamageType_Invalid) { + float dist = (pos - nuke_symbol_.GetPosition()).Length(); + drag_nuke_[i] = dist <= nuke_symbol_.GetSize().x * 0.7f; + return; + } + + weapon_drag_ind[drag_weapon_[i]] = i; + drag_start_[i] = pos; + drag_end_[i] = pos; + + drag_sign_[drag_weapon_[i]].SetPosition(pos); + drag_sign_[drag_weapon_[i]].SetVisible(true); +} + +void Player::Drag(int i, const Vector2f& pos) { + if (drag_weapon_[i] == kDamageType_Invalid) return; - drag_end_ = pos; - drag_sign_[active_weapon_].SetPosition(drag_end_); + drag_end_[i] = pos; + drag_sign_[drag_weapon_[i]].SetPosition(pos); - if (ValidateDrag()) { - if (!drag_valid_ && !IsFiring(active_weapon_)) - WarmupWeapon(active_weapon_); - drag_valid_ = true; + if (ValidateDrag(i)) { + if (!drag_valid_[i] && !IsFiring(drag_weapon_[i])) + WarmupWeapon(drag_weapon_[i]); + drag_valid_[i] = true; } else { - if (drag_valid_ && !IsFiring(active_weapon_)) - CooldownWeapon(active_weapon_); - drag_valid_ = false; + if (drag_valid_[i] && !IsFiring(drag_weapon_[i])) + CooldownWeapon(drag_weapon_[i]); + drag_valid_[i] = false; } } -void Player::DragEnd() { - if (active_weapon_ == kDamageType_Invalid) +void Player::DragEnd(int i) { + if (drag_weapon_[i] == kDamageType_Invalid) { + if (drag_nuke_[i]) { + drag_nuke_[i] = false; + Nuke(); + } return; + } - UpdateTarget(); + UpdateTarget(drag_weapon_[i]); - DamageType type = active_weapon_; - active_weapon_ = kDamageType_Invalid; + DamageType type = drag_weapon_[i]; + drag_weapon_[i] = kDamageType_Invalid; drag_sign_[type].SetVisible(false); - Vector2f fire_dir = (drag_start_ - drag_end_).Normalize(); + Vector2f fire_dir = (drag_start_[i] - drag_end_[i]).Normalize(); - if (drag_valid_ && !IsFiring(type)) { + if (drag_valid_[i] && !IsFiring(type)) { if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { warmup_animator_[type].SetEndCallback( Animator::kFrames, [&, type, fire_dir]() -> void { @@ -265,20 +433,25 @@ void Player::DragEnd() { } } - drag_valid_ = false; - drag_start_ = {0, 0}; - drag_end_ = {0, 0}; + drag_valid_[i] = false; + drag_start_[i] = {0, 0}; + drag_end_[i] = {0, 0}; } -void Player::DragCancel() { - if (active_weapon_ == kDamageType_Invalid) +void Player::DragCancel(int i) { + if (drag_weapon_[i] == kDamageType_Invalid) return; - DamageType type = active_weapon_; - active_weapon_ = kDamageType_Invalid; + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + game->GetEnemy().DeselectTarget(drag_weapon_[i]); + + DamageType type = drag_weapon_[i]; + drag_weapon_[i] = kDamageType_Invalid; drag_sign_[type].SetVisible(false); - if (drag_valid_ && !IsFiring(type)) { + if (drag_valid_[i] && !IsFiring(type)) { if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { warmup_animator_[type].SetEndCallback( Animator::kFrames, [&, type]() -> void { @@ -290,16 +463,16 @@ void Player::DragCancel() { } } - drag_valid_ = false; - drag_start_ = {0, 0}; - drag_end_ = {0, 0}; + drag_valid_[i] = false; + drag_start_[i] = {0, 0}; + drag_end_[i] = {0, 0}; } -bool Player::ValidateDrag() { - Vector2f dir = drag_end_ - drag_start_; +bool Player::ValidateDrag(int i) { + Vector2f dir = drag_end_[i] - drag_start_[i]; float len = dir.Length(); dir.Normalize(); - if (len < weapon_[active_weapon_].GetSize().y / 4) + if (len < weapon_[0].GetSize().y / 3) return false; if (dir.DotProduct(Vector2f(0, 1)) < 0) return false; @@ -307,7 +480,8 @@ bool Player::ValidateDrag() { } void Player::NavigateBack() { - DragCancel(); + DragCancel(0); + DragCancel(1); Engine& engine = Engine::Get(); static_cast(engine.GetGame())->EnterMenuState(); } @@ -315,6 +489,8 @@ void Player::NavigateBack() { bool Player::CreateRenderResources() { Engine::Get().SetImageSource("weapon_tex", "enemy_anims_flare_ok.png", true); Engine::Get().SetImageSource("beam_tex", "enemy_ray_ok.png", true); + Engine::Get().SetImageSource("nuke_symbol_tex", "nuke_frames.png", true); + Engine::Get().SetImageSource("health_bead", "bead.png", true); return true; } diff --git a/src/demo/player.h b/src/demo/player.h index af1f5c5..1c8adaa 100644 --- a/src/demo/player.h +++ b/src/demo/player.h @@ -6,10 +6,13 @@ #include "../base/vecmath.h" #include "../engine/animator.h" #include "../engine/image_quad.h" +#include "../engine/solid_quad.h" +#include "../engine/sound_player.h" #include "damage_type.h" namespace eng { class InputEvent; +class Sound; } // namespace eng class Player { @@ -25,25 +28,56 @@ class Player { void OnInputEvent(std::unique_ptr event); + void TakeDamage(int damage); + + void AddNuke(int n); + + void Reset(); + base::Vector2f GetWeaponPos(DamageType type) const; base::Vector2f GetWeaponScale() const; + int nuke_count() { return nuke_count_; } + private: + std::shared_ptr nuke_explosion_sound_; + std::shared_ptr no_nuke_sound_; + std::shared_ptr laser_shot_sound_; + eng::ImageQuad drag_sign_[2]; eng::ImageQuad weapon_[2]; eng::ImageQuad beam_[2]; eng::ImageQuad beam_spark_[2]; + eng::SoundPlayer laser_shot_[2]; + eng::Animator warmup_animator_[2]; eng::Animator cooldown_animator_[2]; eng::Animator beam_animator_[2]; eng::Animator spark_animator_[2]; - DamageType active_weapon_ = kDamageType_Invalid; + eng::ImageQuad health_bead_[3]; - base::Vector2f drag_start_ = {0, 0}; - base::Vector2f drag_end_ = {0, 0}; - bool drag_valid_ = false; + eng::SolidQuad nuke_; + eng::Animator nuke_animator_; + eng::SoundPlayer nuke_explosion_; + eng::SoundPlayer no_nuke_; + + eng::ImageQuad nuke_symbol_; + eng::Animator nuke_symbol_animator_; + + int nuke_count_ = 0; + + int total_health_ = 3; + int hit_points_ = 0; + + base::Vector2f drag_start_[2] = {{0, 0}, {0, 0}}; + base::Vector2f drag_end_[2] = {{0, 0}, {0, 0}}; + DamageType drag_weapon_[2] = {kDamageType_Invalid, kDamageType_Invalid}; + bool drag_valid_[2] = {false, false}; + int weapon_drag_ind[2] = {0, 0}; + + bool drag_nuke_[2] = {false, false}; DamageType GetWeaponType(const base::Vector2f& pos); @@ -55,13 +89,15 @@ class Player { void SetupWeapons(); - void UpdateTarget(); + void UpdateTarget(DamageType weapon); - void DragStart(const base::Vector2f& pos); - void Drag(const base::Vector2f& pos); - void DragEnd(); - void DragCancel(); - bool ValidateDrag(); + void Nuke(); + + void DragStart(int i, const base::Vector2f& pos); + void Drag(int i, const base::Vector2f& pos); + void DragEnd(int i); + void DragCancel(int i); + bool ValidateDrag(int i); void NavigateBack(); diff --git a/src/demo/sky_quad.cc b/src/demo/sky_quad.cc index ad3901b..30e83e8 100644 --- a/src/demo/sky_quad.cc +++ b/src/demo/sky_quad.cc @@ -20,16 +20,12 @@ SkyQuad::SkyQuad() SkyQuad::~SkyQuad() = default; -bool SkyQuad::Create() { - Engine& engine = Engine::Get(); - - auto source = std::make_unique(); - if (!source->Load("sky.glsl")) +bool SkyQuad::Create(bool without_nebula) { + without_nebula_ = without_nebula; + if (!CreateShaders()) return false; - shader_->Create(std::move(source), engine.GetQuad()->vertex_description(), - Engine::Get().GetQuad()->primitive(), false); - scale_ = engine.GetScreenSize(); + scale_ = Engine::Get().GetScreenSize(); color_animator_.Attach(this); @@ -50,23 +46,37 @@ void SkyQuad::Draw(float frame_frac) { shader_->SetUniform("scale", scale_); shader_->SetUniform("projection", Engine::Get().GetProjectionMatrix()); shader_->SetUniform("sky_offset", sky_offset); - shader_->SetUniform("nebula_color", - {nebula_color_.x, nebula_color_.y, nebula_color_.z}); + if (!without_nebula_) + shader_->SetUniform("nebula_color", + {nebula_color_.x, nebula_color_.y, nebula_color_.z}); shader_->UploadUniforms(); Engine::Get().GetQuad()->Draw(); } void SkyQuad::ContextLost() { - Create(); + CreateShaders(); } void SkyQuad::SwitchColor(const Vector4f& color) { + color_animator_.Pause(Animator::kBlending); + color_animator_.SetTime(Animator::kBlending, 0); color_animator_.SetBlending(color, 5, std::bind(SmoothStep, std::placeholders::_1)); color_animator_.Play(Animator::kBlending, false); } +bool SkyQuad::CreateShaders() { + Engine& engine = Engine::Get(); + + auto source = std::make_unique(); + if (!source->Load(without_nebula_ ? "sky_without_nebula.glsl" : "sky.glsl")) + return false; + shader_->Create(std::move(source), engine.GetQuad()->vertex_description(), + Engine::Get().GetQuad()->primitive(), false); + return true; +} + void SkyQuad::SetSpeed(float speed) { speed_ = speed; -} +} \ No newline at end of file diff --git a/src/demo/sky_quad.h b/src/demo/sky_quad.h index 72d3e23..a9ac0a9 100644 --- a/src/demo/sky_quad.h +++ b/src/demo/sky_quad.h @@ -22,7 +22,7 @@ class SkyQuad : public eng::Animatable { SkyQuad(const SkyQuad&) = delete; SkyQuad& operator=(const SkyQuad&) = delete; - bool Create(); + bool Create(bool without_nebula); void Update(float delta_time); @@ -42,6 +42,8 @@ class SkyQuad : public eng::Animatable { void SetSpeed(float speed); + const base::Vector4f& nebula_color() { return nebula_color_; } + private: std::unique_ptr shader_; @@ -53,6 +55,10 @@ class SkyQuad : public eng::Animatable { eng::Animator color_animator_; float speed_ = 0; + + bool without_nebula_ = false; + + bool CreateShaders(); }; #endif // SKY_QUAD_H