|
|
|
@@ -0,0 +1,765 @@
|
|
|
|
|
// The game's menus, drawn into the HUD canvas: the title screen (006e3410 with DAT_0120761b off), the pause menu (the same with the help image), the new game
|
|
|
|
|
// panel with the game modes (006ceb00 / 006378a0: "normal" and "dailyrun" built in, then every mod with is_game_mode in its mod.xml), the daily run's seed from
|
|
|
|
|
// the game's backend (0084c6f0: GET https://takapuoli.noitagame.com/callback/ -> "version;daily seed;daily practice seed;1;"), the options (006d5620: the tabs
|
|
|
|
|
// General / Graphics / Audio / Input / Streaming / Mod settings), the release notes (0084d020 reads _release_notes.txt beside the game: here release_notes.txt
|
|
|
|
|
// beside our executable, written from the git log at configure time - every entry is a commit, its hash the version), the credits (data/credits.txt) and the
|
|
|
|
|
// game over screen with the run's stats (006e51e0: the cause of death, then the stats box with the bests, New Game / Save death replay / Progress / Quit).
|
|
|
|
|
// Layout in GUI units: `gs` window pixels each (the game's GUI scale: the window height / 360, so a 1440p window draws at 4 like the game).
|
|
|
|
|
#pragma once
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <ctime>
|
|
|
|
|
#include <functional>
|
|
|
|
|
#include <map>
|
|
|
|
|
#include <set>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <thread>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#include "app/hud.h"
|
|
|
|
|
#include "core/wak.h"
|
|
|
|
|
|
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
#include <wininet.h>
|
|
|
|
|
#pragma comment(lib, "wininet.lib")
|
|
|
|
|
|
|
|
|
|
namespace menu {
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- assets ----------------------------------------------------------------
|
|
|
|
|
struct Assets {
|
|
|
|
|
hud::Icon logo, gameOver, gameCompleted, help, p9, p9gray, tab, tabHover, tabSel, bannerSel, grass, bannerNormalBg, bannerNormalFg, bannerDailyBg, bannerDailyFg,
|
|
|
|
|
mountains1, mountains2, layer1, layer2, clouds1, stars[7], cursor, gridBox, gridBoxUnknown, gridNew, iconUnknown;
|
|
|
|
|
std::vector<uint32_t> sky; int skyW = 0, skyH = 0; // weather_gfx/parallax_colors_main_menu.bmp: a column is the sky's gradient from the top (row 0) down
|
|
|
|
|
std::string credits;
|
|
|
|
|
bool loaded = false;
|
|
|
|
|
static bool load_bmp(const std::vector<uint8_t>& raw, std::vector<uint32_t>& px, int& w, int& h) { // 24-bit uncompressed, bottom-up
|
|
|
|
|
if (raw.size() < 54 || raw[0] != 'B' || raw[1] != 'M') return false;
|
|
|
|
|
const uint32_t off = *reinterpret_cast<const uint32_t*>(&raw[10]);
|
|
|
|
|
const int32_t bw = *reinterpret_cast<const int32_t*>(&raw[18]), bh = *reinterpret_cast<const int32_t*>(&raw[22]);
|
|
|
|
|
const uint16_t bpp = *reinterpret_cast<const uint16_t*>(&raw[28]);
|
|
|
|
|
if (bpp != 24 || bw <= 0 || bh == 0) return false;
|
|
|
|
|
const int ah = std::abs(bh), stride = (bw * 3 + 3) & ~3;
|
|
|
|
|
if (off + (size_t)stride * ah > raw.size()) return false;
|
|
|
|
|
w = bw; h = ah; px.assign((size_t)w * h, 0);
|
|
|
|
|
for (int y = 0; y < h; ++y) {
|
|
|
|
|
const uint8_t* row = &raw[off + (size_t)(bh > 0 ? h - 1 - y : y) * stride];
|
|
|
|
|
for (int x = 0; x < w; ++x) px[(size_t)y * w + x] = 0xFF000000u | (uint32_t)row[x * 3 + 2] << 16 | (uint32_t)row[x * 3 + 1] << 8 | row[x * 3];
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
void load(const ng::Wak& wak) {
|
|
|
|
|
auto L = [&](const char* p) { return hud::load_icon(wak, p); };
|
|
|
|
|
logo = L("data/ui_gfx/pause_menu/noita_logo.png"); gameOver = L("data/ui_gfx/game_over_menu/game_over.png"); gameCompleted = L("data/ui_gfx/game_over_menu/game_completed.png");
|
|
|
|
|
help = L("data/ui_gfx/pause_menu/help_keyboardmouse.png"); p9 = L("data/ui_gfx/decorations/9piece0.png"); p9gray = L("data/ui_gfx/decorations/9piece0_gray.png");
|
|
|
|
|
tab = L("data/ui_gfx/decorations/tab.png"); tabHover = L("data/ui_gfx/decorations/tab_hovered.png"); tabSel = L("data/ui_gfx/decorations/tab_selected.png");
|
|
|
|
|
bannerSel = L("data/ui_gfx/game_mode/banner_selected_overlay.png"); grass = L("data/ui_gfx/game_mode/background_grass.png");
|
|
|
|
|
bannerNormalBg = L("data/ui_gfx/game_mode/banner_normal_background.png"); bannerNormalFg = L("data/ui_gfx/game_mode/banner_normal_overlay.png");
|
|
|
|
|
bannerDailyBg = L("data/ui_gfx/game_mode/banner_dailyrun_background.png"); bannerDailyFg = L("data/ui_gfx/game_mode/banner_dailyrun_overlay.png");
|
|
|
|
|
mountains1 = L("data/weather_gfx/parallax_mountains_01.png"); mountains2 = L("data/weather_gfx/parallax_mountains_02.png");
|
|
|
|
|
layer1 = L("data/weather_gfx/parallax_mountains_layer_01.png"); layer2 = L("data/weather_gfx/parallax_mountains_layer_02.png"); clouds1 = L("data/weather_gfx/parallax_clounds_01.png");
|
|
|
|
|
for (int i = 0; i < 7; ++i) stars[i] = L(("data/weather_gfx/star_00" + std::to_string(i + 1) + ".png").c_str());
|
|
|
|
|
cursor = L("data/ui_gfx/mouse_cursor.png");
|
|
|
|
|
gridBox = L("data/ui_gfx/progress_menu/grid_box.png"); gridBoxUnknown = L("data/ui_gfx/progress_menu/grid_box_unknown.png"); // (006dfe40: the Progress screen's cells)
|
|
|
|
|
gridNew = L("data/ui_gfx/progress_menu/grid_highlight_new.png"); iconUnknown = L("data/ui_gfx/progress_menu/icon_unknown.png");
|
|
|
|
|
std::vector<uint8_t> raw;
|
|
|
|
|
if (wak.read("data/weather_gfx/parallax_colors_main_menu.bmp", raw)) load_bmp(raw, sky, skyW, skyH);
|
|
|
|
|
if (wak.read("data/credits.txt", raw)) credits.assign(raw.begin(), raw.end());
|
|
|
|
|
loaded = true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- settings ----------------------------------------------------------------
|
|
|
|
|
// The options of the Options screen, kept in <save dir>/settings.txt (key=value). The engine reads them where they apply (main.cpp).
|
|
|
|
|
struct KeyBind { const char* id; const char* name; int key; };
|
|
|
|
|
struct Settings {
|
|
|
|
|
int window_mode = 0; // 0 windowed, 1 borderless fullscreen
|
|
|
|
|
bool vsync = true, cursor = false, replay_recorder = false, damage_numbers = true, pause_unfocused = true, click_switches_item = false, hover_info_at_mouse = false;
|
|
|
|
|
float music = 1.0f, sfx = 1.0f, screenshake = 1.0f;
|
|
|
|
|
// 006d5620's sliders: brightness -0.09 .. 0.09, contrast -0.12 .. 0.2, gamma -0.32 .. 0.695 (all 0 by default: post_final.frag adds the brightness, scales about
|
|
|
|
|
// 0.5 by 1 + contrast, raises to 1 + gamma), the cosmetic particle coefficient 0.05 .. 1 (the count of every cosmetic emission)
|
|
|
|
|
float brightness = 0.0f, contrast = 0.0f, gamma = 0.0f, cosmetic_particles = 1.0f;
|
|
|
|
|
static constexpr float kBrightMin = -0.09f, kBrightMax = 0.09f, kContrastMin = -0.12f, kContrastMax = 0.2f, kGammaMin = -0.32f, kGammaMax = 0.695f, kCosmeticMin = 0.05f;
|
|
|
|
|
bool online = true, streamer_mode = false; // (streamer mode: the game's Twitch integration switch - 006ceb00 opens the Streaming tab when it goes on)
|
|
|
|
|
std::vector<KeyBind> keys = {{"left", "Move left", 'A'}, {"right", "Move right", 'D'}, {"up", "Levitate / jump", 'W'}, {"down", "Move down", 'S'}, {"kick", "Kick", 'F'},
|
|
|
|
|
{"interact", "Interact / pick up", 'E'}, {"inventory", "Inventory", VK_TAB}, {"drop", "Drop item", 'Q'}, {"pause", "Pause", 'P'}};
|
|
|
|
|
int key(const char* id) const { for (const KeyBind& k : keys) if (!std::strcmp(k.id, id)) return k.key; return 0; }
|
|
|
|
|
std::string path;
|
|
|
|
|
void load(const std::string& p) {
|
|
|
|
|
path = p;
|
|
|
|
|
FILE* f = std::fopen(p.c_str(), "rb");
|
|
|
|
|
if (!f) return;
|
|
|
|
|
char line[512];
|
|
|
|
|
while (std::fgets(line, sizeof line, f)) {
|
|
|
|
|
std::string s(line); while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
|
|
|
|
|
const size_t eq = s.find('=');
|
|
|
|
|
if (eq == std::string::npos) continue;
|
|
|
|
|
const std::string k = s.substr(0, eq), v = s.substr(eq + 1);
|
|
|
|
|
auto b = [&](const char* n, bool& dst) { if (k == n) dst = v == "1"; };
|
|
|
|
|
auto fl = [&](const char* n, float& dst) { if (k == n) dst = (float)std::atof(v.c_str()); };
|
|
|
|
|
if (k == "window_mode") window_mode = std::atoi(v.c_str());
|
|
|
|
|
b("vsync", vsync); b("cursor", cursor); b("replay_recorder", replay_recorder); b("damage_numbers", damage_numbers); b("pause_unfocused", pause_unfocused);
|
|
|
|
|
b("click_switches_item", click_switches_item); b("hover_info_at_mouse", hover_info_at_mouse); b("online", online); b("streamer_mode", streamer_mode);
|
|
|
|
|
fl("music", music); fl("sfx", sfx); fl("screenshake", screenshake); fl("brightness", brightness); fl("contrast", contrast); fl("gamma", gamma); fl("cosmetic_particles", cosmetic_particles);
|
|
|
|
|
if (k.compare(0, 4, "key_") == 0) for (KeyBind& kb : keys) if (k.substr(4) == kb.id) kb.key = std::atoi(v.c_str());
|
|
|
|
|
}
|
|
|
|
|
std::fclose(f);
|
|
|
|
|
brightness = std::clamp(brightness, kBrightMin, kBrightMax); contrast = std::clamp(contrast, kContrastMin, kContrastMax); gamma = std::clamp(gamma, kGammaMin, kGammaMax);
|
|
|
|
|
cosmetic_particles = std::clamp(cosmetic_particles, kCosmeticMin, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
void save() const {
|
|
|
|
|
if (path.empty()) return;
|
|
|
|
|
FILE* f = std::fopen(path.c_str(), "wb");
|
|
|
|
|
if (!f) return;
|
|
|
|
|
std::fprintf(f, "window_mode=%d\nvsync=%d\ncursor=%d\nreplay_recorder=%d\ndamage_numbers=%d\npause_unfocused=%d\nclick_switches_item=%d\nhover_info_at_mouse=%d\nonline=%d\nstreamer_mode=%d\n",
|
|
|
|
|
window_mode, vsync, cursor, replay_recorder, damage_numbers, pause_unfocused, click_switches_item, hover_info_at_mouse, online, streamer_mode);
|
|
|
|
|
std::fprintf(f, "music=%.3f\nsfx=%.3f\nscreenshake=%.3f\nbrightness=%.3f\ncontrast=%.3f\ngamma=%.3f\ncosmetic_particles=%.3f\n", music, sfx, screenshake, brightness, contrast, gamma, cosmetic_particles);
|
|
|
|
|
for (const KeyBind& k : keys) std::fprintf(f, "key_%s=%d\n", k.id, k.key);
|
|
|
|
|
std::fclose(f);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- the run, its stats, the game modes ----------------------------------------------------------------
|
|
|
|
|
struct GameMode { std::string id, name, desc, banner_bg, banner_fg; bool mod = false; };
|
|
|
|
|
struct RunStats { int gold = 0; double playtime = 0; int depth = 0, places = 0, kills = 0, maxhp = 0, items = 0; }; // (depth in metres: the deepest point / 10)
|
|
|
|
|
struct ProgressItem { std::string id, icon, name, desc; bool known = false, is_new = false; int kills = 0, killed_by = 0; }; // a perk, a spell or an enemy of the Progress screen (006dfe40)
|
|
|
|
|
struct ModInfo { std::string id, name, desc; bool enabled = false, game_mode = false; }; // a folder of mods/ (the Mods screen: 00b84f0's list, enabled in order)
|
|
|
|
|
struct Context { // what main.cpp knows, given to the menu every frame
|
|
|
|
|
int w = 0, h = 0, mx = 0, my = 0, wheel = 0;
|
|
|
|
|
bool click = false, lmb = false, escape = false, has_run = false, mods_used = false, dead = false;
|
|
|
|
|
int key_pressed = 0; // a key that went down this frame (the Input tab's rebinding)
|
|
|
|
|
std::string build, run_mode, location, death_cause;
|
|
|
|
|
uint32_t seed = 0;
|
|
|
|
|
const std::string* release_notes = nullptr; // the text beside the executable
|
|
|
|
|
const std::vector<GameMode>* modes = nullptr;
|
|
|
|
|
const std::vector<std::string>* mods = nullptr; // the enabled mods
|
|
|
|
|
RunStats stats, best; int total_deaths = 0, total_wins = 0;
|
|
|
|
|
Settings* settings = nullptr;
|
|
|
|
|
const std::vector<ProgressItem>* perks = nullptr; const std::vector<ProgressItem>* actions = nullptr; const std::vector<ProgressItem>* enemies = nullptr; // the Progress screen
|
|
|
|
|
int secrets_found = 0; bool ending0 = false, ending1 = false; // (the progress_* flags: the secret_* / progress_* ones found, the two endings)
|
|
|
|
|
std::vector<ModInfo>* all_mods = nullptr; // every mod in mods/: the Mods screen toggles and reorders them, Action::ApplyMods writes the list
|
|
|
|
|
bool replay_available = false; // the replay recorder holds frames: the game over's "Save death replay" opens the clip editor
|
|
|
|
|
bool suspended = false; // the clip editor is up over the menu: nothing drawn, nothing read
|
|
|
|
|
std::function<std::string(const std::string&)> tr; // "$menu_continue" -> the game's text (common.csv), the key itself when unknown
|
|
|
|
|
};
|
|
|
|
|
enum class Action { None, Continue, StartGame, Quit, SaveQuit, ApplySettings, ResetProgress, SaveReplay, ApplyMods, OpenModFolder, RestartMods, ProgressSeen };
|
|
|
|
|
struct Result { Action action = Action::None; std::string mode_id; uint32_t seed = 0; bool daily = false, daily_practice = false; };
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- the daily seed ----------------------------------------------------------------
|
|
|
|
|
// 0084c6f0: HTTPS GET takapuoli.noitagame.com/callback/ (WinINet); the body "<version hash>;<daily run seed>;<daily practice seed>;1;" (the game reads it as lines)
|
|
|
|
|
struct Daily {
|
|
|
|
|
std::atomic<int> state{0}; // 0 not asked, 1 fetching, 2 ok, 3 failed
|
|
|
|
|
uint32_t seed = 0, practice = 0;
|
|
|
|
|
std::string version;
|
|
|
|
|
std::thread th;
|
|
|
|
|
~Daily() { if (th.joinable()) th.detach(); }
|
|
|
|
|
void fetch() {
|
|
|
|
|
if (state.load() != 0) return;
|
|
|
|
|
state = 1;
|
|
|
|
|
th = std::thread([this] {
|
|
|
|
|
std::string body;
|
|
|
|
|
HINTERNET net = InternetOpenA("Noita", INTERNET_OPEN_TYPE_PRECONFIG, nullptr, nullptr, 0);
|
|
|
|
|
if (net) {
|
|
|
|
|
HINTERNET con = InternetConnectA(net, "takapuoli.noitagame.com", 443, "", "", INTERNET_SERVICE_HTTP, 0, 0);
|
|
|
|
|
if (con) {
|
|
|
|
|
HINTERNET req = HttpOpenRequestA(con, "GET", "/callback/", "HTTP/1.0", nullptr, nullptr, INTERNET_FLAG_SECURE | INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0);
|
|
|
|
|
if (req && HttpSendRequestA(req, nullptr, 0, nullptr, 0)) {
|
|
|
|
|
char buf[1024]; DWORD n = 0;
|
|
|
|
|
while (InternetReadFile(req, buf, sizeof buf, &n) && n > 0 && body.size() < 4096) body.append(buf, n);
|
|
|
|
|
}
|
|
|
|
|
if (req) InternetCloseHandle(req);
|
|
|
|
|
InternetCloseHandle(con);
|
|
|
|
|
}
|
|
|
|
|
InternetCloseHandle(net);
|
|
|
|
|
}
|
|
|
|
|
std::vector<std::string> parts; std::string cur;
|
|
|
|
|
for (char ch : body) { if (ch == ';' || ch == '\n') { parts.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; }
|
|
|
|
|
if (!cur.empty()) parts.push_back(cur);
|
|
|
|
|
auto num = [](const std::string& s, uint32_t& out) { if (s.empty() || s.size() > 10 || s.find_first_not_of("0123456789") != std::string::npos) return false; out = (uint32_t)std::strtoul(s.c_str(), nullptr, 10); return true; };
|
|
|
|
|
if (parts.size() >= 3 && num(parts[1], seed) && num(parts[2], practice)) { version = parts[0]; state = 2; }
|
|
|
|
|
else state = 3;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- the menu ----------------------------------------------------------------
|
|
|
|
|
class Menu {
|
|
|
|
|
public:
|
|
|
|
|
enum class Screen { None, Title, NewGame, Pause, Options, ReleaseNotes, Credits, Mods, Progress, GameOver, GameOverStats, Confirm };
|
|
|
|
|
Screen screen = Screen::None;
|
|
|
|
|
Daily daily;
|
|
|
|
|
bool active() const { return screen != Screen::None; }
|
|
|
|
|
void open(Screen s) { screen = s; hover_ = -1; scroll_ = 0; if (s == Screen::NewGame) { selMode_ = -1; } if (s == Screen::Credits) creditsT_ = 0.0; }
|
|
|
|
|
void game_over(const std::string& cause) { cause_ = cause; gameOverT_ = 0.0; screen = Screen::GameOver; }
|
|
|
|
|
void confirm(const std::string& text, Action yes) { confirmText_ = text; confirmAction_ = yes; confirmBack_ = screen; screen = Screen::Confirm; }
|
|
|
|
|
bool in_game() const { return inGame_; }
|
|
|
|
|
bool over_game() const { return screen == Screen::GameOver || screen == Screen::GameOverStats; } // (the game over plays over the running world: no pause, no background)
|
|
|
|
|
void set_in_game(bool b) { inGame_ = b; }
|
|
|
|
|
uint32_t last_daily_played = 0; // (stats/_last_daily_run_played.txt: the daily can be played once a day)
|
|
|
|
|
|
|
|
|
|
Result frame(hud::Canvas& cv, const Assets& as, const Context& c) {
|
|
|
|
|
Result r;
|
|
|
|
|
if (!active() || c.suspended) return r;
|
|
|
|
|
gs_ = std::max(1, (int)std::lround(c.h / 360.0));
|
|
|
|
|
cv_ = &cv; as_ = &as; c_ = &c; tick_ += 1.0 / 60.0;
|
|
|
|
|
clicked_ = c.click; hot_ = -1; itemN_ = 0;
|
|
|
|
|
if (screen == Screen::Title || screen == Screen::NewGame || (!inGame_ && !c.dead && (screen == Screen::Options || screen == Screen::ReleaseNotes || screen == Screen::Credits || screen == Screen::Mods || screen == Screen::Progress || screen == Screen::Confirm)))
|
|
|
|
|
background();
|
|
|
|
|
else if (!over_game()) { cv.rect(0, 0, cv.w, cv.h, hud::rgba(0, 0, 0, 150)); cv.mark_all(); }
|
|
|
|
|
switch (screen) {
|
|
|
|
|
case Screen::Title: title(r); break;
|
|
|
|
|
case Screen::Pause: pause(r); break;
|
|
|
|
|
case Screen::NewGame: newGame(r); break;
|
|
|
|
|
case Screen::Options: options(r); break;
|
|
|
|
|
case Screen::ReleaseNotes: releaseNotes(); break;
|
|
|
|
|
case Screen::Credits: credits(); break;
|
|
|
|
|
case Screen::Mods: mods(r); break;
|
|
|
|
|
case Screen::Progress: progress(r); break;
|
|
|
|
|
case Screen::GameOver: gameOver(r); break;
|
|
|
|
|
case Screen::GameOverStats: gameOverStats(r); break;
|
|
|
|
|
case Screen::Confirm: confirmScreen(r); break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
if (c.escape) { // Esc: back to the menu behind, or (from the pause menu) back to the game
|
|
|
|
|
if (screen == Screen::Pause) { screen = Screen::None; r.action = Action::Continue; }
|
|
|
|
|
else if (screen == Screen::Title || screen == Screen::GameOver || screen == Screen::GameOverStats) {}
|
|
|
|
|
else if (screen == Screen::NewGame && c.dead) screen = Screen::GameOverStats;
|
|
|
|
|
else if (screen == Screen::Confirm) screen = confirmBack_;
|
|
|
|
|
else { if (screen == Screen::Progress) r.action = Action::ProgressSeen; screen = back(); }
|
|
|
|
|
}
|
|
|
|
|
if (as.cursor.ok()) cv.icon(as.cursor, c.mx, c.my, std::max(1, gs_ / 2)); // (the game's cursor: the system's is hidden over the window)
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
hud::Canvas* cv_ = nullptr; const Assets* as_ = nullptr; const Context* c_ = nullptr;
|
|
|
|
|
int gs_ = 2, hover_ = -1, hot_ = -1, itemN_ = 0, selMode_ = 0, tab_ = 0, scroll_ = 0, bindIdx_ = -1, sliderDrag_ = -1;
|
|
|
|
|
bool clicked_ = false, inGame_ = false;
|
|
|
|
|
double tick_ = 0.0, creditsT_ = 0.0, gameOverT_ = 0.0;
|
|
|
|
|
std::string cause_, confirmText_; Action confirmAction_ = Action::None; Screen confirmBack_ = Screen::Title;
|
|
|
|
|
std::vector<uint32_t> bg_; int bgW_ = 0, bgH_ = 0;
|
|
|
|
|
uint32_t white() const { return hud::rgba(255, 255, 255); }
|
|
|
|
|
uint32_t grey() const { return hud::rgba(190, 190, 200); }
|
|
|
|
|
uint32_t dim() const { return hud::rgba(120, 120, 130); }
|
|
|
|
|
uint32_t yellow() const { return hud::rgba(255, 230, 80); }
|
|
|
|
|
std::string tr(const std::string& k) const { return c_->tr ? c_->tr(k) : k; }
|
|
|
|
|
Screen back() const { return c_->dead ? Screen::GameOverStats : inGame_ ? Screen::Pause : Screen::Title; } // the screen a sub-menu returns to
|
|
|
|
|
std::string fmt(const std::string& key, const std::string& a0, const std::string& a1 = std::string()) const { // the game's "$0" / "$1" substitution
|
|
|
|
|
std::string s = tr(key);
|
|
|
|
|
for (const auto& [tag, val] : {std::pair<const char*, const std::string*>{"$0", &a0}, {"$1", &a1}}) { const size_t p = s.find(tag); if (p != std::string::npos) s.replace(p, 2, *val); }
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
int tw(const std::string& s) const { return cv_->text_width(s, gs_); }
|
|
|
|
|
int lineH() const { return 11 * gs_; }
|
|
|
|
|
|
|
|
|
|
// ---- the parallax background: the sky's gradient, the mountain layers, drifting clouds and twinkling stars (the title and the game over screens) ----
|
|
|
|
|
void background() {
|
|
|
|
|
hud::Canvas& cv = *cv_; const Assets& as = *as_;
|
|
|
|
|
if (bgW_ != cv.w || bgH_ != cv.h) {
|
|
|
|
|
bgW_ = cv.w; bgH_ = cv.h; bg_.assign((size_t)cv.w * cv.h, 0xFF101018u);
|
|
|
|
|
hud::Canvas tmp; tmp.px = bg_.data(); tmp.w = cv.w; tmp.h = cv.h; tmp.tiles.assign((size_t)((cv.w + 31) >> 5) * ((cv.h + 31) >> 5), 0); tmp.tw = (cv.w + 31) >> 5; tmp.th = (cv.h + 31) >> 5;
|
|
|
|
|
// parallax_colors_main_menu.bmp: a column is a time of day (512 of them), the rows come in bands of 20 (a colour, then a white and a black marker row):
|
|
|
|
|
// band 0 the sky's top, 1 its horizon, 2 the clouds, 3 .. the mountain layers from the far one. Column 288 is the night of the title screen.
|
|
|
|
|
auto band = [&](int k) -> uint32_t { if (as.sky.empty()) return k == 0 ? 0xFF31253Au : k == 1 ? 0xFF5A4D54u : k == 3 ? 0xFF383C52u : 0xFF292E43u; return as.sky[(size_t)std::min(as.skyH - 1, 20 * k) * as.skyW + std::min(as.skyW - 1, 288)]; };
|
|
|
|
|
const uint32_t top = band(0), bot = band(1);
|
|
|
|
|
for (int y = 0; y < cv.h; ++y) {
|
|
|
|
|
const double f = std::min(1.0, y / (cv.h * 0.75));
|
|
|
|
|
auto mix = [&](int sh) { return (uint32_t)(((top >> sh) & 255) * (1.0 - f) + ((bot >> sh) & 255) * f); };
|
|
|
|
|
const uint32_t c = 0xFF000000u | mix(16) << 16 | mix(8) << 8 | mix(0);
|
|
|
|
|
for (int x = 0; x < cv.w; ++x) bg_[(size_t)y * cv.w + x] = c;
|
|
|
|
|
}
|
|
|
|
|
auto layer = [&](const hud::Icon& ic, double scale, int dy, uint32_t tint) { // the layers are white masks: tinted with their band's colour
|
|
|
|
|
if (!ic.ok()) return;
|
|
|
|
|
hud::Icon t2 = ic; for (uint32_t& p : t2.px) p = (p & 0xFF000000u) | (tint & 0xFFFFFFu);
|
|
|
|
|
const int w = (int)(ic.w * scale), h = (int)(ic.h * scale); tmp.icon_fit(t2, (cv.w - w) / 2, cv.h - h + dy, w, h);
|
|
|
|
|
};
|
|
|
|
|
const double sc = std::max(cv.w / 1024.0, cv.h * 0.75 / 512.0);
|
|
|
|
|
layer(as.layer2, sc, (int)(cv.h * 0.08), band(3)); layer(as.layer1, sc, (int)(cv.h * 0.16), band(4));
|
|
|
|
|
}
|
|
|
|
|
std::memcpy(cv.px, bg_.data(), bg_.size() * 4); cv.mark_all();
|
|
|
|
|
uint32_t seed = 12345u; // the stars (a fixed scatter over the sky, twinkling)
|
|
|
|
|
for (int i = 0; i < 60; ++i) {
|
|
|
|
|
seed = seed * 1664525u + 1013904223u; const int x = (int)((seed >> 8) % (uint32_t)cv.w);
|
|
|
|
|
seed = seed * 1664525u + 1013904223u; const int y = (int)((seed >> 8) % (uint32_t)std::max(1, cv.h * 6 / 10));
|
|
|
|
|
seed = seed * 1664525u + 1013904223u; const hud::Icon& st = as.stars[(seed >> 8) % 7];
|
|
|
|
|
const double tw = 0.6 + 0.4 * std::sin(tick_ * (1.0 + (i % 5) * 0.3) + i);
|
|
|
|
|
if (!st.ok()) continue;
|
|
|
|
|
const int s = std::max(1, gs_ / 2);
|
|
|
|
|
for (int j = 0; j < st.h; ++j) for (int k = 0; k < st.w; ++k) { const uint32_t p = st.px[(size_t)j * st.w + k]; if (p >> 24) cv.rect(x + k * s, y + j * s, s, s, (p & 0xFFFFFFu) | (uint32_t)((p >> 24) * tw) << 24); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
// ---- widgets ----
|
|
|
|
|
bool inRect(int x, int y, int w, int h) const { return c_->mx >= x && c_->mx < x + w && c_->my >= y && c_->my < y + h; }
|
|
|
|
|
// a text button centred on cx (or left-aligned at x with centred == false): grey, white when hovered; true when clicked
|
|
|
|
|
bool button(int x, int y, const std::string& text, bool centred = true, bool enabled = true) {
|
|
|
|
|
const int w = tw(text), h = 8 * gs_, bx = centred ? x - w / 2 : x;
|
|
|
|
|
const bool over = enabled && inRect(bx - 2 * gs_, y - 2 * gs_, w + 4 * gs_, h + 4 * gs_);
|
|
|
|
|
cv_->text(bx, y, text, !enabled ? dim() : over ? white() : grey(), gs_);
|
|
|
|
|
if (over && c_->settings && !c_->settings->cursor) {}
|
|
|
|
|
return over && clicked_;
|
|
|
|
|
}
|
|
|
|
|
void label(int x, int y, const std::string& text, uint32_t col) { cv_->text(x, y, text, col, gs_); }
|
|
|
|
|
void panel(int x, int y, int w, int h, bool gray = false) { // a 9-piece box (decorations/9piece0.png: 3-pixel corners)
|
|
|
|
|
const hud::Icon& p = gray ? as_->p9gray : as_->p9;
|
|
|
|
|
if (!p.ok()) { cv_->rect(x, y, w, h, hud::rgba(20, 20, 24, 230)); cv_->frame(x, y, w, h, hud::rgba(170, 140, 90)); return; }
|
|
|
|
|
const int cs = 3, s = gs_;
|
|
|
|
|
auto piece = [&](int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh) {
|
|
|
|
|
if (dw <= 0 || dh <= 0) return;
|
|
|
|
|
hud::Icon sub; sub.w = sw; sub.h = sh; sub.px.resize((size_t)sw * sh);
|
|
|
|
|
for (int j = 0; j < sh; ++j) for (int i = 0; i < sw; ++i) sub.px[(size_t)j * sw + i] = p.px[(size_t)(sy + j) * p.w + sx + i];
|
|
|
|
|
cv_->icon_fit(sub, dx, dy, dw, dh);
|
|
|
|
|
};
|
|
|
|
|
const int c = cs * s;
|
|
|
|
|
piece(0, 0, cs, cs, x, y, c, c); piece(p.w - cs, 0, cs, cs, x + w - c, y, c, c); piece(0, p.h - cs, cs, cs, x, y + h - c, c, c); piece(p.w - cs, p.h - cs, cs, cs, x + w - c, y + h - c, c, c);
|
|
|
|
|
piece(cs, 0, p.w - 2 * cs, cs, x + c, y, w - 2 * c, c); piece(cs, p.h - cs, p.w - 2 * cs, cs, x + c, y + h - c, w - 2 * c, c);
|
|
|
|
|
piece(0, cs, cs, p.h - 2 * cs, x, y + c, c, h - 2 * c); piece(p.w - cs, cs, cs, p.h - 2 * cs, x + w - c, y + c, c, h - 2 * c);
|
|
|
|
|
piece(cs, cs, p.w - 2 * cs, p.h - 2 * cs, x + c, y + c, w - 2 * c, h - 2 * c);
|
|
|
|
|
}
|
|
|
|
|
// the text of a toggle "Label: Value" that cycles on a click; returns true when clicked
|
|
|
|
|
bool toggle(int x, int y, const std::string& text, const std::string& value) { return button(x, y, text + " " + value, false); }
|
|
|
|
|
// a slider: the label, then a bar of `bw` px and the value as "$menu_slider_percentage" (006c6470: x 100, "%"); drag sets `v` in lo..hi
|
|
|
|
|
void slider(int x, int y, const std::string& text, float& v, int id, float lo = 0.0f, float hi = 1.0f) {
|
|
|
|
|
label(x, y, text, grey());
|
|
|
|
|
const int bx = x + tw(text) + 6 * gs_, bw = 60 * gs_, bh = 6 * gs_, by = y + gs_;
|
|
|
|
|
const float f = std::clamp((v - lo) / (hi - lo), 0.0f, 1.0f);
|
|
|
|
|
cv_->frame(bx, by, bw, bh, grey()); cv_->rect(bx + 1, by + 1, (int)((bw - 2) * f), bh - 2, hud::rgba(200, 180, 120));
|
|
|
|
|
char pct[16]; std::snprintf(pct, sizeof pct, "%d", (int)std::lround(v * 100.0f));
|
|
|
|
|
label(bx + bw + 4 * gs_, y, fmt("$menu_slider_percentage", pct), grey());
|
|
|
|
|
const bool over = inRect(bx - 2 * gs_, by - 2 * gs_, bw + 4 * gs_, bh + 4 * gs_);
|
|
|
|
|
if (over && c_->lmb && (sliderDrag_ < 0 || sliderDrag_ == id)) sliderDrag_ = id;
|
|
|
|
|
if (sliderDrag_ == id) { if (c_->lmb) v = lo + (hi - lo) * std::clamp((float)(c_->mx - bx) / (float)(bw - 2), 0.0f, 1.0f); else sliderDrag_ = -1; }
|
|
|
|
|
}
|
|
|
|
|
void buildLine() { // bottom-left: the build, like the game's "Noita - Build <date>"
|
|
|
|
|
const int s = std::max(1, gs_ / 2);
|
|
|
|
|
cv_->text(4 * s, cv_->h - 20 * s, "Noita GPU - Build " + c_->build, dim(), s);
|
|
|
|
|
if (c_->has_run) cv_->text(4 * s, cv_->h - 11 * s, "Seed: " + std::to_string(c_->seed) + " Mode: " + c_->run_mode + (c_->mods_used ? " Mods used: yes" : ""), dim(), s);
|
|
|
|
|
}
|
|
|
|
|
void logo(int cy) { // the logo at 0.8 x the GUI scale
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
if (!as.logo.ok()) { cv_->text(cv_->w / 2 - tw("Noita") , cy, "Noita", yellow(), gs_ * 2); return; }
|
|
|
|
|
const double sc = gs_ * 0.8; const int w = (int)(as.logo.w * sc), h = (int)(as.logo.h * sc); // (the wak's logo is 133 x 77: 425 px wide at 1440p like the game's)
|
|
|
|
|
cv_->icon_fit(as.logo, (cv_->w - w) / 2, cy - h / 2, w, h);
|
|
|
|
|
}
|
|
|
|
|
// ---- screens ----
|
|
|
|
|
void title(Result& r) {
|
|
|
|
|
logo((int)(cv_->h * 0.3));
|
|
|
|
|
int y = (int)(cv_->h * 0.5); const int cx = cv_->w / 2;
|
|
|
|
|
if (c_->has_run && button(cx, y, tr("$menu_continue"))) { screen = Screen::None; r.action = Action::Continue; }
|
|
|
|
|
if (c_->has_run) y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_newgame"))) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_options"))) open(Screen::Options); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_mods"))) open(Screen::Mods); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_releasenotes"))) open(Screen::ReleaseNotes); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_credits"))) open(Screen::Credits); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_quit"))) r.action = Action::Quit;
|
|
|
|
|
buildLine();
|
|
|
|
|
}
|
|
|
|
|
void pause(Result& r) {
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
int y = 6 * gs_;
|
|
|
|
|
if (as.help.ok()) { const double sc = gs_ * 0.9; const int w = (int)(as.help.w * sc), h = (int)(as.help.h * sc); cv_->icon_fit(as.help, (cv_->w - w) / 2, y, w, h); y += h + 4 * gs_; }
|
|
|
|
|
const int cx = cv_->w / 2;
|
|
|
|
|
cv_->text(cx - tw(tr("$menu_paused")) / 2, y, tr("$menu_paused"), dim(), gs_); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_continue"))) { screen = Screen::None; r.action = Action::Continue; } y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_newgame"))) confirm(tr("$menu_newgame_areyousure"), Action::None); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_progress"))) open(Screen::Progress); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_options"))) open(Screen::Options); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_mods"))) open(Screen::Mods); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_releasenotes"))) open(Screen::ReleaseNotes); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_credits"))) open(Screen::Credits); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menu_saveandquit"))) r.action = Action::SaveQuit;
|
|
|
|
|
buildLine();
|
|
|
|
|
}
|
|
|
|
|
void confirmScreen(Result& r) { // "Are you sure ...?" Yes / No
|
|
|
|
|
const int w = 180 * gs_, h = 50 * gs_, x = (cv_->w - w) / 2, y = (cv_->h - h) / 2;
|
|
|
|
|
panel(x, y, w, h);
|
|
|
|
|
std::vector<std::string> lines; wrap(confirmText_, w - 12 * gs_, lines);
|
|
|
|
|
int ty = y + 6 * gs_;
|
|
|
|
|
for (const std::string& l : lines) { label(x + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
|
|
|
|
if (button(x + w / 3, y + h - 12 * gs_, tr("$menu_yes"))) {
|
|
|
|
|
if (confirmAction_ == Action::None) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } // (a new game from the pause menu: the game mode panel)
|
|
|
|
|
else { r.action = confirmAction_; screen = confirmBack_; }
|
|
|
|
|
}
|
|
|
|
|
if (button(x + 2 * w / 3, y + h - 12 * gs_, tr("$menu_no"))) screen = confirmBack_;
|
|
|
|
|
}
|
|
|
|
|
void wrap(const std::string& text, int width, std::vector<std::string>& out) const {
|
|
|
|
|
std::string line, word;
|
|
|
|
|
auto flush = [&]() { out.push_back(line); line.clear(); };
|
|
|
|
|
for (size_t i = 0; i <= text.size(); ++i) {
|
|
|
|
|
const char ch = i < text.size() ? text[i] : ' ';
|
|
|
|
|
if (ch == '\\' && i + 1 < text.size() && text[i + 1] == 'n') { line += (line.empty() || word.empty() ? "" : " ") + word; word.clear(); flush(); ++i; continue; }
|
|
|
|
|
if (ch == '\n') { line += (line.empty() || word.empty() ? "" : " ") + word; word.clear(); flush(); continue; }
|
|
|
|
|
if (ch == ' ') {
|
|
|
|
|
const std::string cand = line.empty() ? word : line + " " + word;
|
|
|
|
|
if (!line.empty() && tw(cand) > width) { flush(); line = word; } else line = cand;
|
|
|
|
|
word.clear();
|
|
|
|
|
} else word += ch;
|
|
|
|
|
}
|
|
|
|
|
if (!line.empty()) out.push_back(line);
|
|
|
|
|
}
|
|
|
|
|
void newGame(Result& r) {
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
const std::vector<GameMode>& modes = *c_->modes;
|
|
|
|
|
// (006ceb00: the banners in a framed column, the description box beside it taking the rest of a 274-unit panel, the bar below; nothing selected at first)
|
|
|
|
|
const int bw = 64 * gs_, bh = 36 * gs_, gap = 2 * gs_, pw = 208 * gs_;
|
|
|
|
|
const int colX = (cv_->w - (bw + pw + 4 * gs_)) / 2, top = (int)(cv_->h * 0.3);
|
|
|
|
|
const int ph = std::max(4, (int)modes.size()) * (bh + gap) + gap;
|
|
|
|
|
label(colX, top - 12 * gs_, tr("$menu_newgame_selectgamemode"), grey());
|
|
|
|
|
panel(colX - gap, top - gap, bw + 2 * gap, ph, true);
|
|
|
|
|
for (size_t i = 0; i < modes.size(); ++i) {
|
|
|
|
|
const int y = top + (int)i * (bh + gap);
|
|
|
|
|
const hud::Icon* bg = &as.bannerNormalBg; const hud::Icon* fg = &as.bannerNormalFg;
|
|
|
|
|
if (modes[i].id == "dailyrun") { bg = &as.bannerDailyBg; fg = &as.bannerDailyFg; }
|
|
|
|
|
if (modes[i].mod) { bg = modeIcon(modes[i].banner_bg, bg); fg = modeIcon(modes[i].banner_fg, fg); }
|
|
|
|
|
if (bg && bg->ok()) cv_->icon_fit(*bg, colX, y, bw, bh); else cv_->rect(colX, y, bw, bh, hud::rgba(60, 50, 70));
|
|
|
|
|
if (fg && fg->ok()) cv_->icon_fit(*fg, colX, y, bw, bh);
|
|
|
|
|
const bool over = inRect(colX, y, bw, bh);
|
|
|
|
|
if (over) selMode_ = (int)i; // the banner under the mouse is the one described (it stays until another is hovered)
|
|
|
|
|
if ((int)i == selMode_ && as.bannerSel.ok()) cv_->icon_fit(as.bannerSel, colX, y, bw, bh);
|
|
|
|
|
if (over && clicked_) { selMode_ = (int)i; startMode(r); }
|
|
|
|
|
}
|
|
|
|
|
// the description box
|
|
|
|
|
const int px = colX + bw + 4 * gs_;
|
|
|
|
|
panel(px, top - gap, pw, ph, false);
|
|
|
|
|
if (selMode_ >= 0 && selMode_ < (int)modes.size()) {
|
|
|
|
|
const GameMode& m = modes[selMode_];
|
|
|
|
|
int ty = top + 4 * gs_;
|
|
|
|
|
label(px + 6 * gs_, ty, tr(m.name), yellow()); ty += lineH();
|
|
|
|
|
std::vector<std::string> lines; wrap(tr(m.desc), pw - 12 * gs_, lines);
|
|
|
|
|
for (const std::string& l : lines) { label(px + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
|
|
|
|
if (m.id == "dailyrun" || m.id == "daily_practice") {
|
|
|
|
|
ty += 4 * gs_;
|
|
|
|
|
std::string st;
|
|
|
|
|
if (!c_->settings || !c_->settings->online) st = tr("$gamemode_dailyrun_desc_offline");
|
|
|
|
|
else if (daily.state.load() == 1) st = "Connecting...";
|
|
|
|
|
else if (daily.state.load() == 3) st = tr("$gamemode_dailyrun_desc_connection_failed");
|
|
|
|
|
else if (daily.state.load() == 2) {
|
|
|
|
|
if (m.id == "dailyrun" && last_daily_played == daily.seed) st = tr("$gamemode_dailyrun_desc_played");
|
|
|
|
|
else st = (m.id == "dailyrun" ? "Daily run seed: " : "Daily practice seed: ") + std::to_string(m.id == "dailyrun" ? daily.seed : daily.practice);
|
|
|
|
|
}
|
|
|
|
|
lines.clear(); wrap(st, pw - 12 * gs_, lines);
|
|
|
|
|
for (const std::string& l : lines) { label(px + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// the bar below: the streamer mode, the mod settings
|
|
|
|
|
const int by = top + ph + 2 * gs_;
|
|
|
|
|
panel(colX - gap, by, bw + pw + 4 * gs_ + 2 * gap, 14 * gs_, true);
|
|
|
|
|
{ // 006ceb00: "Streamer mode: on/off" is the Twitch integration's switch; switching it on opens the Options at the Streaming tab (to connect)
|
|
|
|
|
Settings* S = c_->settings; const bool sm = S && S->streamer_mode;
|
|
|
|
|
if (toggle(colX + 4 * gs_, by + 3 * gs_, tr("$menu_streamermode").substr(0, tr("$menu_streamermode").find("$0")), tr(sm ? "$option_on" : "$option_off")) && S) {
|
|
|
|
|
S->streamer_mode = !S->streamer_mode; S->save();
|
|
|
|
|
if (S->streamer_mode) { open(Screen::Options); tab_ = 4; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (button(colX + bw + pw - tw(tr("$menu_mods_settings_short")), by + 3 * gs_, tr("$menu_mods_settings_short"), false)) { open(Screen::Options); tab_ = 5; }
|
|
|
|
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = c_->dead ? Screen::GameOverStats : (inGame_ ? Screen::Pause : Screen::Title);
|
|
|
|
|
}
|
|
|
|
|
std::map<std::string, hud::Icon> modeIcons_;
|
|
|
|
|
const hud::Icon* modeIcon(const std::string& file, const hud::Icon* fallback) {
|
|
|
|
|
if (file.empty()) return fallback;
|
|
|
|
|
auto it = modeIcons_.find(file);
|
|
|
|
|
if (it == modeIcons_.end()) { hud::Icon ic; if (loose_icon) ic = loose_icon(file); it = modeIcons_.emplace(file, std::move(ic)).first; }
|
|
|
|
|
return it->second.ok() ? &it->second : fallback;
|
|
|
|
|
}
|
|
|
|
|
public:
|
|
|
|
|
std::function<hud::Icon(const std::string&)> loose_icon; // a mod's banner (mods/<id>/files/...), read through the wak's loose root
|
|
|
|
|
private:
|
|
|
|
|
bool canStart(const GameMode& m) const {
|
|
|
|
|
if (m.id == "dailyrun") return daily.state.load() == 2 && last_daily_played != daily.seed;
|
|
|
|
|
if (m.id == "daily_practice") return daily.state.load() == 2;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
void startMode(Result& r) {
|
|
|
|
|
const GameMode& m = (*c_->modes)[selMode_];
|
|
|
|
|
if (!canStart(m)) return;
|
|
|
|
|
r.action = Action::StartGame; r.mode_id = m.id;
|
|
|
|
|
if (m.id == "dailyrun") { r.seed = daily.seed; r.daily = true; }
|
|
|
|
|
else if (m.id == "daily_practice") { r.seed = daily.practice; r.daily_practice = true; }
|
|
|
|
|
else { uint32_t s = (uint32_t)GetTickCount() ^ (uint32_t)std::time(nullptr) * 2654435761u; r.seed = s ? s : 1u; }
|
|
|
|
|
screen = Screen::None;
|
|
|
|
|
}
|
|
|
|
|
void options(Result& r) {
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
static const char* tabs[6] = {"General", "Graphics", "Audio", "Input", "Streaming", "Mod settings"};
|
|
|
|
|
const int pw = std::min(cv_->w - 16 * gs_, 280 * gs_), px = (cv_->w - pw) / 2, py = 24 * gs_, ph = cv_->h - py - 24 * gs_;
|
|
|
|
|
int tx = px + 4 * gs_;
|
|
|
|
|
for (int i = 0; i < 6; ++i) {
|
|
|
|
|
const int tw0 = 44 * gs_, th = 10 * gs_;
|
|
|
|
|
const bool sel = tab_ == i, over = inRect(tx, py - th, tw0, th);
|
|
|
|
|
const hud::Icon& ic = sel ? as.tabSel : (over ? as.tabHover : as.tab);
|
|
|
|
|
if (ic.ok()) cv_->icon_fit(ic, tx, py - th - (sel ? gs_ : 0), tw0, th + (sel ? gs_ : 0)); else cv_->frame(tx, py - th, tw0, th, grey());
|
|
|
|
|
const std::string t = tabs[i]; const int s = std::max(1, gs_ * 2 / 3);
|
|
|
|
|
cv_->text(tx + (tw0 - cv_->text_width(t, s)) / 2, py - th + 3 * gs_ - (sel ? gs_ : 0), t, sel ? white() : grey(), s);
|
|
|
|
|
if (over && clicked_) { tab_ = i; scroll_ = 0; bindIdx_ = -1; }
|
|
|
|
|
tx += tw0 + gs_;
|
|
|
|
|
}
|
|
|
|
|
panel(px, py, pw, ph);
|
|
|
|
|
Settings& S = *c_->settings;
|
|
|
|
|
int x = px + 10 * gs_, y = py + 8 * gs_ - scroll_;
|
|
|
|
|
auto header = [&](const char* t) { label(x, y, t, dim()); y += lineH(); };
|
|
|
|
|
auto onoff = [&](bool b) { return tr(b ? "$option_on" : "$option_off"); };
|
|
|
|
|
auto opt = [&](const char* key, bool& b) { if (toggle(x, y, tr(key).substr(0, tr(key).find("$0")), onoff(b))) b = !b; y += lineH(); };
|
|
|
|
|
if (tab_ == 0) {
|
|
|
|
|
header("WINDOW");
|
|
|
|
|
if (toggle(x, y, "Window mode:", S.window_mode == 0 ? "Windowed" : "Fullscreen")) S.window_mode = 1 - S.window_mode; y += lineH();
|
|
|
|
|
label(x, y, "Resolution: " + std::to_string(c_->w) + " x " + std::to_string(c_->h), grey()); y += lineH();
|
|
|
|
|
opt("$menuoptions_vsync", S.vsync);
|
|
|
|
|
y += 4 * gs_; header("COMPATIBILITY");
|
|
|
|
|
opt("$menuoptions_application_rendered_cursor", S.cursor);
|
|
|
|
|
y += 4 * gs_; header("REPLAY RECORDER");
|
|
|
|
|
opt("$menuoptions_replayrecorder", S.replay_recorder);
|
|
|
|
|
y += 4 * gs_; header("ONLINE");
|
|
|
|
|
if (toggle(x, y, "Online features:", onoff(S.online))) S.online = !S.online; y += lineH();
|
|
|
|
|
y += 4 * gs_; header("MISCELLANEOUS");
|
|
|
|
|
label(x, y, tr("$menuoptions_language"), grey()); y += lineH();
|
|
|
|
|
if (button(x, y, tr("$menuoptions_resetsave"), false)) confirm(tr("$menuoptions_resetsave_areyousure"), Action::ResetProgress);
|
|
|
|
|
y += lineH();
|
|
|
|
|
} else if (tab_ == 1) {
|
|
|
|
|
header(tr("$menuoptions_heading_rendering").c_str());
|
|
|
|
|
slider(x, y, tr("$menuoptions_brightness"), S.brightness, 1, Settings::kBrightMin, Settings::kBrightMax); y += lineH();
|
|
|
|
|
slider(x, y, tr("$menuoptions_contrast"), S.contrast, 2, Settings::kContrastMin, Settings::kContrastMax); y += lineH();
|
|
|
|
|
slider(x, y, tr("$menuoptions_gamma"), S.gamma, 3, Settings::kGammaMin, Settings::kGammaMax); y += lineH();
|
|
|
|
|
slider(x, y, tr("$menuoptions_cosmeticparticlecoeff"), S.cosmetic_particles, 4, Settings::kCosmeticMin, 1.0f); y += lineH();
|
|
|
|
|
r.action = Action::ApplySettings; // (the picture follows the sliders as they move)
|
|
|
|
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_graphics").c_str());
|
|
|
|
|
opt("$menuoptions_damagenumbers", S.damage_numbers);
|
|
|
|
|
y += 4 * gs_; header(tr("$menuoptions_heading_accessibility").c_str());
|
|
|
|
|
slider(x, y, tr("$menuoptions_screenshake_intensity"), S.screenshake, 5); y += lineH();
|
|
|
|
|
} else if (tab_ == 2) {
|
|
|
|
|
header(tr("$menuoptions_heading_volume").c_str());
|
|
|
|
|
slider(x, y, tr("$menuoptions_musicvolume"), S.music, 6); y += lineH();
|
|
|
|
|
slider(x, y, tr("$menuoptions_soundsvolume"), S.sfx, 7); y += lineH();
|
|
|
|
|
} else if (tab_ == 3) {
|
|
|
|
|
header(tr("$menuoptions_heading_controls").c_str());
|
|
|
|
|
for (size_t i = 0; i < S.keys.size(); ++i) {
|
|
|
|
|
KeyBind& k = S.keys[i];
|
|
|
|
|
const std::string v = bindIdx_ == (int)i ? "..." : keyName(k.key);
|
|
|
|
|
if (toggle(x, y, std::string(k.name) + ":", v)) bindIdx_ = (int)i;
|
|
|
|
|
y += 9 * gs_;
|
|
|
|
|
}
|
|
|
|
|
if (bindIdx_ >= 0 && c_->key_pressed) { if (c_->key_pressed != VK_ESCAPE) S.keys[bindIdx_].key = c_->key_pressed; bindIdx_ = -1; }
|
|
|
|
|
label(x, y, tr("$menuoptions_reset_keyboard"), dim()); y += lineH();
|
|
|
|
|
if (button(x, y, tr("$menuoptions_configurecontrols_reset_all"), false)) { Settings d; S.keys = d.keys; } y += lineH();
|
|
|
|
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_mouse").c_str());
|
|
|
|
|
opt("$menuoptions_ui_inventory_icons_always_clickable", S.click_switches_item);
|
|
|
|
|
opt("$menuoptions_showhoverinfonexttomouse", S.hover_info_at_mouse);
|
|
|
|
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_input").c_str());
|
|
|
|
|
opt("$menuoptions_pause_the_game_when_unfocused", S.pause_unfocused);
|
|
|
|
|
} else if (tab_ == 4) { // 006d5620: TWITCH CHANNEL - Connect / Disconnect (the OAuth sign-in and the chat votes), then the vote settings; the switch itself is the streamer mode
|
|
|
|
|
header(tr("$menuoptions_heading_streaming_integration").c_str());
|
|
|
|
|
if (toggle(x, y, tr("$menu_streamermode").substr(0, tr("$menu_streamermode").find("$0")), onoff(S.streamer_mode))) S.streamer_mode = !S.streamer_mode; y += lineH();
|
|
|
|
|
button(x, y, tr("$menu_streaming_connect_twitch"), false, false); y += lineH();
|
|
|
|
|
std::vector<std::string> lines; wrap("The Twitch sign-in and the chat votes are not part of this build (to-do #47).", pw - 20 * gs_, lines);
|
|
|
|
|
for (const std::string& l : lines) { label(x, y, l, dim()); y += 9 * gs_; }
|
|
|
|
|
} else {
|
|
|
|
|
header("MODS");
|
|
|
|
|
if (!c_->mods || c_->mods->empty()) label(x, y, "No mods enabled.", grey());
|
|
|
|
|
for (const std::string& m : c_->mods ? *c_->mods : std::vector<std::string>()) { label(x, y, m + " - " + tr("$menu_mods_nosettings"), grey()); y += 9 * gs_; }
|
|
|
|
|
}
|
|
|
|
|
scrollBy(c_->wheel * 12 * gs_, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
|
|
|
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_applyandreturn"), false)) { S.save(); r.action = Action::ApplySettings; screen = back(); }
|
|
|
|
|
}
|
|
|
|
|
void scrollBy(int d, int maxScroll) { scroll_ = std::clamp(scroll_ + d, 0, std::max(0, maxScroll)); }
|
|
|
|
|
static std::string keyName(int k) {
|
|
|
|
|
if (k >= 'A' && k <= 'Z') return std::string(1, (char)k);
|
|
|
|
|
if (k >= '0' && k <= '9') return std::string(1, (char)k);
|
|
|
|
|
switch (k) { case VK_TAB: return "Tab"; case VK_SPACE: return "Space"; case VK_LEFT: return "Left"; case VK_RIGHT: return "Right"; case VK_UP: return "Up"; case VK_DOWN: return "Down";
|
|
|
|
|
case VK_SHIFT: return "Shift"; case VK_CONTROL: return "Ctrl"; case VK_RETURN: return "Enter"; case VK_LBUTTON: return "LMB"; case VK_RBUTTON: return "RMB"; default: break; }
|
|
|
|
|
char b[16]; std::snprintf(b, sizeof b, "key %d", k); return b;
|
|
|
|
|
}
|
|
|
|
|
void textScreen(const std::string& titleText, const std::vector<std::string>& lines, bool autoScroll) {
|
|
|
|
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
|
|
|
|
label(px, py - 10 * gs_, titleText, grey());
|
|
|
|
|
panel(px, py, pw, ph);
|
|
|
|
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
|
|
|
|
const int total = (int)lines.size() * lh;
|
|
|
|
|
if (autoScroll) { creditsT_ += 1.0; scroll_ = std::clamp((int)(creditsT_ * 0.5 * s) - ph / 2, 0, std::max(0, total - ph + 20 * gs_)); scrollBy(c_->wheel * 3 * lh, total - ph + 20 * gs_); }
|
|
|
|
|
else scrollBy(c_->wheel * 3 * lh, total - ph + 20 * gs_);
|
|
|
|
|
int y = py + 6 * gs_ - scroll_;
|
|
|
|
|
for (const std::string& l : lines) {
|
|
|
|
|
uint32_t col = grey(); std::string t = l;
|
|
|
|
|
if (!t.empty() && t[0] == '#') { col = yellow(); t = t.substr(1); }
|
|
|
|
|
else if (!t.empty() && t[0] == '*') { col = white(); t = t.substr(1); }
|
|
|
|
|
std::vector<std::string> parts; // (a long line wraps at the box's width)
|
|
|
|
|
std::string line, word;
|
|
|
|
|
for (size_t i = 0; i <= t.size(); ++i) {
|
|
|
|
|
const char ch = i < t.size() ? t[i] : ' ';
|
|
|
|
|
if (ch == ' ') { const std::string cand = line.empty() ? word : line + " " + word; if (!line.empty() && cv_->text_width(cand, s) > pw - 12 * gs_) { parts.push_back(line); line = word; } else line = cand; word.clear(); }
|
|
|
|
|
else word += ch;
|
|
|
|
|
}
|
|
|
|
|
parts.push_back(line);
|
|
|
|
|
for (size_t k = 0; k < parts.size(); ++k) {
|
|
|
|
|
if (y + lh >= py + 3 * gs_ && y < py + ph - 3 * gs_) cv_->text(px + 6 * gs_ + (k ? 8 * s : 0), y, parts[k], k ? grey() : col, s);
|
|
|
|
|
y += lh;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = back();
|
|
|
|
|
}
|
|
|
|
|
void releaseNotes() {
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
if (!c_->release_notes || c_->release_notes->empty()) lines.push_back("No release notes beside the executable (release_notes.txt).");
|
|
|
|
|
else { std::string cur; for (char ch : *c_->release_notes) { if (ch == '\n') { lines.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; } if (!cur.empty()) lines.push_back(cur); }
|
|
|
|
|
textScreen(tr("$menu_releasenotes"), lines, false);
|
|
|
|
|
}
|
|
|
|
|
void credits() {
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
std::string cur; for (char ch : as_->credits) { if (ch == '\n') { lines.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; }
|
|
|
|
|
if (!cur.empty()) lines.push_back(cur);
|
|
|
|
|
std::vector<std::string> ours = {"*NOITA GPU", "A Vulkan rebuild of Noita's engine.", "", "*ORIGINAL GAME", "Noita by Nolla Games - the art, the data and the design below are theirs.", ""};
|
|
|
|
|
ours.insert(ours.end(), lines.begin(), lines.end());
|
|
|
|
|
textScreen(tr("$menu_credits"), ours, true);
|
|
|
|
|
}
|
|
|
|
|
// The Mods screen: the mods of mods/ in load order, a click on the name enables / disables (the game's: "Click mod name to enable/disable the mod"), Move up /
|
|
|
|
|
// Move down reorder, the list is written at once (save/mod_config.xml, the game's layout) and applies at the next start - "Restart with enabled mods active"
|
|
|
|
|
// relaunches the run, "Start a new game with enabled mods active" opens the game modes. A game-mode mod is played from the New game menu, not enabled here.
|
|
|
|
|
void mods(Result& r) {
|
|
|
|
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
|
|
|
|
label(px, py - 10 * gs_, tr("$menu_mods"), grey());
|
|
|
|
|
panel(px, py, pw, ph);
|
|
|
|
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
|
|
|
|
int x = px + 6 * gs_, y = py + 6 * gs_ - scroll_;
|
|
|
|
|
std::vector<std::string> help; wrap(tr("$menu_mods_help"), pw - 12 * gs_, help);
|
|
|
|
|
for (const std::string& l : help) { cv_->text(x, y, l, dim(), s); y += lh; }
|
|
|
|
|
y += lh;
|
|
|
|
|
cv_->text(x, y, tr("$menu_mods_availablemods"), white(), s); y += lh + 2 * gs_;
|
|
|
|
|
std::vector<ModInfo>* mods = c_->all_mods;
|
|
|
|
|
if (!mods || mods->empty()) { cv_->text(x, y, tr("$menu_mods_availablemods_none"), grey(), s); y += lh; }
|
|
|
|
|
std::string tip; int tipY = 0;
|
|
|
|
|
for (size_t i = 0; mods && i < mods->size(); ++i) {
|
|
|
|
|
ModInfo& m = (*mods)[i];
|
|
|
|
|
const std::string box = m.game_mode ? " - " : m.enabled ? "[x]" : "[ ]";
|
|
|
|
|
const std::string line = box + " " + (m.name.empty() ? m.id : m.name);
|
|
|
|
|
const int w = cv_->text_width(line, s);
|
|
|
|
|
const bool over = inRect(x - gs_, y - gs_, w + 2 * gs_, lh);
|
|
|
|
|
cv_->text(x, y, line, m.game_mode ? dim() : over || m.enabled ? white() : grey(), s);
|
|
|
|
|
if (over && clicked_ && !m.game_mode) { m.enabled = !m.enabled; r.action = Action::ApplyMods; }
|
|
|
|
|
if (over) { tip = m.desc + (m.game_mode ? tr("$menu_mods_customactivation") : ""); tipY = y; }
|
|
|
|
|
const int bx = px + pw - 60 * gs_; // Move up / Move down (small buttons on the row)
|
|
|
|
|
auto sbtn = [&](int sx, const std::string& t2) { const int w2 = cv_->text_width(t2, s); const bool ov = inRect(sx - gs_, y - gs_, w2 + 2 * gs_, lh); cv_->text(sx, y, t2, ov ? white() : dim(), s); return ov && clicked_; };
|
|
|
|
|
if (i > 0 && sbtn(bx, tr("$menu_mods_moveup"))) { std::swap((*mods)[i], (*mods)[i - 1]); r.action = Action::ApplyMods; }
|
|
|
|
|
if (i + 1 < mods->size() && sbtn(bx + 26 * gs_, tr("$menu_mods_movedown"))) { std::swap((*mods)[i], (*mods)[i + 1]); r.action = Action::ApplyMods; }
|
|
|
|
|
y += lh;
|
|
|
|
|
}
|
|
|
|
|
y += lh;
|
|
|
|
|
cv_->text(x, y, tr("$menu_mods_help_paused"), dim(), s); y += lh + gs_;
|
|
|
|
|
if (button(x, y, tr("$menu_mods_openmodfolder"), false)) r.action = Action::OpenModFolder; y += lineH();
|
|
|
|
|
if (c_->has_run && button(x, y, tr("$menu_mods_restart"), false)) { r.action = Action::RestartMods; screen = Screen::None; } if (c_->has_run) y += lineH();
|
|
|
|
|
if (button(x, y, tr("$menu_mods_newgame"), false)) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } y += lineH();
|
|
|
|
|
if (!tip.empty()) { // the mod's description beside the list
|
|
|
|
|
std::vector<std::string> lines; wrap(tip, 110 * gs_, lines);
|
|
|
|
|
const int bw = 116 * gs_, bh = (int)lines.size() * lh + 6 * gs_, bx = std::min(cv_->w - bw - 2 * gs_, c_->mx + 6 * gs_), by = std::min(cv_->h - bh - 2 * gs_, tipY);
|
|
|
|
|
panel(bx, by, bw, bh, true);
|
|
|
|
|
int ly = by + 3 * gs_; for (const std::string& l : lines) { cv_->text(bx + 3 * gs_, ly, l, grey(), s); ly += lh; }
|
|
|
|
|
}
|
|
|
|
|
scrollBy(c_->wheel * 3 * lh, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
|
|
|
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = back();
|
|
|
|
|
}
|
|
|
|
|
// The Progress screen (006dfe40): three grids - the perks (9 columns), the spells (12) and the enemies (9) - each cell a grid_box with the icon when the
|
|
|
|
|
// perk / spell was picked up (the perk_picked_<id> / action_<id> persistent flag) or the enemy killed, grid_box_unknown with icon_unknown otherwise; an entry
|
|
|
|
|
// new since the screen was last seen pulses with grid_highlight_new. Hovering shows the name and the description, "???" when unknown (the enemies: Kills /
|
|
|
|
|
// Killed by). Below: the endings discovered and the secrets found.
|
|
|
|
|
void progress(Result& r) {
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
|
|
|
|
label(px, py - 10 * gs_, tr("$menu_progress"), grey());
|
|
|
|
|
panel(px, py, pw, ph);
|
|
|
|
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
|
|
|
|
const int cell = (as.gridBox.ok() ? as.gridBox.w : 20) * gs_, pitch = cell + gs_;
|
|
|
|
|
int x = px + 6 * gs_, y = py + 6 * gs_ - scroll_;
|
|
|
|
|
const ProgressItem* hot = nullptr; bool hotEnemy = false;
|
|
|
|
|
auto grid = [&](const char* header, const std::vector<ProgressItem>* items, int cols, bool enemies) {
|
|
|
|
|
if (!items) return;
|
|
|
|
|
int known = 0; for (const ProgressItem& it : *items) known += it.known;
|
|
|
|
|
cv_->text(x, y, tr(header) + " " + std::to_string(known) + " / " + std::to_string(items->size()), white(), s); y += lh + gs_;
|
|
|
|
|
cols = std::max(1, std::min(cols, (pw - 12 * gs_) / pitch));
|
|
|
|
|
for (size_t i = 0; i < items->size(); ++i) {
|
|
|
|
|
const ProgressItem& it = (*items)[i];
|
|
|
|
|
const int cx = x + (int)(i % cols) * pitch, cy = y + (int)(i / cols) * pitch;
|
|
|
|
|
if (cy + cell < py + 3 * gs_ || cy > py + ph - 3 * gs_) continue; // (scrolled out of the box)
|
|
|
|
|
const hud::Icon& box = it.known || !as.gridBoxUnknown.ok() ? as.gridBox : as.gridBoxUnknown;
|
|
|
|
|
if (box.ok()) cv_->icon_fit(box, cx, cy, cell, cell); else cv_->frame(cx, cy, cell, cell, it.known ? grey() : dim());
|
|
|
|
|
const hud::Icon* ic = it.known ? iconOf(it.icon) : nullptr;
|
|
|
|
|
if (!ic || !ic->ok()) ic = &as.iconUnknown;
|
|
|
|
|
if (ic->ok()) { const int iw = std::min(cell - 4 * gs_, ic->w * gs_), ih = std::min(cell - 4 * gs_, ic->h * gs_); cv_->icon_fit(*ic, cx + (cell - iw) / 2, cy + (cell - ih) / 2, iw, ih); }
|
|
|
|
|
if (it.is_new && it.known && as.gridNew.ok()) { // (006df000: the highlight's alpha follows a sine of the frame)
|
|
|
|
|
hud::Icon hi = as.gridNew; const double a = 0.5 + 0.5 * std::sin(tick_ * 5.0 + i * 0.7);
|
|
|
|
|
for (uint32_t& p : hi.px) p = (p & 0xFFFFFFu) | (uint32_t)((p >> 24) * a) << 24;
|
|
|
|
|
cv_->icon_fit(hi, cx, cy, cell, cell);
|
|
|
|
|
}
|
|
|
|
|
if (inRect(cx, cy, cell, cell) && cy >= py && cy + cell <= py + ph) { hot = ⁢ hotEnemy = enemies; }
|
|
|
|
|
}
|
|
|
|
|
y += ((int)items->size() + cols - 1) / cols * pitch + lh;
|
|
|
|
|
};
|
|
|
|
|
grid("$progress_perks", c_->perks, 9, false);
|
|
|
|
|
grid("$progress_actions", c_->actions, 12, false);
|
|
|
|
|
grid("$progress_enemies", c_->enemies, 9, true);
|
|
|
|
|
if (c_->ending0 || c_->ending1) { cv_->text(x, y, tr(c_->ending0 && c_->ending1 ? "$progress_ending01" : c_->ending0 ? "$progress_ending0" : "$progress_ending1"), grey(), s); y += lh; }
|
|
|
|
|
cv_->text(x, y, fmt("$progress_completed", std::to_string(c_->secrets_found), ""), grey(), s); y += lh;
|
|
|
|
|
if (hot) { // the information box of the cell under the mouse
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
if (!hot->known) lines.push_back(tr("$progress_unknown"));
|
|
|
|
|
else {
|
|
|
|
|
lines.push_back(hot->is_new ? fmt("$progress_new", tr(hot->name)) : tr(hot->name));
|
|
|
|
|
if (hotEnemy) { lines.push_back(fmt("$progress_kills", std::to_string(hot->kills))); lines.push_back(fmt("$progress_killed_by", std::to_string(hot->killed_by))); }
|
|
|
|
|
else wrap(tr(hot->desc), 110 * gs_, lines);
|
|
|
|
|
}
|
|
|
|
|
const int bw = 116 * gs_, bh = (int)lines.size() * lh + 6 * gs_, bx = std::min(cv_->w - bw - 2 * gs_, c_->mx + 8 * gs_), by = std::min(cv_->h - bh - 2 * gs_, c_->my + 8 * gs_);
|
|
|
|
|
panel(bx, by, bw, bh, true);
|
|
|
|
|
int ly = by + 3 * gs_; for (size_t k = 0; k < lines.size(); ++k) { cv_->text(bx + 3 * gs_, ly, lines[k], k == 0 ? white() : grey(), s); ly += lh; }
|
|
|
|
|
}
|
|
|
|
|
scrollBy(c_->wheel * 3 * lh, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
|
|
|
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) { r.action = Action::ProgressSeen; screen = back(); }
|
|
|
|
|
}
|
|
|
|
|
const hud::Icon* iconOf(const std::string& file) { return modeIcon(file, nullptr); } // (any image of the wak or a mod, cached)
|
|
|
|
|
void gameOver(Result& r) {
|
|
|
|
|
gameOverT_ += 1.0 / 60.0;
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
const double fade = std::min(1.0, gameOverT_ / 1.5);
|
|
|
|
|
const int cx = cv_->w / 2; int y = (int)(cv_->h * 0.18);
|
|
|
|
|
if (as.gameOver.ok()) { const int w = as.gameOver.w * gs_ / 1, h = as.gameOver.h * gs_; const int ww = std::min(w, cv_->w - 16 * gs_), hh = h * ww / std::max(1, w); cv_->icon_fit(as.gameOver, cx - ww / 2, y - (int)(10 * gs_ * (1.0 - fade)), ww, hh); y += hh + 4 * gs_; }
|
|
|
|
|
if (gameOverT_ < 1.0) return;
|
|
|
|
|
std::string cause = tr("$stat_cause_of_death"); const size_t p = cause.find("$0"); if (p != std::string::npos) cause.replace(p, 2, cause_);
|
|
|
|
|
cv_->text(cx - tw(cause) / 2, y, cause, grey(), gs_); y += lineH();
|
|
|
|
|
if (button(cx, y, tr("$menugameover_nextbutton"))) screen = Screen::GameOverStats;
|
|
|
|
|
(void)r;
|
|
|
|
|
}
|
|
|
|
|
void gameOverStats(Result& r) {
|
|
|
|
|
const Assets& as = *as_;
|
|
|
|
|
const int cx = cv_->w / 2; int y = (int)(cv_->h * 0.18);
|
|
|
|
|
if (as.gameOver.ok()) { const int w = as.gameOver.w * gs_, h = as.gameOver.h * gs_; const int ww = std::min(w, cv_->w - 16 * gs_), hh = h * ww / std::max(1, w); cv_->icon_fit(as.gameOver, cx - ww / 2, y, ww, hh); y += hh - 6 * gs_; }
|
|
|
|
|
const int pw = 192 * gs_, px = cx - pw / 2, ph = std::min(200 * gs_, cv_->h - y - 4 * gs_); // (006e51e0: the box of 192 x 200 units under the title)
|
|
|
|
|
panel(px, y, pw, ph);
|
|
|
|
|
int ty = y + 12 * gs_; const int lx = px + 36 * gs_;
|
|
|
|
|
auto row = [&](const std::string& key, const std::string& val, const std::string& best) {
|
|
|
|
|
label(lx, ty, tr(key), grey());
|
|
|
|
|
cv_->text(lx + tw(tr(key)) + 3 * gs_, ty, val, yellow(), gs_);
|
|
|
|
|
if (!best.empty()) { std::string b = tr("$stat_best"); const size_t p = b.find("$0"); if (p != std::string::npos) b.replace(p, 2, best); cv_->text(lx + tw(tr(key)) + tw(val) + 6 * gs_, ty, b, dim(), gs_); }
|
|
|
|
|
ty += 9 * gs_;
|
|
|
|
|
};
|
|
|
|
|
std::string cause = tr("$stat_cause_of_death"); const size_t p = cause.find("$0"); if (p != std::string::npos) cause.replace(p, 2, cause_);
|
|
|
|
|
label(lx, ty, cause, grey()); ty += 9 * gs_;
|
|
|
|
|
row("$stat_modsenabled", tr(c_->mods_used ? "$menu_yes" : "$menu_no"), "");
|
|
|
|
|
auto timeStr = [](double s) { char b[32]; const int t = (int)s; std::snprintf(b, sizeof b, "%d:%02d:%02d", t / 3600, (t / 60) % 60, t % 60); return std::string(b); };
|
|
|
|
|
row("$stat_gold", std::to_string(c_->stats.gold), std::to_string(c_->best.gold));
|
|
|
|
|
row("$stat_time", timeStr(c_->stats.playtime), timeStr(c_->best.playtime));
|
|
|
|
|
row("$stat_depth", std::to_string(c_->stats.depth) + "m", std::to_string(c_->best.depth) + "m");
|
|
|
|
|
row("$stat_places_visited", std::to_string(c_->stats.places), std::to_string(c_->best.places));
|
|
|
|
|
row("$stat_enemies_slain", std::to_string(c_->stats.kills), std::to_string(c_->best.kills));
|
|
|
|
|
row("$stat_max_hp", std::to_string(c_->stats.maxhp), std::to_string(c_->best.maxhp));
|
|
|
|
|
row("$stat_items_found", std::to_string(c_->stats.items), std::to_string(c_->best.items));
|
|
|
|
|
row("$stat_total_deaths", std::to_string(c_->total_deaths), "");
|
|
|
|
|
row("$stat_total_wins", std::to_string(c_->total_wins), "");
|
|
|
|
|
ty += 8 * gs_;
|
|
|
|
|
if (button(cx, ty, tr("$menugameover_newgame"))) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } ty += lineH();
|
|
|
|
|
if (button(cx, ty, tr("$menugameover_savereplay"), true, c_->replay_available)) r.action = Action::SaveReplay; ty += lineH(); // (006e51e0: with the replay recorder on, the clip editor over the game over)
|
|
|
|
|
if (button(cx, ty, tr("$menu_progress"))) open(Screen::Progress); ty += lineH();
|
|
|
|
|
if (button(cx, ty, tr("$menugameover_quit"))) r.action = Action::Quit;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace menu
|