kaliber/src/demo/menu.cc

650 lines
21 KiB
C++
Raw Normal View History

#include "demo/menu.h"
2020-04-13 11:24:53 +00:00
#include <cmath>
2021-08-31 21:21:29 +00:00
#include <string>
2020-04-13 11:24:53 +00:00
#include <vector>
#include "base/collusion_test.h"
#include "base/interpolation.h"
#include "base/log.h"
#include "engine/engine.h"
#include "engine/font.h"
#include "engine/image.h"
#include "engine/input_event.h"
#include "engine/sound.h"
#include "demo/demo.h"
2020-04-13 11:24:53 +00:00
2021-08-31 21:21:29 +00:00
using namespace std::string_literals;
2020-04-13 11:24:53 +00:00
using namespace base;
using namespace eng;
namespace {
constexpr char kVersionStr[] = "Version 1.0.3";
2021-08-31 21:21:29 +00:00
2020-04-13 11:24:53 +00:00
constexpr char kMenuOption[Menu::kOption_Max][10] = {"continue", "start",
"credits", "exit"};
constexpr float kMenuOptionSpace = 1.5f;
2021-02-18 14:42:08 +00:00
const Vector4f kColorNormal = {1, 1, 1, 1};
2021-08-31 21:21:29 +00:00
const Vector4f kColorHighlight = {20, 20, 20, 1};
2020-04-13 11:24:53 +00:00
constexpr float kBlendingSpeed = 0.12f;
2021-08-31 21:21:29 +00:00
const Vector4f kColorSwitch[2] = {{0.003f, 0.91f, 0.99f, 1},
{0.33f, 0.47, 0.51f, 1}};
2021-02-18 14:42:08 +00:00
const Vector4f kColorFadeOut = {1, 1, 1, 0};
2020-04-13 11:24:53 +00:00
constexpr float kFadeSpeed = 0.2f;
2021-08-31 21:21:29 +00:00
const Vector4f kHighScoreColor = {0.895f, 0.692f, 0.24f, 1};
const char kLastWave[] = "last_wave";
constexpr int kMaxStartWave = 10;
2020-04-13 11:24:53 +00:00
} // namespace
Menu::Menu() = default;
2020-04-13 11:24:53 +00:00
Menu::~Menu() = default;
bool Menu::Initialize() {
2021-08-31 21:21:29 +00:00
click_sound_ = std::make_shared<Sound>();
if (!click_sound_->Load("menu_click.mp3", false))
return false;
Demo* game = static_cast<Demo*>(Engine::Get().GetGame());
const Font& font = game->GetFont();
2020-04-13 11:24:53 +00:00
max_text_width_ = -1;
for (int i = 0; i < kOption_Max; ++i) {
int width, height;
font.CalculateBoundingBox(kMenuOption[i], width, height);
if (width > max_text_width_)
max_text_width_ = width;
}
if (!CreateRenderResources())
return false;
2020-04-13 11:24:53 +00:00
for (int i = 0; i < kOption_Max; ++i) {
items_[i].text.Create("menu_tex", {1, 4});
2021-08-31 21:21:29 +00:00
items_[i].text.SetZOrder(41);
2020-04-13 11:24:53 +00:00
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 {
2021-08-31 21:21:29 +00:00
items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr);
2023-05-16 06:51:00 +00:00
// Wait until click sound ends before exiting.
if (i == kExit)
click_.SetEndCallback(
[&, i]() -> void { selected_option_ = (Option)i; });
else
selected_option_ = (Option)i;
2020-04-13 11:24:53 +00:00
};
items_[i].text_animator.Attach(&items_[i].text);
}
// Get the item positions calculated.
SetOptionEnabled(kContinue, true);
2021-08-31 21:21:29 +00:00
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(
2022-08-25 23:28:53 +00:00
Lerp(3.0f, 8.0f, Engine::Get().GetRandomGenerator().Rand()));
logo_animator_[1].Play(Animator::kFrames, true);
logo_animator_[1].Play(Animator::kTimer, false);
2021-08-31 21:21:29 +00:00
});
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(
2022-08-25 23:28:53 +00:00
Lerp(3.0f, 8.0f, Engine::Get().GetRandomGenerator().Rand()));
2021-08-31 21:21:29 +00:00
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<Demo*>(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<Demo*>(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<Demo*>(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<Demo*>(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);
2020-04-13 11:24:53 +00:00
return true;
}
void Menu::OnInputEvent(std::unique_ptr<InputEvent> event) {
2021-08-31 21:21:29 +00:00
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;
2021-02-18 14:42:08 +00:00
if (event->GetType() == InputEvent::kDragStart) {
tap_pos_[0] = event->GetVector();
tap_pos_[1] = event->GetVector();
2021-02-18 14:42:08 +00:00
} else if (event->GetType() == InputEvent::kDrag) {
tap_pos_[1] = event->GetVector();
}
2020-04-13 11:24:53 +00:00
if (event->GetType() != InputEvent::kDragEnd || IsAnimating())
2020-04-13 11:24:53 +00:00
return;
for (int i = 0; i < kOption_Max; ++i) {
if (items_[i].hide)
continue;
if (!Intersection(items_[i].text.GetPosition(),
2021-02-18 14:42:08 +00:00
items_[i].text.GetSize() * Vector2f(1.2f, 2),
tap_pos_[0]))
2020-04-13 11:24:53 +00:00
continue;
if (!Intersection(items_[i].text.GetPosition(),
2021-02-18 14:42:08 +00:00
items_[i].text.GetSize() * Vector2f(1.2f, 2),
tap_pos_[1]))
2020-04-13 11:24:53 +00:00
continue;
items_[i].text_animator.SetEndCallback(Animator::kBlending,
items_[i].select_item_cb_);
items_[i].text_animator.SetBlending(kColorHighlight, kBlendingSpeed);
items_[i].text_animator.Play(Animator::kBlending, false);
2021-08-31 21:21:29 +00:00
click_.Play(false);
2020-04-13 11:24:53 +00:00
}
}
void Menu::SetOptionEnabled(Option o, bool enable) {
int first = -1, last = -1;
for (int i = 0; i < kOption_Max; ++i) {
if (i == o)
items_[i].hide = !enable;
if (!items_[i].hide) {
items_[i].text.SetPosition({0, 0});
2020-04-13 11:24:53 +00:00
if (last >= 0) {
items_[i].text.PlaceToBottomOf(items_[last].text);
items_[i].text.Translate(items_[last].text.GetPosition() *
2021-02-18 14:42:08 +00:00
Vector2f(0, 1));
2020-04-13 11:24:53 +00:00
items_[i].text.Translate(
{0, items_[last].text.GetSize().y * -kMenuOptionSpace});
2020-04-13 11:24:53 +00:00
}
if (first < 0)
first = i;
last = i;
}
}
float center_offset_y =
(items_[first].text.GetPosition().y - items_[last].text.GetPosition().y) /
2;
2020-04-13 11:24:53 +00:00
for (int i = 0; i < kOption_Max; ++i) {
if (!items_[i].hide)
items_[i].text.Translate({0, center_offset_y});
}
}
void Menu::Show() {
2021-08-31 21:21:29 +00:00
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<Demo*>(Engine::Get().GetGame())->GetHighScore()) {
high_score_value_ =
static_cast<Demo*>(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);
2022-08-25 23:28:53 +00:00
high_score_animator_.Play(Animator::kBlending, true);
high_score_animator_.Play(Animator::kTimer, false);
2021-08-31 21:21:29 +00:00
});
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);
2020-04-13 11:24:53 +00:00
for (int i = 0; i < kOption_Max; ++i) {
if (items_[i].hide)
continue;
items_[i].text_animator.SetEndCallback(
Animator::kBlending, [&, i]() -> void {
items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr);
});
items_[i].text_animator.SetBlending(kColorNormal, kFadeSpeed);
items_[i].text_animator.Play(Animator::kBlending, false);
items_[i].text.SetVisible(true);
}
2021-08-31 21:21:29 +00:00
toggle_audio_.Show();
toggle_music_.Show();
toggle_vibration_.Show();
Demo* game = static_cast<Demo*>(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();
}
2020-04-13 11:24:53 +00:00
}
2021-08-31 21:21:29 +00:00
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);
2020-04-13 11:24:53 +00:00
selected_option_ = kOption_Invalid;
for (int i = 0; i < kOption_Max; ++i) {
if (items_[i].hide)
continue;
items_[i].text_animator.SetEndCallback(
Animator::kBlending, [&, i]() -> void {
items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr);
items_[i].text.SetVisible(false);
});
items_[i].text_animator.SetBlending(kColorFadeOut, kFadeSpeed);
items_[i].text_animator.Play(Animator::kBlending, false);
}
2021-08-31 21:21:29 +00:00
toggle_audio_.Hide();
toggle_music_.Hide();
toggle_vibration_.Hide();
if (starting_wave_.image().IsVisible()) {
starting_wave_.Hide();
wave_up_.Hide();
}
2020-04-13 11:24:53 +00:00
}
bool Menu::CreateRenderResources() {
2021-08-31 21:21:29 +00:00
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<Image> {
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
constexpr char btn_text[] = "[ ]";
int w, h;
font.CalculateBoundingBox(btn_text, w, h);
auto image = std::make_unique<Image>();
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<Image> {
const Font* font = Engine::Get().GetSystemFont();
int w, h;
font->CalculateBoundingBox(kVersionStr, w, h);
auto image = std::make_unique<Image>();
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;
}
2021-08-31 21:21:29 +00:00
std::unique_ptr<Image> Menu::CreateMenuImage() {
2020-04-13 11:24:53 +00:00
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
int line_height = font.GetLineHeight() + 1;
auto image = std::make_unique<Image>();
image->Create(max_text_width_, line_height * kOption_Max);
// Fill the area of each menu item with gradient.
2021-08-31 21:21:29 +00:00
image->GradientV({0.80f, 0.87f, 0.93f, 0},
kColorSwitch[0] * Vector4f(1, 1, 1, 0), line_height);
2020-04-13 11:24:53 +00:00
for (int i = 0; i < kOption_Max; ++i) {
int w, h;
font.CalculateBoundingBox(kMenuOption[i], w, h);
float x = (image->GetWidth() - w) / 2;
float y = line_height * i;
font.Print(x, y, kMenuOption[i], image->GetBuffer(), image->GetWidth());
2020-04-13 11:24:53 +00:00
}
2021-08-31 21:21:29 +00:00
image->Compress();
return image;
}
std::unique_ptr<Image> Menu::CreateHighScoreImage() {
std::string text = "High Score: "s + std::to_string(high_score_value_);
const Font& font = static_cast<Demo*>(Engine::Get().GetGame())->GetFont();
int width, height;
font.CalculateBoundingBox(text, width, height);
auto image = std::make_unique<Image>();
image->Create(width, height);
image->Clear({1, 1, 1, 0});
font.Print(0, 0, text, image->GetBuffer(), image->GetWidth());
image->Compress();
2020-04-13 11:24:53 +00:00
return image;
}
bool Menu::IsAnimating() {
for (int i = 0; i < kOption_Max; ++i) {
if (items_[i].text_animator.IsPlaying(Animator::kBlending))
return true;
}
return false;
}
2021-08-31 21:21:29 +00:00
//
// Menu::Button implementation
//
void Menu::Button::Create(const std::string& asset_name,
std::array<int, 2> 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<eng::Image> Menu::Radio::CreateImage() {
const Font& font = static_cast<Demo*>(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>();
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;
}