Add Egg TV menu

This commit is contained in:
James R 2023-04-26 14:32:55 -07:00
parent 7ed356d620
commit 3ccfb71f04
10 changed files with 2434 additions and 1 deletions

View file

@ -411,6 +411,9 @@ extern menu_t EXTRAS_ReplayHutDef;
extern menuitem_t EXTRAS_ReplayStart[];
extern menu_t EXTRAS_ReplayStartDef;
extern menuitem_t EXTRAS_EggTV[];
extern menu_t EXTRAS_EggTVDef;
// PAUSE
extern menuitem_t PAUSE_Main[];
extern menu_t PAUSE_MainDef;
@ -1039,6 +1042,10 @@ boolean M_QuitReplayHut(void);
void M_HutStartReplay(INT32 choice);
void M_PrepReplayList(void);
// Extras: Egg TV
void M_EggTV(INT32 choice);
void M_EggTV_RefreshButtonLabels(void);
// Pause menu:

View file

@ -2,6 +2,7 @@ target_sources(SRB2SDL2 PRIVATE
extras-1.c
extras-addons.c
extras-challenges.c
extras-egg-tv.cpp
extras-replay-hut.c
extras-statistics.c
main-1.c
@ -40,4 +41,5 @@ target_sources(SRB2SDL2 PRIVATE
play-online-server-browser.c
)
add_subdirectory(class-egg-tv)
add_subdirectory(transient)

View file

@ -0,0 +1,4 @@
target_sources(SRB2SDL2 PRIVATE
EggTV.cpp
EggTVData.cpp
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,335 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by James Robert Roman
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __EGGTV_HPP__
#define __EGGTV_HPP__
#include <algorithm>
#include <cstddef>
#include <functional>
#include <vector>
#include "EggTVData.hpp"
#include "EggTVGraphics.hpp"
#include "../../doomdef.h" // TICRATE
#include "../../i_time.h"
#include "../../k_menu.h"
#include "../../m_fixed.h"
#include "../../sounds.h"
#include "../../v_draw.hpp"
namespace srb2::menus::egg_tv
{
class EggTV : private EggTVData, private EggTVGraphics
{
public:
struct InputReaction
{
bool bypass = false; // use default menu controls
bool effect = false; // input was not ignored
sfxenum_t sound = sfx_s3k5b;
};
explicit EggTV() : EggTVData() {}
InputReaction input(int pid);
bool select();
void back();
void watch() const;
void erase();
void toggle_favorite();
void standings();
bool favorited() const;
void draw() const;
private:
// TODO: separate some of these classes into separate files for general use
class Animation
{
public:
class Value
{
public:
Value(fixed_t n) : n_(n) {}
Value invert() const { return FRACUNIT - n_; }
Value invert_if(bool yes) const { return yes ? invert() : *this; }
operator fixed_t() const { return n_; }
private:
fixed_t n_;
};
enum Timing
{
kOnce,
kLooping,
};
explicit Animation(unsigned int duration, Timing kind = kOnce) : kind_(kind), defaultDuration_(duration) {}
void start(int offset = 0) { start_at(now() + offset); }
void stop() { start_at(now() - duration()); }
void start_with(const Animation& b) { start_at(b.starting_point()); }
void start_after(const Animation& b) { start_at(b.stopping_point()); }
void end_with(const Animation& b) { start_at(b.stopping_point() - duration()); }
void end_before(const Animation& b) { start_at(b.starting_point() - duration()); }
void delay() { start(1 - elapsed()); }
void extend(float f) { duration_ += defaultDuration_ * f; }
tic_t starting_point() const { return epoch_; }
tic_t stopping_point() const { return epoch_ + duration(); }
bool running() const
{
switch (kind_)
{
case kOnce:
return elapsed() < duration();
case kLooping:
return !delayed();
}
return false;
}
bool delayed() const { return starting_point() > now(); }
unsigned int elapsed() const { return delayed() ? 0 : now() - starting_point(); }
unsigned int duration() const { return duration_; }
Value variable() const { return variable_or(0); }
Value reverse() const { return FRACUNIT - variable_or(FRACUNIT); }
Value reverse_if(bool yes) const { return yes ? reverse() : variable(); }
Animation operator +(int offset) const
{
Animation anim = *this;
anim.epoch_ += offset;
return anim;
}
Animation operator -(int offset) const { return *this + -(offset); }
private:
Timing kind_;
unsigned int defaultDuration_;
unsigned int duration_ = defaultDuration_;
tic_t epoch_ = 0;
void start_at(tic_t when)
{
epoch_ = when;
duration_ = defaultDuration_;
}
fixed_t variable_or(fixed_t alt) const
{
switch (kind_)
{
case kOnce:
return running() ? count() : alt;
case kLooping:
return count() % FRACUNIT;
}
return alt;
}
fixed_t count() const { return (elapsed() * FRACUNIT) / std::max(1u, duration()); }
static tic_t now() { return gametic; }
};
class Mode
{
public:
enum Value
{
kFolders,
kGrid,
kReplay,
kStandings,
};
explicit Mode(Value mode) : next_(mode) {}
void change(Value mode, tic_t when)
{
prev_ = next_;
next_ = mode;
stop_ = when;
}
Value get() const { return (stop_ <= gametic ? next_ : prev_); }
Value next() const { return next_; }
bool changing() const { return gametic < stop_; }
bool changing_to(Value type) const { return changing() && next_ == type; }
bool operator ==(Value b) const { return get() == b; }
operator Value() const { return get(); }
private:
Value next_;
Value prev_ = next_;
tic_t stop_ = 0;
};
class Cursor
{
public:
using limiter_t = std::function<int()>;
using anims_t = std::vector<Animation*>;
explicit Cursor(anims_t anims, limiter_t limiter) : limiter_(limiter), anims_(anims) {}
int pos() const { return pos_; }
int end() const { return std::max(1, limiter_()); }
float fraction() const { return pos() / static_cast<float>(end()); }
int change() const { return change_; }
bool wrap() const
{
const int prev = pos() - change();
return prev < 0 || prev >= end();
}
Cursor& operator +=(int n)
{
if (n != 0)
{
pos_ += n;
change_ = n;
}
if (pos_ < 0)
{
pos_ = end() - 1;
}
else
{
pos_ %= end();
}
if (n != 0)
{
// TODO?
for (Animation* anim : anims_)
{
if (anim->running())
{
anim->delay();
}
else
{
anim->start();
}
}
}
return *this;
}
Cursor& operator =(int n)
{
pos_ = n;
pos_ += 0;
return *this;
}
private:
limiter_t limiter_;
int pos_ = 0;
int change_ = 0;
anims_t anims_;
};
Mode mode_{Mode::kFolders};
Animation rowSlide_{4};
Animation folderSlide_{8};
Animation dashRise_{6};
Animation dashTextRise_{8};
Animation dashTextScroll_{12*TICRATE, Animation::kLooping};
Animation gridFade_{4};
Animation gridPopulate_{16};
Animation gridSelectX_{3};
Animation gridSelectY_{3};
Animation gridSelectShake_{6};
Animation enhanceMove_{2};
Animation enhanceZoom_{4};
Animation replaySlide_{6};
Animation buttonSlide_{6};
Animation buttonHover_{4};
Animation replayTitleScroll_{24*TICRATE, Animation::kLooping};
Animation favSlap_{8};
Animation standingsRowSlide_{6};
static constexpr int kGridColsPerRow = 5;
Cursor folderRow_{{&rowSlide_}, [this]{ return folders_.size(); }};
Cursor gridCol_{{&gridSelectX_}, []{ return kGridColsPerRow; }};
Cursor gridRow_{{&gridSelectY_, &rowSlide_}, [this] { return grid_rows(); }};
Cursor standingsRow_{{&standingsRowSlide_}, [this]{ return standings_rows(); }};
std::size_t grid_index() const { return gridCol_.pos() + (gridRow_.pos() * kGridColsPerRow); }
std::size_t grid_rows() const;
std::size_t standings_rows() const;
PatchManager::patch gametype_graphic(const Replay& replay) const;
void draw_background() const;
void draw_overlay() const;
void draw_folder_header() const;
void draw_folders() const;
void draw_scroll_bar() const;
void draw_dash() const;
void draw_dash_text(const Replay& replay) const;
enum class GridMode
{
kPopulating,
kFinal,
};
struct GridOffsets;
template <GridMode Mode>
void draw_grid() const;
void draw_grid_mesh(const GridOffsets& grid) const;
void draw_grid_select(const GridOffsets& grid) const;
void draw_grid_enhance(const GridOffsets& grid) const;
void draw_replay(const Replay& replay) const;
void draw_replay_photo(const Replay& replay, Draw pic) const;
void draw_replay_buttons() const;
void draw_standings(const Replay& replay) const;
};
}; // namespace srb2::menus::egg_tv
#endif // __EGGTV_HPP__

View file

@ -0,0 +1,369 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by James Robert Roman
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include <algorithm>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <string_view>
#include <fmt/format.h>
#include <fmt/std.h> // std::filesystem::path formatter
#include <nlohmann/json.hpp>
#include "../../cxxutil.hpp"
#include "EggTVData.hpp"
#include "../../doomdef.h" // CONS_Alert
#include "../../doomstat.h" // gametypes
#include "../../g_demo.h"
#include "../../k_menu.h" // DF_ENCORE
#include "../../r_skins.h"
using namespace srb2::menus::egg_tv;
namespace fs = std::filesystem;
using nlohmann::json;
template <>
struct fmt::formatter<fs::filesystem_error> : formatter<std::string>
{
template <typename FormatContext>
auto format(const fs::filesystem_error& ex, FormatContext& ctx) const
{
return formatter<std::string>::format(
fmt::format("{}, path1={}, path2={}", ex.what(), ex.path1(), ex.path2()),
ctx
);
}
};
namespace
{
template <class... Args>
void print_error(fmt::format_string<Args...> format, Args&&... args)
{
CONS_Alert(CONS_ERROR, "Egg TV: %s\n", fmt::format(format, args...).c_str());
}
template <class To, class From>
To time_point_conv(From time)
{
// https://stackoverflow.com/a/58237530/10850779
return std::chrono::time_point_cast<typename To::duration>(To::clock::now() + (time - From::clock::now()));
}
json& ensure_array(json& object, const char* key)
{
json& array = object[key];
if (!array.is_array())
{
array = json::array();
}
return array;
}
}; // namespace
EggTVData::EggTVData() : favorites_(ensure_array(favoritesFile_, "favorites"))
{
try
{
cache_folders();
}
catch (const fs::filesystem_error& ex)
{
print_error("{}", ex);
}
}
json EggTVData::cache_favorites() const
{
json object;
try
{
std::ifstream(favoritesPath_) >> object;
}
catch (const std::exception& ex)
{
print_error("{}", ex.what());
}
return object;
}
EggTVData::Folder::Cache::Cache(Folder& folder) : folder_(folder)
{
try
{
for (const fs::directory_entry& entry : fs::directory_iterator(folder_.path()))
{
if (!entry.is_regular_file())
{
continue;
}
try
{
replays_.emplace_back(
*this,
entry.path().filename(),
time_point_conv<time_point_t>(entry.last_write_time())
);
}
catch (const fs::filesystem_error& ex)
{
print_error("{}", ex);
}
}
}
catch (const fs::filesystem_error& ex)
{
print_error("{}", ex);
}
auto predicate = [](const ReplayRef& a, const ReplayRef& b)
{
// Favorites come first
if (a.favorited() != b.favorited())
{
return a.favorited();
}
return a.time() > b.time(); // sort newest to oldest
};
std::sort(replays_.begin(), replays_.end(), predicate);
// Refresh folder size
folder_.size_ = replays_.size();
}
std::shared_ptr<EggTVData::Replay> EggTVData::Folder::Cache::replay(std::size_t idx)
{
if (idx >= size())
{
return {};
}
return replays_[idx].replay();
}
void EggTVData::Folder::Cache::release(const ReplayRef& ref)
{
const auto& it = std::find_if(replays_.begin(), replays_.end(), [&ref](const ReplayRef& b) { return &b == &ref; });
SRB2_ASSERT(it != replays_.end());
replays_.erase(it);
folder_.size_--;
}
EggTVData::Folder::Folder(EggTVData& tv, const fs::directory_entry& entry) :
size_(std::distance(fs::directory_iterator(entry.path()), fs::directory_iterator())),
time_(time_point_conv<decltype(time_)>(entry.last_write_time())),
tv_(&tv),
name_(entry.path().filename().string())
{
SRB2_ASSERT(entry.path().parent_path() == tv_->root_);
}
EggTVData::Replay::Title::operator const std::string() const
{
return second().empty() ? first() : fmt::format("{} - {}", first(), second());
}
EggTVData::Replay::Replay(Folder::Cache::ReplayRef& ref) : ref_(&ref)
{
const fs::path path = this->path();
menudemo_t info = {};
if (path.native().size() >= sizeof info.filepath)
{
return;
}
std::copy_n(path.string().c_str(), path.native().size() + 1, info.filepath);
G_LoadDemoInfo(&info);
if (info.type != MD_LOADED)
{
return;
}
{
constexpr std::string_view kDelimiter = " - ";
const std::string_view str = info.title;
const std::size_t mid = str.find(kDelimiter);
title_ = Title(str.substr(0, mid), mid == std::string::npos ? "" : str.substr(mid + kDelimiter.size()));
//title_ = Title("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", "WWWWWWWWWWWWWWWWWWWWWWWWWWW");
}
map_ = info.map;
if (info.gametype == GT_RACE)
{
gametype_ = Gametype(GT_RACE, Gametype::Race {
info.numlaps,
kartspeed_cons_t[(info.kartspeed & ~(DF_ENCORE)) + 1].strvalue,
(info.kartspeed & DF_ENCORE) != 0,
});
}
else
{
gametype_ = Gametype(info.gametype);
}
for (const auto& data : info.standings)
{
if (!data.ranking)
{
break;
}
Standing& standing = standings_.emplace_back();
standing.name = data.name;
if (data.skin < numskins)
{
standing.skin = data.skin;
}
standing.color = data.color;
if (data.timeorscore != UINT32_MAX - 1) // NO CONTEST
{
if (gametype_.ranks_time())
{
standing.time = data.timeorscore;
}
else if (gametype_.ranks_points())
{
standing.score = data.timeorscore;
}
}
}
invalid_ = false;
}
EggTVData::Replay::~Replay()
{
if (!erased_)
{
return;
}
// Delayed erase function ensures there are no references
// left before ReplayRef is removed from cache.
try
{
fs::remove(path());
}
catch (const fs::filesystem_error& ex)
{
// catch inside loop so individual errors don't bail completely
print_error("{}", ex);
}
// Clear deleted replays from favorites too!
if (favorited())
{
toggle_favorite();
}
ref_->cache().release(*ref_);
}
void EggTVData::Replay::toggle_favorite() const
{
const auto& it = ref_->iterator_to_favorite();
if (it != ref_->favorites().end())
{
ref_->favorites().erase(it);
}
else
{
ref_->favorites().emplace_back(ref_->favorites_path());
}
ref_->cache().folder().tv().save_favorites();
}
void EggTVData::cache_folders()
{
for (const fs::directory_entry& entry : fs::directory_iterator(root_))
{
try
{
Folder folder(*this, entry);
if (!folder.empty())
{
folders_.push_back(folder);
}
}
catch (const fs::filesystem_error& ex)
{
// catch inside loop so individual errors don't bail completely
print_error("{}", ex);
}
}
sort_folders();
}
void EggTVData::sort_folders()
{
auto predicate = [this](const Folder& a, const Folder& b)
{
switch (folderSort_)
{
case FolderSort::kDate:
return a.time() > b.time();
case FolderSort::kName:
return a.name() < b.name(); // ascending order
case FolderSort::kSize:
return a.size() > b.size();
}
return false;
};
std::sort(folders_.begin(), folders_.end(), predicate);
}
void EggTVData::save_favorites() const
{
try
{
std::ofstream(favoritesPath_) << favoritesFile_;
}
catch (const std::exception& ex)
{
print_error("{}", ex.what());
}
}

View file

@ -0,0 +1,268 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by James Robert Roman
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __EGGTVDATA_HPP__
#define __EGGTVDATA_HPP__
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include <nlohmann/json.hpp>
#include "../../cxxutil.hpp"
#include "../../d_main.h" // srb2home
#include "../../doomstat.h" // gametype_t
#include "../../doomtype.h"
namespace srb2::menus::egg_tv
{
class EggTVData
{
private:
const std::filesystem::path root_ = std::filesystem::path{srb2home} / "media/replay/online";
const std::filesystem::path favoritesPath_ = root_ / "favorites.json";
nlohmann::json favoritesFile_ = cache_favorites();
nlohmann::json& favorites_;
nlohmann::json cache_favorites() const;
void cache_folders();
void save_favorites() const;
public:
using time_point_t = std::chrono::time_point<std::chrono::system_clock>;
explicit EggTVData();
class Replay;
class Folder
{
public:
class Cache
{
public:
class ReplayRef
{
public:
explicit ReplayRef(Cache& cache, std::filesystem::path filename, time_point_t time) :
cache_(&cache), filename_(filename), time_(time)
{
}
Cache& cache() const { return *cache_; }
const std::filesystem::path& filename() const { return filename_; }
const time_point_t& time() const { return time_; }
std::shared_ptr<Replay> replay()
{
SRB2_ASSERT(!released_); // do not call after released
if (!replay_)
{
replay_ = std::make_shared<Replay>(*this);
}
return replay_;
}
void release()
{
replay_.reset();
released_ = true;
}
bool favorited() const { return iterator_to_favorite() != favorites().end(); }
nlohmann::json& favorites() const { return cache().folder().tv().favorites_; }
std::string favorites_path() const
{
// path::generic_string converts to forward
// slashes on Windows. This should suffice to make
// the JSON file portable across installations.
return (std::filesystem::path{cache().folder().name()} / filename()).generic_string();
}
nlohmann::json::const_iterator iterator_to_favorite() const
{
return std::find(favorites().begin(), favorites().end(), favorites_path());
}
private:
Cache* cache_;
std::filesystem::path filename_;
time_point_t time_;
std::shared_ptr<Replay> replay_;
bool released_ = false;
};
explicit Cache(Folder& folder);
std::shared_ptr<Replay> replay(std::size_t idx);
void release(const ReplayRef& ref);
std::size_t size() const { return folder_.size(); }
Folder& folder() const { return folder_; }
private:
Folder& folder_;
std::vector<ReplayRef> replays_;
};
explicit Folder(EggTVData& tv, const std::filesystem::directory_entry& entry);
int x = 0;
int y = 0;
bool empty() { return size() == 0; }
std::filesystem::path path() const { return tv_->root_ / name_; }
EggTVData& tv() const { return *tv_; }
std::size_t size() const { return size_; }
const time_point_t& time() const { return time_; }
const std::string& name() const { return name_; }
std::unique_ptr<Cache> load() { return std::make_unique<Cache>(*this); };
bool operator ==(const Folder& b) const { return this == &b; }
private:
std::size_t size_;
time_point_t time_;
EggTVData* tv_;
std::string name_;
};
class Replay
{
public:
class Title
{
public:
explicit Title() {}
explicit Title(const std::string_view& first, const std::string_view& second) :
first_(first), second_(second)
{
}
const std::string& first() const { return first_; }
const std::string& second() const { return second_; }
operator const std::string() const;
private:
std::string first_, second_;
};
struct Standing
{
std::string name;
std::optional<std::size_t> skin;
std::size_t color;
std::optional<tic_t> time;
std::optional<UINT32> score;
};
class Gametype
{
public:
struct Race
{
int laps;
std::string_view speed;
bool encore;
};
explicit Gametype() {}
explicit Gametype(INT16 gt, Race race) : gametype_(get(gt)), var_(race) {}
explicit Gametype(INT16 gt) : gametype_(get(gt)) {}
bool valid() const { return gametype_; }
std::string_view name() const { return valid() ? gametype_->name : "<Unknown gametype>"; }
UINT32 rules() const { return valid() ? gametype_->rules : 0u; }
bool ranks_time() const { return !ranks_points(); }
bool ranks_points() const { return rules() & GTR_POINTLIMIT; }
const Race* race() const { return std::get_if<Race>(&var_); }
private:
const gametype_t* gametype_ = nullptr;
std::variant<std::monostate, Race> var_;
static gametype_t* get(INT16 gt) { return gt >= 0 && gt < numgametypes ? gametypes[gt] : nullptr; }
};
explicit Replay(Folder::Cache::ReplayRef& ref);
~Replay();
void mark_for_deletion()
{
erased_ = true;
ref_->release();
}
void toggle_favorite() const;
bool invalid() const { return invalid_; }
bool favorited() const { return ref_->iterator_to_favorite() != ref_->favorites().end(); }
std::filesystem::path path() const { return ref_->cache().folder().path() / ref_->filename(); }
const time_point_t& date() const { return ref_->time(); }
std::size_t map() const { return map_; }
const Title& title() const { return title_; }
const Gametype& gametype() const { return gametype_; }
const std::vector<Standing>& standings() const { return standings_; }
const Standing* winner() const { return standings_.empty() ? nullptr : &standings_.front(); }
private:
Folder::Cache::ReplayRef* ref_;
bool invalid_ = true;
bool erased_ = false;
std::vector<Standing> standings_;
std::size_t map_;
Title title_;
Gametype gametype_;
};
enum class FolderSort
{
kDate,
kName,
kSize,
};
std::vector<Folder> folders_;
std::unique_ptr<Folder::Cache> cache_;
FolderSort folderSort_ = FolderSort::kDate;
void sort_folders();
};
}; // namsepace srb2::menus::egg_tv
#endif // __EGGTVDATA_HPP__

View file

@ -0,0 +1,161 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by James Robert Roman
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __EGGTVGRAPHICS_HPP__
#define __EGGTVGRAPHICS_HPP__
#include <array>
#include <string_view>
#include <unordered_map>
#include "../../doomdef.h" // skincolornum_t
#include "../../v_draw.hpp"
namespace srb2::menus::egg_tv
{
class EggTVGraphics
{
public:
struct PatchManager
{
using patch = const char*;
template <int Size>
using array = std::array<patch, Size>;
template <int Size>
struct Animation
{
array<Size> data;
patch animate(int interval) const;
};
Animation<2> bg = {
"RHETSCB1",
"RHETSCB2",
};
patch mod = "RHTVMOD";
patch overlay = "RHETTV";
patch bar = "RH_BAR";
array<2> folder = {
"RHFLDR02",
"RHFLDR03",
};
patch dash = "RHMQBR";
patch grid = "RHGRID";
patch empty = "RHTVSQN0";
Animation<4> tv = {
"RHTVSTC1",
"RHTVSTC2",
"RHTVSTC3",
"RHTVSTC4",
};
std::array<Animation<2>, 2> nodata = {
Animation<2> {
"RHTVSQN1",
"RHTVSQN2",
},
Animation<2> {
"RHTVSQN3",
"RHTVSQN4",
},
};
std::array<Animation<2>, 2> corrupt = {
Animation<2> {
"RHTVSQN5",
"RHTVSQN6",
},
Animation<2> {
"RHTVSQN7",
"RHTVSQN8",
},
};
patch select = "RHTVSQSL";
std::unordered_map<std::string_view, patch> gametype = {
{"Race", "RHGT1"},
{"Battle", "RHGT2"},
// TODO: requires support in the demo format
//{"Prisons", "RHGT3"},
//{"Special", "RHGT4"},
};
patch fav = "RHFAV";
patch button = "RHMNBR";
struct
{
struct
{
patch up = "RHSBRU";
patch down = "RHSBRD";
}
arrow;
struct
{
patch top = "RHSBR01";
patch mid = "RHSBR02";
patch bottom = "RHSBR03";
}
bead;
}
scroll;
};
struct ColorManager
{
using color = skincolornum_t;
template <int Size>
using array = std::array<color, Size>;
array<2> bar = {
SKINCOLOR_JET,
SKINCOLOR_BANANA,
};
array<2> folder = {
SKINCOLOR_NONE,
SKINCOLOR_THUNDER,
};
array<2> button = {
SKINCOLOR_BLACK,
SKINCOLOR_THUNDER,
};
color scroll = SKINCOLOR_THUNDER;
array<2> select = {
SKINCOLOR_NONE,
SKINCOLOR_MAROON,
};
};
static const PatchManager patches_;
static const ColorManager colors_;
};
}; // namespace srb2::menus::egg_tv
#endif // __EGGTVGRAPHICS_HPP__

View file

@ -24,7 +24,7 @@ menuitem_t EXTRAS_Main[] =
NULL, {.routine = M_Statistics}, 0, 0},
{IT_STRING | IT_CALL, NULL, NULL,
NULL, {.routine = M_ReplayHut}, 0, 0},
NULL, {.routine = M_EggTV}, 0, 0},
{IT_STRING | IT_CALL, NULL, NULL,
NULL, {.routine = M_SoundTest}, 0, 0},

131
src/menus/extras-egg-tv.cpp Normal file
View file

@ -0,0 +1,131 @@
/// \brief Extras Menu: Egg TV
#include "class-egg-tv/EggTV.hpp"
#include "../k_menu.h"
#include "../s_sound.h"
using namespace srb2::menus::egg_tv;
namespace
{
std::unique_ptr<EggTV> g_egg_tv;
void M_DrawEggTV()
{
g_egg_tv->draw();
}
boolean M_QuitEggTV()
{
g_egg_tv = {};
return true;
}
boolean M_HandleEggTV(INT32 choice)
{
(void)choice;
const UINT8 pid = 0;
const EggTV::InputReaction reaction = g_egg_tv->input(pid);
if (reaction.bypass)
{
return false;
}
if (reaction.effect)
{
S_StartSound(nullptr, reaction.sound);
M_SetMenuDelay(pid);
}
return true;
}
void M_DeleteReplayChoice(INT32 choice)
{
if (choice == MA_YES)
{
g_egg_tv->erase();
//S_StartSound(nullptr, sfx_s3k4e); // BOOM
S_StartSound(nullptr, sfx_monch); // :)
}
}
void M_DeleteReplay(INT32 c)
{
(void)c;
M_StartMessage(
"Are you sure you want to\n"
"delete this replay?\n"
"\n"
"\x85" "This cannot be undone.\n" "\x80"
"\n"
"Press (A) to confirm or (B) to cancel",
FUNCPTRCAST(M_DeleteReplayChoice),
MM_YESNO
);
S_StartSound(nullptr, sfx_s3k36); // lel skid
}
void M_FavoriteReplay(INT32 c)
{
(void)c;
g_egg_tv->toggle_favorite();
S_StartSound(nullptr, sfx_s1c9);
}
}; // namespace
// extras menu: replay hut
menuitem_t EXTRAS_EggTV[] =
{
{IT_STRING | IT_CALL, "WATCH REPLAY", NULL, NULL, {.routine = [](auto) { g_egg_tv->watch(); }}, 0, 0},
{IT_STRING | IT_CALL, "STANDINGS", NULL, NULL, {.routine = [](auto) { g_egg_tv->standings(); }}, 0, 0},
{IT_STRING | IT_CALL, "FAVORITE", NULL, NULL, {.routine = M_FavoriteReplay}, 0, 0},
{IT_SPACE},
{IT_STRING | IT_CALL, "DELETE REPLAY", NULL, NULL, {.routine = M_DeleteReplay}, 0, 0},
{IT_SPACE},
{IT_STRING | IT_CALL, "GO BACK", NULL, NULL, {.routine = [](auto) { g_egg_tv->back(); }}, 0, 0},
};
menu_t EXTRAS_EggTVDef =
{
sizeof (EXTRAS_EggTV)/sizeof (menuitem_t),
&EXTRAS_MainDef,
0,
EXTRAS_EggTV,
30, 80,
0, 0,
0,
"REPLAY", // music
41, 1,
M_DrawEggTV,
NULL,
NULL,
M_QuitEggTV,
M_HandleEggTV
};
// Call this to construct Egg TV menu
void M_EggTV(INT32 choice)
{
g_egg_tv = std::make_unique<EggTV>();
M_SetupNextMenu(&EXTRAS_EggTVDef, false);
}
void M_EggTV_RefreshButtonLabels()
{
EXTRAS_EggTV[2].text = g_egg_tv->favorited() ? "UNFAVORITE" : "FAVORITE";
}