Merge branch 'splitscreen-spectator' into 'master'

Complete splitscreen spectator support (parties not tested) -- Director working, freecam working, HUD working and polished

See merge request KartKrew/Kart!1541
This commit is contained in:
Oni 2023-10-15 01:40:33 +00:00
commit f495b9ff7d
24 changed files with 823 additions and 566 deletions

View file

@ -137,7 +137,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
k_terrain.c
k_brightmap.c
k_terrain.c
k_director.c
k_director.cpp
k_follower.c
k_profiles.c
k_specialstage.c

View file

@ -1645,7 +1645,7 @@ static void Command_View_f(void)
return;
}
if (demo.freecam)
if (camera[viewnum-1].freecam)
return;
displayplayerp = &displayplayers[viewnum-1];

View file

@ -86,6 +86,8 @@ class TiccmdBuilder
UINT8 forplayer() const { return ssplayer - 1; }
player_t* player() const { return &players[g_localplayers[forplayer()]]; }
bool freecam() const { return camera[forplayer()].freecam; }
UINT8 swap_ssplayer() const
{
if (ssplayer == cv_1pswap.value)
@ -239,13 +241,13 @@ class TiccmdBuilder
{
if (M_MenuButtonPressed(pid, MBT_C))
{
P_ToggleDemoCamera();
P_ToggleDemoCamera(forplayer());
}
}
bool director_input()
{
if (demo.freecam || !K_DirectorIsAvailable(viewnum))
if (freecam() || !K_DirectorIsAvailable(viewnum))
{
return false;
}
@ -253,13 +255,13 @@ class TiccmdBuilder
if (M_MenuButtonPressed(pid, MBT_A))
{
G_AdjustView(ssplayer, 1, true);
K_ToggleDirector(false);
K_ToggleDirector(forplayer(), false);
}
if (M_MenuButtonPressed(pid, MBT_X))
{
G_AdjustView(ssplayer, -1, true);
K_ToggleDirector(false);
K_ToggleDirector(forplayer(), false);
}
if (player()->spectator == true)
@ -272,7 +274,7 @@ class TiccmdBuilder
if (M_MenuButtonPressed(pid, MBT_R))
{
K_ToggleDirector(true);
K_ToggleDirector(forplayer(), true);
}
}
@ -283,7 +285,7 @@ class TiccmdBuilder
bool spectator_analog_input()
{
if (!player()->spectator && !objectplacing && !demo.freecam)
if (!player()->spectator && !objectplacing && !freecam())
{
return false;
}
@ -387,7 +389,7 @@ class TiccmdBuilder
map(gc_item, BT_ATTACK); // fire
map(gc_lookback, BT_LOOKBACK); // rear view
map(gc_respawn, BT_RESPAWN | BT_EBRAKEMASK); // respawn
map(gc_respawn, BT_RESPAWN | (freecam() ? 0 : BT_EBRAKEMASK)); // respawn
map(gc_vote, BT_VOTE); // mp general function button
// lua buttons a thru c
@ -406,7 +408,7 @@ public:
common_button_input();
};
if (demo.playback || demo.freecam || player()->spectator)
if (demo.playback || freecam() || player()->spectator)
{
// freecam is controllable even while paused
@ -416,7 +418,7 @@ public:
{
regular_input();
if (demo.freecam)
if (freecam())
{
toggle_freecam_input();
}

View file

@ -1653,9 +1653,6 @@ void G_ConfirmRewind(tic_t rewindtime)
COM_BufInsertText("renderview on\n");
if (demo.freecam)
return; // don't touch from there
splitscreen = oldss;
displayplayers[0] = olddp1;
displayplayers[1] = olddp2;
@ -4126,7 +4123,13 @@ void G_StopDemo(void)
demo.timing = false;
singletics = false;
demo.freecam = false;
{
UINT8 i;
for (i = 0; i < MAXSPLITSCREENPLAYERS; ++i)
{
camera[i].freecam = false;
}
}
Z_Free(demo.skinlist);
demo.skinlist = NULL;

View file

@ -1364,7 +1364,7 @@ boolean G_Responder(event_t *ev)
}
}
if (gamestate == GS_LEVEL && ev->type == ev_keydown && multiplayer && demo.playback && !demo.freecam)
if (gamestate == GS_LEVEL && ev->type == ev_keydown && multiplayer && demo.playback)
{
// Allow pausing
if (

View file

@ -1,4 +1,5 @@
target_sources(SRB2SDL2 PRIVATE
powerup.cpp
spectator.cpp
timer.cpp
)

247
src/hud/spectator.cpp Normal file
View file

@ -0,0 +1,247 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by Kart Krew
//
// 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 <initializer_list>
#include <optional>
#include <utility>
#include <fmt/format.h>
#include "../v_draw.hpp"
#include "../command.h"
#include "../d_clisrv.h"
#include "../d_player.h"
#include "../doomdef.h"
#include "../doomstat.h"
#include "../g_game.h"
#include "../g_party.h"
#include "../i_time.h"
#include "../k_director.h"
#include "../k_hud.h"
#include "../p_local.h"
#include "../r_fps.h"
extern "C" consvar_t cv_maxplayers;
using srb2::Draw;
namespace
{
struct List
{
struct Field
{
Field(const char* label, Draw::Button button, std::optional<bool> pressed = {}) :
label_(Draw::TextElement(label).font(Draw::Font::kThin)),
button_(button),
pressed_(pressed)
{
}
int width() const { return label_.width() + kButtonWidth + kButtonMargin + kFieldSpacing; }
void draw(const Draw& row, bool left) const
{
Draw col = row.x(left ? kButtonWidth + kButtonMargin : kFieldSpacing);
col.text(label_);
col = col.x(left ? -(kButtonWidth + kButtonMargin) : width() - (kButtonWidth + kFieldSpacing));
if (r_splitscreen)
{
auto small_button_offset = [&]
{
switch (button_)
{
case Draw::Button::l:
case Draw::Button::r:
return -4;
default:
return -2;
}
};
col.y(small_button_offset()).small_button(button_, pressed_);
}
else
{
col.y(-4).button(button_, pressed_);
}
}
private:
static constexpr int kButtonWidth = 14;
static constexpr int kButtonMargin = 2;
static constexpr int kFieldSpacing = 8;
Draw::TextElement label_;
Draw::Button button_;
std::optional<bool> pressed_;
};
List(int x, int y) : row_(split_draw(x, y, left_)) {}
void insert(std::initializer_list<Field> fields)
{
auto total_width = [&fields]
{
int width = 0;
for (const Field& field : fields)
{
width += field.width();
}
return width;
};
Draw col = left_ ? row_ : row_.x(-total_width());
for (const Field& field : fields)
{
field.draw(col, left_);
col = col.x(field.width());
}
row_ = row_.y(r_splitscreen ? -13 : -17);
}
private:
bool left_ = r_splitscreen > 1 && R_GetViewNumber() & 1;
Draw row_;
static Draw split_draw(int x, int y, bool left)
{
return Draw(
left ? x : (BASEVIDWIDTH / (r_splitscreen > 1 ? 2 : 1)) - x,
(BASEVIDHEIGHT / (r_splitscreen ? 2 : 1)) - y
)
.align(Draw::Align::kLeft)
.flags(
V_SNAPTOBOTTOM |
(left ? V_SNAPTOLEFT : V_SNAPTORIGHT) |
(r_splitscreen > 1 ? V_HUDTRANS : V_SLIDEIN) |
V_SPLITSCREEN
);
}
};
}; // namespace
void K_drawSpectatorHUD(boolean director)
{
const UINT8 viewnum = R_GetViewNumber();
UINT8 numingame = 0;
for (UINT8 i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && !players[i].spectator)
{
numingame++;
}
}
player_t* player = [viewnum]() -> player_t*
{
if (viewnum >= G_PartySize(consoleplayer))
{
return nullptr;
}
UINT8 p = G_PartyMember(consoleplayer, viewnum);
if (!playeringame[p] || players[p].spectator == false)
{
return nullptr;
}
return &players[p];
}();
List list = [director]
{
switch (r_splitscreen)
{
case 0:
return List(director ? 80 : 20, 20);
case 1:
return List(40, 20);
default:
return List(10, 20);
}
}();
if (player)
{
std::string label = [player]
{
if (player->flashing)
{
return ". . .";
}
else if (player->pflags & PF_WANTSTOJOIN)
{
return "Cancel Join";
}
else
{
return "Join";
}
}();
if (cv_maxplayers.value)
{
label += fmt::format(" [{}/{}]", numingame, cv_maxplayers.value);
}
list.insert({{label.c_str(), Draw::Button::l}});
}
if (director || camera[viewnum].freecam)
{
// Not locked into freecam -- can toggle it.
if (director)
{
list.insert({{"Freecam", Draw::Button::c}});
}
else
{
bool press = D_LocalTiccmd(viewnum)->buttons & BT_RESPAWN;
const char* label = (press && I_GetTime() % 16 < 8) ? "> <" : ">< ";
list.insert({{label, Draw::Button::y, press}, {"Exit", Draw::Button::c}});
}
}
if (director)
{
if (numingame > 1)
{
list.insert({{"+", Draw::Button::a}, {"-", Draw::Button::x}});
}
if (player)
{
list.insert({{K_DirectorIsEnabled(viewnum) ? "\x82" "Director" : "Director", Draw::Button::r}});
}
}
else
{
auto bt = D_LocalTiccmd(viewnum)->buttons;
list.insert({{"", Draw::Button::r, bt & BT_DRIFT}, {"Pivot", Draw::Button::b, bt & BT_LOOKBACK}});
list.insert({{"+", Draw::Button::a, bt & BT_ACCELERATE}, {"-", Draw::Button::x, bt & BT_BRAKE}});
}
}

View file

@ -1,331 +0,0 @@
// SONIC ROBO BLAST 2 KART
//-----------------------------------------------------------------------------
/// \file k_director.c
/// \brief SRB2kart automatic spectator camera.
#include "k_kart.h"
#include "k_respawn.h"
#include "doomdef.h"
#include "g_game.h"
#include "v_video.h"
#include "k_director.h"
#include "d_netcmd.h"
#include "p_local.h"
#include "g_party.h"
#define SWITCHTIME TICRATE * 5 // cooldown between unforced switches
#define BOREDOMTIME 3 * TICRATE / 2 // how long until players considered far apart?
#define TRANSFERTIME TICRATE // how long to delay reaction shots?
#define BREAKAWAYDIST 4000 // how *far* until players considered far apart?
#define WALKBACKDIST 600 // how close should a trailing player be before we switch?
#define PINCHDIST 30000 // how close should the leader be to be considered "end of race"?
struct directorinfo directorinfo;
void K_InitDirector(void)
{
INT32 playernum;
directorinfo.active = false;
directorinfo.cooldown = SWITCHTIME;
directorinfo.freeze = 0;
directorinfo.attacker = 0;
directorinfo.maxdist = 0;
for (playernum = 0; playernum < MAXPLAYERS; playernum++)
{
directorinfo.sortedplayers[playernum] = -1;
directorinfo.gap[playernum] = INT32_MAX;
directorinfo.boredom[playernum] = 0;
}
}
static fixed_t K_GetFinishGap(INT32 leader, INT32 follower)
{
fixed_t dista = players[follower].distancetofinish;
fixed_t distb = players[leader].distancetofinish;
if (players[follower].position < players[leader].position)
{
return distb - dista;
}
else
{
return dista - distb;
}
}
static void K_UpdateDirectorPositions(void)
{
INT32 playernum;
INT32 position;
player_t* target;
memset(directorinfo.sortedplayers, -1, sizeof(directorinfo.sortedplayers));
for (playernum = 0; playernum < MAXPLAYERS; playernum++)
{
target = &players[playernum];
if (playeringame[playernum] && !target->spectator && target->position > 0)
{
directorinfo.sortedplayers[target->position - 1] = playernum;
}
}
for (position = 0; position < MAXPLAYERS - 1; position++)
{
directorinfo.gap[position] = INT32_MAX;
if (directorinfo.sortedplayers[position] == -1 || directorinfo.sortedplayers[position + 1] == -1)
{
continue;
}
directorinfo.gap[position] = P_ScaleFromMap(K_GetFinishGap(directorinfo.sortedplayers[position], directorinfo.sortedplayers[position + 1]), FRACUNIT);
if (directorinfo.gap[position] >= BREAKAWAYDIST)
{
directorinfo.boredom[position] = min(BOREDOMTIME * 2, directorinfo.boredom[position] + 1);
}
else if (directorinfo.boredom[position] > 0)
{
directorinfo.boredom[position]--;
}
}
directorinfo.maxdist = P_ScaleFromMap(players[directorinfo.sortedplayers[0]].distancetofinish, FRACUNIT);
}
static boolean K_CanSwitchDirector(void)
{
INT32 *displayplayerp = &displayplayers[0];
if (players[*displayplayerp].trickpanel > 0)
{
return false;
}
if (directorinfo.cooldown > 0)
{
return false;
}
return true;
}
static void K_DirectorSwitch(INT32 player, boolean force)
{
if (!directorinfo.active)
{
return;
}
if (P_IsDisplayPlayer(&players[player]))
{
return;
}
if (players[player].exiting)
{
return;
}
if (!force && !K_CanSwitchDirector())
{
return;
}
G_ResetView(1, player, true);
directorinfo.cooldown = SWITCHTIME;
}
static void K_DirectorForceSwitch(INT32 player, INT32 time)
{
if (players[player].exiting)
{
return;
}
directorinfo.attacker = player;
directorinfo.freeze = time;
}
void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source)
{
if (!P_IsDisplayPlayer(player))
{
return;
}
if (inflictor && inflictor->player)
{
K_DirectorForceSwitch(inflictor->player - players, TRANSFERTIME);
}
else if (source && source->player)
{
K_DirectorForceSwitch(source->player - players, TRANSFERTIME);
}
}
void K_DrawDirectorDebugger(void)
{
INT32 position;
INT32 leader;
INT32 follower;
INT32 ytxt;
if (!cv_kartdebugdirector.value)
{
return;
}
V_DrawThinString(10, 0, V_70TRANS, va("PLACE"));
V_DrawThinString(40, 0, V_70TRANS, va("CONF?"));
V_DrawThinString(80, 0, V_70TRANS, va("GAP"));
V_DrawThinString(120, 0, V_70TRANS, va("BORED"));
V_DrawThinString(150, 0, V_70TRANS, va("COOLDOWN: %d", directorinfo.cooldown));
V_DrawThinString(230, 0, V_70TRANS, va("MAXDIST: %d", directorinfo.maxdist));
for (position = 0; position < MAXPLAYERS - 1; position++)
{
ytxt = 10 * (position + 1);
leader = directorinfo.sortedplayers[position];
follower = directorinfo.sortedplayers[position + 1];
if (leader == -1 || follower == -1)
break;
V_DrawThinString(10, ytxt, V_70TRANS, va("%d", position));
V_DrawThinString(20, ytxt, V_70TRANS, va("%d", position + 1));
if (players[leader].positiondelay)
{
V_DrawThinString(40, ytxt, V_70TRANS, va("NG"));
}
V_DrawThinString(80, ytxt, V_70TRANS, va("%d", directorinfo.gap[position]));
if (directorinfo.boredom[position] >= BOREDOMTIME)
{
V_DrawThinString(120, ytxt, V_70TRANS, va("BORED"));
}
else
{
V_DrawThinString(120, ytxt, V_70TRANS, va("%d", directorinfo.boredom[position]));
}
V_DrawThinString(150, ytxt, V_70TRANS, va("%s", player_names[leader]));
V_DrawThinString(230, ytxt, V_70TRANS, va("%s", player_names[follower]));
}
}
void K_UpdateDirector(void)
{
INT32 *displayplayerp = &displayplayers[0];
INT32 targetposition;
K_UpdateDirectorPositions();
if (directorinfo.cooldown > 0) {
directorinfo.cooldown--;
}
// handle pending forced switches
if (directorinfo.freeze > 0)
{
if (!(--directorinfo.freeze))
K_DirectorSwitch(directorinfo.attacker, true);
return;
}
// if there's only one player left in the list, just switch to that player
if (directorinfo.sortedplayers[0] != -1 && directorinfo.sortedplayers[1] == -1)
{
K_DirectorSwitch(directorinfo.sortedplayers[0], false);
return;
}
// aaight, time to walk through the standings to find the first interesting pair
// NB: targetposition/sortedplayers is 0-indexed, aiming at the "back half" of a given pair by default.
// we adjust for this when comparing to player->position or when looking at the leading player, Don't Freak Out
for (targetposition = 1; targetposition < MAXPLAYERS; targetposition++)
{
INT32 target;
// you are out of players, try again
if (directorinfo.sortedplayers[targetposition] == -1)
{
break;
}
// pair too far apart? try the next one
if (directorinfo.boredom[targetposition - 1] >= BOREDOMTIME)
{
continue;
}
// pair finished? try the next one
if (players[directorinfo.sortedplayers[targetposition]].exiting)
{
continue;
}
// don't risk switching away from forward pairs at race end, might miss something!
if (directorinfo.maxdist > PINCHDIST)
{
// if the "next" player is close enough, they should be able to see everyone fine!
// walk back through the standings to find a vantage that gets everyone in frame.
// (also creates a pretty cool effect w/ overtakes at speed)
while (targetposition < MAXPLAYERS && directorinfo.gap[targetposition] < WALKBACKDIST)
{
targetposition++;
}
}
target = directorinfo.sortedplayers[targetposition];
// stop here since we're already viewing this player
if (*displayplayerp == target)
{
break;
}
// if this is a splitscreen player, try next pair
if (P_IsDisplayPlayer(&players[target]))
{
continue;
}
// if we're certain the back half of the pair is actually in this position, try to switch
if (!players[target].positiondelay)
{
K_DirectorSwitch(target, false);
}
// even if we're not certain, if we're certain we're watching the WRONG player, try to switch
if (players[*displayplayerp].position != targetposition+1 && !players[*displayplayerp].positiondelay)
{
K_DirectorSwitch(target, false);
}
break;
}
}
void K_ToggleDirector(boolean active)
{
if (directorinfo.active != active)
{
directorinfo.cooldown = 0; // switch immediately
}
directorinfo.active = active;
}
boolean K_DirectorIsAvailable(UINT8 viewnum)
{
return viewnum <= r_splitscreen && viewnum < G_PartySize(consoleplayer) &&
displayplayers[viewnum] != G_PartyMember(consoleplayer, viewnum);
}

384
src/k_director.cpp Normal file
View file

@ -0,0 +1,384 @@
// SONIC ROBO BLAST 2 KART
//-----------------------------------------------------------------------------
/// \file k_director.c
/// \brief SRB2kart automatic spectator camera.
#include <algorithm>
#include <array>
#include "cxxutil.hpp"
#include "k_kart.h"
#include "k_respawn.h"
#include "doomdef.h"
#include "doomstat.h"
#include "g_game.h"
#include "v_video.h"
#include "k_director.h"
#include "d_netcmd.h"
#include "p_local.h"
#include "g_party.h"
#include "command.h"
extern "C" consvar_t cv_devmode_screen;
#define SWITCHTIME TICRATE * 5 // cooldown between unforced switches
#define BOREDOMTIME 3 * TICRATE / 2 // how long until players considered far apart?
#define TRANSFERTIME TICRATE // how long to delay reaction shots?
#define BREAKAWAYDIST 4000 // how *far* until players considered far apart?
#define WALKBACKDIST 600 // how close should a trailing player be before we switch?
#define PINCHDIST 30000 // how close should the leader be to be considered "end of race"?
static fixed_t K_GetFinishGap(INT32 leader, INT32 follower)
{
fixed_t dista = players[follower].distancetofinish;
fixed_t distb = players[leader].distancetofinish;
if (players[follower].position < players[leader].position)
{
return distb - dista;
}
else
{
return dista - distb;
}
}
struct DirectorInfo
{
UINT8 viewnum; // which screen does this director apply to?
boolean active = false; // is view point switching enabled?
tic_t cooldown = SWITCHTIME; // how long has it been since we last switched?
tic_t freeze = 0; // when nonzero, fixed switch pending, freeze logic!
INT32 attacker = 0; // who to switch to when freeze delay elapses
INT32 maxdist = 0; // how far is the closest player from finishing?
struct PlayerStat
{
INT32 sorted = -1; // position-1 goes in, player index comes out.
INT32 gap = INT32_MAX; // gap between a given position and their closest pursuer
INT32 boredom = 0; // how long has a given position had no credible attackers?
}
playerstat[MAXPLAYERS];
DirectorInfo(UINT8 viewnum_) : viewnum(viewnum_) {}
INT32 viewplayernum() const { return displayplayers[viewnum]; }
player_t* viewplayer() const { return &players[viewplayernum()]; }
void update()
{
INT32 targetposition;
update_positions();
if (cooldown > 0) {
cooldown--;
}
// handle pending forced switches
if (freeze > 0)
{
if (!(--freeze))
change(attacker, true);
return;
}
// if there's only one player left in the list, just switch to that player
if (playerstat[0].sorted != -1 && playerstat[1].sorted == -1)
{
change(playerstat[0].sorted, false);
return;
}
// aaight, time to walk through the standings to find the first interesting pair
// NB: targetposition/PlayerStat::sorted is 0-indexed, aiming at the "back half" of a given pair by default.
// we adjust for this when comparing to player->position or when looking at the leading player, Don't Freak Out
for (targetposition = 1; targetposition < MAXPLAYERS; targetposition++)
{
INT32 target;
// you are out of players, try again
if (playerstat[targetposition].sorted == -1)
{
break;
}
// pair too far apart? try the next one
if (playerstat[targetposition - 1].boredom >= BOREDOMTIME)
{
continue;
}
// pair finished? try the next one
if (players[playerstat[targetposition].sorted].exiting)
{
continue;
}
// don't risk switching away from forward pairs at race end, might miss something!
if (maxdist > PINCHDIST)
{
// if the "next" player is close enough, they should be able to see everyone fine!
// walk back through the standings to find a vantage that gets everyone in frame.
// (also creates a pretty cool effect w/ overtakes at speed)
while (targetposition < MAXPLAYERS && playerstat[targetposition].gap < WALKBACKDIST)
{
targetposition++;
}
}
target = playerstat[targetposition].sorted;
// stop here since we're already viewing this player
if (viewplayernum() == target)
{
break;
}
// if this is a splitscreen player, try next pair
if (P_IsDisplayPlayer(&players[target]))
{
continue;
}
// if we're certain the back half of the pair is actually in this position, try to switch
if (!players[target].positiondelay)
{
change(target, false);
}
// even if we're not certain, if we're certain we're watching the WRONG player, try to switch
if (viewplayer()->position != targetposition+1 && !viewplayer()->positiondelay)
{
change(target, false);
}
break;
}
}
void force_switch(INT32 player, INT32 time)
{
if (players[player].exiting)
{
return;
}
attacker = player;
freeze = time;
}
private:
void update_positions()
{
INT32 playernum;
INT32 position;
player_t* target;
for (PlayerStat& stat : playerstat)
{
stat.sorted = -1;
}
for (playernum = 0; playernum < MAXPLAYERS; playernum++)
{
target = &players[playernum];
if (playeringame[playernum] && !target->spectator && target->position > 0)
{
playerstat[target->position - 1].sorted = playernum;
}
}
for (position = 0; position < MAXPLAYERS - 1; position++)
{
playerstat[position].gap = INT32_MAX;
if (playerstat[position].sorted == -1 || playerstat[position + 1].sorted == -1)
{
continue;
}
playerstat[position].gap = P_ScaleFromMap(
K_GetFinishGap(playerstat[position].sorted, playerstat[position + 1].sorted),
FRACUNIT
);
if (playerstat[position].gap >= BREAKAWAYDIST)
{
playerstat[position].boredom = std::min(BOREDOMTIME * 2, playerstat[position].boredom + 1);
}
else if (playerstat[position].boredom > 0)
{
playerstat[position].boredom--;
}
}
maxdist = P_ScaleFromMap(players[playerstat[0].sorted].distancetofinish, FRACUNIT);
}
bool can_change() const
{
if (viewplayer()->trickpanel > 0)
{
return false;
}
if (cooldown > 0)
{
return false;
}
return true;
}
void change(INT32 player, boolean force)
{
if (!active)
{
return;
}
if (players[player].exiting)
{
return;
}
if (!force && !can_change())
{
return;
}
G_ResetView(1 + viewnum, player, true);
cooldown = SWITCHTIME;
}
};
struct DirectorInfoManager
{
DirectorInfo& operator [](UINT8 viewnum)
{
SRB2_ASSERT(viewnum < MAXSPLITSCREENPLAYERS);
return info_[viewnum];
}
auto begin() { return info_.begin(); }
auto end() { return std::next(begin(), r_splitscreen + 1); }
private:
static_assert(MAXSPLITSCREENPLAYERS == 4);
std::array<DirectorInfo, MAXSPLITSCREENPLAYERS> info_{0, 1, 2, 3};
}
g_directorinfo;
void K_InitDirector(void)
{
g_directorinfo = {};
}
void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source)
{
for (DirectorInfo& directorinfo : g_directorinfo)
{
if (directorinfo.viewplayer() != player)
{
continue;
}
if (inflictor && inflictor->player)
{
directorinfo.force_switch(inflictor->player - players, TRANSFERTIME);
}
else if (source && source->player)
{
directorinfo.force_switch(source->player - players, TRANSFERTIME);
}
}
}
void K_DrawDirectorDebugger(void)
{
INT32 position;
INT32 leader;
INT32 follower;
INT32 ytxt;
if (!cv_kartdebugdirector.value)
{
return;
}
DirectorInfo& directorinfo = g_directorinfo[cv_devmode_screen.value];
V_DrawThinString(10, 0, V_70TRANS, va("PLACE"));
V_DrawThinString(40, 0, V_70TRANS, va("CONF?"));
V_DrawThinString(80, 0, V_70TRANS, va("GAP"));
V_DrawThinString(120, 0, V_70TRANS, va("BORED"));
V_DrawThinString(150, 0, V_70TRANS, va("COOLDOWN: %d", directorinfo.cooldown));
V_DrawThinString(230, 0, V_70TRANS, va("MAXDIST: %d", directorinfo.maxdist));
for (position = 0; position < MAXPLAYERS - 1; position++)
{
ytxt = 10 * (position + 1);
leader = directorinfo.playerstat[position].sorted;
follower = directorinfo.playerstat[position + 1].sorted;
if (leader == -1 || follower == -1)
break;
V_DrawThinString(10, ytxt, V_70TRANS, va("%d", position));
V_DrawThinString(20, ytxt, V_70TRANS, va("%d", position + 1));
if (players[leader].positiondelay)
{
V_DrawThinString(40, ytxt, V_70TRANS, va("NG"));
}
V_DrawThinString(80, ytxt, V_70TRANS, va("%d", directorinfo.playerstat[position].gap));
if (directorinfo.playerstat[position].boredom >= BOREDOMTIME)
{
V_DrawThinString(120, ytxt, V_70TRANS, va("BORED"));
}
else
{
V_DrawThinString(120, ytxt, V_70TRANS, va("%d", directorinfo.playerstat[position].boredom));
}
V_DrawThinString(150, ytxt, V_70TRANS, va("%s", player_names[leader]));
V_DrawThinString(230, ytxt, V_70TRANS, va("%s", player_names[follower]));
}
}
void K_UpdateDirector(void)
{
for (DirectorInfo& directorinfo : g_directorinfo)
{
directorinfo.update();
}
}
void K_ToggleDirector(UINT8 viewnum, boolean active)
{
DirectorInfo& directorinfo = g_directorinfo[viewnum];
if (directorinfo.active != active)
{
directorinfo.cooldown = 0; // switch immediately
}
directorinfo.active = active;
}
boolean K_DirectorIsEnabled(UINT8 viewnum)
{
return g_directorinfo[viewnum].active;
}
boolean K_DirectorIsAvailable(UINT8 viewnum)
{
return viewnum <= r_splitscreen && viewnum < G_PartySize(consoleplayer) &&
displayplayers[viewnum] != G_PartyMember(consoleplayer, viewnum);
}

View file

@ -10,24 +10,12 @@
extern "C" {
#endif
extern struct directorinfo
{
boolean active; // is view point switching enabled?
tic_t cooldown; // how long has it been since we last switched?
tic_t freeze; // when nonzero, fixed switch pending, freeze logic!
INT32 attacker; // who to switch to when freeze delay elapses
INT32 maxdist; // how far is the closest player from finishing?
INT32 sortedplayers[MAXPLAYERS]; // position-1 goes in, player index comes out.
INT32 gap[MAXPLAYERS]; // gap between a given position and their closest pursuer
INT32 boredom[MAXPLAYERS]; // how long has a given position had no credible attackers?
} directorinfo;
void K_InitDirector(void);
void K_UpdateDirector(void);
void K_DrawDirectorDebugger(void);
void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source);
void K_ToggleDirector(boolean active);
void K_ToggleDirector(UINT8 viewnum, boolean active);
boolean K_DirectorIsEnabled(UINT8 viewnum);
boolean K_DirectorIsAvailable(UINT8 viewnum);
#ifdef __cplusplus

View file

@ -3381,7 +3381,7 @@ static boolean K_ShowPlayerNametag(player_t *p)
return false;
}
if (demo.playback == true && demo.freecam == true)
if (demo.playback == true && camera[R_GetViewNumber()].freecam == true)
{
return true;
}
@ -3762,7 +3762,7 @@ static void K_drawKartNameTags(void)
if (result.onScreen == true)
{
if (!(demo.playback == true && demo.freecam == true) && P_IsDisplayPlayer(ntplayer) &&
if (!(demo.playback == true && camera[cnum].freecam == true) && P_IsDisplayPlayer(ntplayer) &&
ntplayer != &players[displayplayers[cnum]])
{
localindicator = G_PartyPosition(ntplayer - players);
@ -5169,77 +5169,6 @@ void K_drawButtonAnim(INT32 x, INT32 y, INT32 flags, patch_t *button[2], tic_t a
K_drawButton(x << FRACBITS, y << FRACBITS, flags, button, anim);
}
static void K_DrawDirectorButton(INT32 idx, const char *label, patch_t *kp[2], INT32 textflags)
{
INT32 flags = V_SNAPTORIGHT | V_SLIDEIN | V_SPLITSCREEN;
INT32 x = (BASEVIDWIDTH/2) - 10;
INT32 y = (idx * 16);
if (r_splitscreen <= 1)
{
x = BASEVIDWIDTH - 60;
if (r_splitscreen == 0)
{
y += BASEVIDHEIGHT - 78;
}
}
textflags |= flags;
K_drawButtonAnim(x, y - 4, flags, kp, leveltime);
V_DrawRightAlignedThinString(x - 2, y, textflags, label);
}
static void K_drawDirectorHUD(void)
{
const UINT8 viewnum = R_GetViewNumber();
const INT32 p = viewnum < G_PartySize(consoleplayer) ? G_PartyMember(consoleplayer, viewnum) : -1;
const char *itemtxt = "Join";
UINT8 offs = 0;
UINT8 numingame = 0;
UINT8 i;
if (!LUA_HudEnabled(hud_textspectator))
{
return;
}
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && !players[i].spectator)
numingame++;
if (numingame > 1 && r_splitscreen == 0) // simplifies things a lot
{
K_DrawDirectorButton(1, "Next Player", kp_button_a[0], 0);
K_DrawDirectorButton(2, "Prev Player", kp_button_x[0], 0);
offs = 2;
}
K_DrawDirectorButton(offs + 1, "Freecam", kp_button_c[0], 0);
if (p == -1 || !playeringame[p] || players[p].spectator == false)
{
return;
}
// TODO: this is too close to the screen bottom
K_DrawDirectorButton(offs + 2, "Director", kp_button_r,
(directorinfo.active ? V_YELLOWMAP : 0));
if (players[p].flashing)
itemtxt = ". . .";
else if (players[p].pflags & PF_WANTSTOJOIN)
itemtxt = "Cancel Join";
if (cv_maxplayers.value)
{
itemtxt = va("%s [%d/%d]", itemtxt, numingame, cv_maxplayers.value);
}
K_DrawDirectorButton(0, itemtxt, kp_button_l, 0);
}
static void K_drawDistributionDebugger(void)
{
itemroulette_t rouletteData = {0};
@ -5455,8 +5384,8 @@ static void K_DrawGPRankDebugger(void)
void K_drawKartHUD(void)
{
boolean islonesome = false;
boolean freecam = demo.freecam; //disable some hud elements w/ freecam
UINT8 viewnum = R_GetViewNumber();
boolean freecam = camera[viewnum].freecam; //disable some hud elements w/ freecam
// Define the X and Y for each drawn object
// This is handled by console/menu values
@ -5531,7 +5460,7 @@ void K_drawKartHUD(void)
}
}
if (!stplyr->spectator && !demo.freecam) // Bottom of the screen elements, don't need in spectate mode
if (!stplyr->spectator && !freecam) // Bottom of the screen elements, don't need in spectate mode
{
if (demo.title) // Draw title logo instead in demo.titles
{
@ -5666,9 +5595,9 @@ void K_drawKartHUD(void)
if (stplyr->karthud[khud_trickcool])
K_drawTrickCool();
if (freecam)
if ((freecam || stplyr->spectator) && LUA_HudEnabled(hud_textspectator))
{
K_DrawDirectorButton(3, "Freecam", kp_button_c[0], 0);
K_drawSpectatorHUD(false);
}
if (modeattacking || freecam) // everything after here is MP and debug only
@ -5687,9 +5616,9 @@ void K_drawKartHUD(void)
K_drawKartPowerUps();
if (K_DirectorIsAvailable(viewnum) == true)
if (K_DirectorIsAvailable(viewnum) == true && LUA_HudEnabled(hud_textspectator))
{
K_drawDirectorHUD();
K_drawSpectatorHUD(true);
}
if (cv_kartdebugdistribution.value)

View file

@ -43,6 +43,7 @@ void K_LoadKartHUDGraphics(void);
void K_drawKartHUD(void);
void K_drawKartFreePlay(void);
void K_drawKartPowerUps(void);
void K_drawSpectatorHUD(boolean director);
void K_drawKartTimestamp(tic_t drawtime, INT32 TX, INT32 TY, INT32 splitflags, UINT8 mode);
void K_drawKart2PTimestamp(void);
void K_drawKart4PTimestamp(void);

View file

@ -12279,7 +12279,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim)
victim->eggmanexplode = 6*TICRATE;
K_StopRoulette(&victim->itemRoulette);
if (P_IsDisplayPlayer(victim) && !demo.freecam)
if (P_IsDisplayPlayer(victim))
S_StartSound(NULL, sfx_itrole);
K_AddHitLag(source->mo, 2, true);

View file

@ -1633,7 +1633,7 @@ void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd)
//player->karthud[khud_itemblinkmode] = 1;
//player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT);
if (P_IsDisplayPlayer(player) && !demo.freecam)
if (P_IsDisplayPlayer(player))
{
S_StartSound(NULL, sfx_itrole);
}
@ -1680,7 +1680,7 @@ void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd)
player->karthud[khud_itemblinkmode] = 0;
player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT);
if (P_IsDisplayPlayer(player) && !demo.freecam)
if (P_IsDisplayPlayer(player))
{
if (roulette->ringbox)
{
@ -1714,7 +1714,7 @@ void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd)
// This makes the roulette produce the random noises.
roulette->sound = (roulette->sound + 1) % 8;
if (P_IsDisplayPlayer(player) && !demo.freecam)
if (P_IsDisplayPlayer(player))
{
if (roulette->ringbox)
S_StartSound(NULL, sfx_s240);

View file

@ -236,7 +236,11 @@ void M_PlaybackToggleFreecam(INT32 choice)
splitscreen = 0;
R_ExecuteSetViewSize();
P_ToggleDemoCamera();
UINT8 i;
for (i = 0; i <= r_splitscreen; ++i)
{
P_ToggleDemoCamera(i);
}
}
void M_PlaybackQuit(INT32 choice)

View file

@ -92,6 +92,8 @@ void P_UnlinkThinker(thinker_t *thinker);
struct camera_t
{
boolean chase;
boolean freecam;
angle_t aiming;
// Things used by FS cameras.
@ -126,6 +128,9 @@ struct camera_t
// SRB2Kart: camera pitches on slopes
angle_t pitch;
// Freecam: A button was held since entering from menu, so don't move camera
UINT8 button_a_held;
// Freecam: aiming needs to be reset after switching from chasecam
boolean reset_aiming;
@ -134,14 +139,6 @@ struct camera_t
angle_t old_angle, old_aiming;
};
// demo freecam or something before i commit die
struct demofreecam_s {
UINT8 button_a_held; // A button was held since entering from menu, so don't move camera
};
extern struct demofreecam_s democam;
extern camera_t camera[MAXSPLITSCREENPLAYERS];
extern consvar_t cv_cam_dist[MAXSPLITSCREENPLAYERS], cv_cam_still[MAXSPLITSCREENPLAYERS], cv_cam_height[MAXSPLITSCREENPLAYERS];
extern consvar_t cv_cam_speed[MAXSPLITSCREENPLAYERS], cv_cam_rotate[MAXSPLITSCREENPLAYERS];
@ -156,7 +153,7 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam);
void P_SlideCameraMove(camera_t *thiscam);
void P_DemoCameraMovement(camera_t *cam, UINT8 num);
boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcalled);
void P_ToggleDemoCamera(void);
void P_ToggleDemoCamera(UINT8 viewnum);
boolean P_PlayerInPain(player_t *player);
void P_ResetPlayer(player_t *player);

View file

@ -50,6 +50,7 @@
#include "k_director.h"
#include "m_easing.h"
#include "k_podium.h"
#include "g_party.h"
actioncache_t actioncachehead;
@ -12220,22 +12221,29 @@ void P_SpawnPlayer(INT32 playernum)
}
}
// Spectating when there is literally any other player in
// the level enables director cam. Or if the first player
// enters the game, spectate them.
// TODO: how do we support splitscreen?
if (playernum == consoleplayer || pcount == 1)
{
K_ToggleDirector(players[consoleplayer].spectator && pcount > 0);
}
boolean director = p->spectator && pcount > 0;
// TODO: handle splitscreen
// Spectators can switch to freecam. This should be
// disabled when they enter the race, or when the level
// changes.
if (playernum == consoleplayer && !demo.playback)
if (G_IsPartyLocal(playernum))
{
demo.freecam = false;
// Spectating when there is literally any other
// player in the level enables director cam.
K_ToggleDirector(G_PartyPosition(playernum), director);
// Spectators can switch to freecam. This should be
// disabled when they enter the race, or when the level
// changes.
if (!demo.playback)
{
camera[G_PartyPosition(playernum)].freecam = false;
}
}
else if (pcount == 1)
{
// If the first player enters the game, view them.
for (i = 0; i <= r_splitscreen; ++i)
{
K_ToggleDirector(i, director);
}
}
}

View file

@ -1077,17 +1077,17 @@ boolean P_IsDisplayPlayer(player_t *player)
return false;
}
// Freecam still techically has a player in
// displayplayers. But since the camera is detached, it
// would be weird if sounds were heard from that player's
// perspective.
if (demo.freecam)
{
return false;
}
for (i = 0; i <= r_splitscreen; i++) // DON'T skip P1
{
if (camera[i].freecam)
{
// Freecam still techically has a player in
// displayplayers. But since the camera is
// detached, it would be weird if sounds were
// heard from that player's perspective.
continue;
}
if (player == &players[displayplayers[i]])
return true;
}
@ -2802,11 +2802,10 @@ static void P_DeathThink(player_t *player)
}
}
// TODO: support splitscreen
// Spectate another player after 2 seconds
if (player == &players[consoleplayer] && playerGone == true && (gametyperules & GTR_BUMPERS) && player->deadtimer == 2*TICRATE)
if (G_IsPartyLocal(player - players) && playerGone == true && (gametyperules & GTR_BUMPERS) && player->deadtimer == 2*TICRATE)
{
K_ToggleDirector(true);
K_ToggleDirector(G_PartyPosition(player - players), true);
}
// Keep time rolling
@ -2885,8 +2884,6 @@ fixed_t t_cam_dist[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};
fixed_t t_cam_height[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};
fixed_t t_cam_rotate[MAXSPLITSCREENPLAYERS] = {-42,-42,-42,-42};
struct demofreecam_s democam;
void P_DemoCameraMovement(camera_t *cam, UINT8 num)
{
ticcmd_t *cmd;
@ -2914,27 +2911,31 @@ void P_DemoCameraMovement(camera_t *cam, UINT8 num)
cam->angle += turning;
// camera movement:
if (!democam.button_a_held)
if (!cam->button_a_held)
{
if (cmd->buttons & BT_ACCELERATE)
int dir = ((cmd->buttons & BT_ACCELERATE) ? 1 : 0) + ((cmd->buttons & BT_BRAKE) ? -1 : 0);
switch (dir)
{
cam->z += 32*mapobjectscale;
moving = true;
}
else if (cmd->buttons & BT_BRAKE)
{
cam->z -= 32*mapobjectscale;
moving = true;
case 1:
cam->z += 32*mapobjectscale;
moving = true;
break;
case -1:
cam->z -= 32*mapobjectscale;
moving = true;
break;
}
}
if (!(cmd->buttons & (BT_ACCELERATE | BT_DRIFT)) && democam.button_a_held)
if (!(cmd->buttons & (BT_ACCELERATE | BT_DRIFT)) && cam->button_a_held)
{
democam.button_a_held--;
cam->button_a_held--;
}
// if you hold item, you will lock on to displayplayer. (The last player you were ""f12-ing"")
if (demo.freecam && cmd->buttons & BT_ATTACK)
// if you hold Y, you will lock on to displayplayer. (The last player you were ""f12-ing"")
if (cam->freecam && cmd->buttons & BT_RESPAWN)
{
lastp = &players[displayplayers[0]]; // Fun fact, I was trying displayplayers[0]->mo as if it was Lua like an absolute idiot.
cam->angle = R_PointToAngle2(cam->x, cam->y, lastp->mo->x, lastp->mo->y);
@ -2954,7 +2955,7 @@ void P_DemoCameraMovement(camera_t *cam, UINT8 num)
// forward/back will have a slope. So, as long as democam
// controls haven't been used to alter the vertical angle,
// slowly reset it to flat.
if ((cam->reset_aiming && moving) || ((cmd->buttons & BT_DRIFT) && !democam.button_a_held))
if ((cam->reset_aiming && moving) || ((cmd->buttons & BT_DRIFT) && !cam->button_a_held))
{
INT32 aiming = cam->aiming;
INT32 smooth = FixedMul(ANGLE_11hh / 4, FCOS(cam->aiming));
@ -2995,17 +2996,19 @@ void P_DemoCameraMovement(camera_t *cam, UINT8 num)
cam->subsector = R_PointInSubsector(cam->x, cam->y);
}
void P_ToggleDemoCamera(void)
void P_ToggleDemoCamera(UINT8 viewnum)
{
if (!demo.freecam) // toggle on
camera_t *cam = &camera[viewnum];
if (!cam->freecam) // toggle on
{
demo.freecam = true;
democam.button_a_held = 2;
camera[0].reset_aiming = true;
cam->freecam = true;
cam->button_a_held = 2;
cam->reset_aiming = true;
}
else // toggle off
{
demo.freecam = false;
cam->freecam = false;
}
}
@ -3014,7 +3017,7 @@ void P_ResetCamera(player_t *player, camera_t *thiscam)
tic_t tries = 0;
fixed_t x, y, z;
if (demo.freecam)
if (thiscam->freecam)
return; // do not reset the camera there.
if (!player->mo)
@ -3100,7 +3103,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
num = 0;
}
if (demo.freecam || player->spectator)
if (thiscam->freecam || player->spectator)
{
P_DemoCameraMovement(thiscam, num);
return true;

View file

@ -906,7 +906,7 @@ void R_ApplyViewMorph(int s)
width*vid.bpp, height, width*vid.bpp, vid.width);
}
angle_t R_ViewRollAngle(const player_t *player)
angle_t R_ViewRollAngle(const player_t *player, UINT8 viewnum)
{
angle_t roll = 0;
@ -927,7 +927,7 @@ angle_t R_ViewRollAngle(const player_t *player)
if (cv_tilting.value)
{
if (!player->spectator && !demo.freecam)
if (!player->spectator && !camera[viewnum].freecam)
{
roll += player->tilt;
}
@ -1169,7 +1169,7 @@ R_SetupCommonFrame
newview->y += offset.y;
newview->z += offset.z;
newview->roll = R_ViewRollAngle(player);
newview->roll = R_ViewRollAngle(player, viewnum);
if (subsector)
newview->sector = subsector->sector;

View file

@ -168,7 +168,7 @@ void R_Init(void);
void R_CheckViewMorph(int split);
void R_ApplyViewMorph(int split);
angle_t R_ViewRollAngle(const player_t *player);
angle_t R_ViewRollAngle(const player_t *player, UINT8 viewnum);
// just sets setsizeneeded true
extern boolean setsizeneeded;

View file

@ -444,7 +444,7 @@ void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
listenmobj[i] = player->mo;
}
if (origin && origin == listenmobj[i] && !demo.freecam)
if (origin && origin == listenmobj[i] && !camera[i].freecam)
{
itsUs = true;
}
@ -755,15 +755,15 @@ void S_UpdateSounds(void)
{
boolean itsUs = false;
if (!demo.freecam)
for (i = r_splitscreen; i >= 0; i--)
{
for (i = r_splitscreen; i >= 0; i--)
{
if (c->origin != listenmobj[i])
continue;
if (camera[i].freecam)
continue;
itsUs = true;
}
if (c->origin != listenmobj[i])
continue;
itsUs = true;
}
if (itsUs == false)

View file

@ -1192,7 +1192,7 @@ static void ST_overlayDrawer(void)
{
if (cv_showviewpointtext.value)
{
if (!demo.title && !P_IsLocalPlayer(stplyr) && !demo.freecam)
if (!demo.title && !P_IsLocalPlayer(stplyr) && !camera[viewnum].freecam)
{
if (!r_splitscreen)
{
@ -1215,55 +1215,6 @@ static void ST_overlayDrawer(void)
}
}
if (!hu_showscores && netgame && !mapreset)
{
if (stplyr->spectator && displayplayers[viewnum] == g_localplayers[viewnum] && LUA_HudEnabled(hud_textspectator))
{
const char *itemtxt = M_GetText("Item - Join Game");
if (stplyr->flashing)
itemtxt = M_GetText("Item - . . .");
else if (stplyr->pflags & PF_WANTSTOJOIN)
itemtxt = M_GetText("Item - Cancel Join");
else if (G_GametypeHasTeams())
itemtxt = M_GetText("Item - Join Team");
if (cv_maxplayers.value)
{
UINT8 numingame = 0;
UINT8 i;
for (i = 0; i < MAXPLAYERS; i++)
if (playeringame[i] && !players[i].spectator)
numingame++;
itemtxt = va("%s (%s: %d)", itemtxt, M_GetText("Slots left"), max(0, cv_maxplayers.value - numingame));
}
// SRB2kart: changed positions & text
if (r_splitscreen)
{
V_DrawThinString(2, (BASEVIDHEIGHT/2)-20, V_YELLOWMAP|V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("- SPECTATING -"));
V_DrawThinString(2, (BASEVIDHEIGHT/2)-10, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, itemtxt);
}
else
{
V_DrawString(2, BASEVIDHEIGHT-40, V_HUDTRANSHALF|V_SPLITSCREEN|V_YELLOWMAP|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("- SPECTATING -"));
V_DrawString(2, BASEVIDHEIGHT-30, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, itemtxt);
if (stplyr->cmd.buttons & BT_LOOKBACK)
{
V_DrawString(2, BASEVIDHEIGHT-20, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Lookback - Camera pivot"));
V_DrawString(2, BASEVIDHEIGHT-10, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Aim forward/backward"));
}
else
{
V_DrawString(2, BASEVIDHEIGHT-20, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Accelerate - Float"));
V_DrawString(2, BASEVIDHEIGHT-10, V_HUDTRANSHALF|V_SPLITSCREEN|V_SNAPTOLEFT|V_SNAPTOBOTTOM, M_GetText("Brake - Sink"));
}
}
}
}
K_DrawMidVote();
K_DrawDialogue();

View file

@ -10,6 +10,7 @@
#include "doomdef.h" // skincolornum_t
#include "doomtype.h"
#include "hu_stuff.h"
#include "i_time.h"
#include "k_hud.h"
#include "m_fixed.h"
#include "r_draw.h"
@ -84,6 +85,53 @@ void Chain::string(const char* str, INT32 flags, Font font) const
V_DrawStringScaled(x, y, FloatToFixed(scale_), FRACUNIT, FRACUNIT, flags, colormap_, font_to_fontno(font), str);
}
namespace
{
patch_t** get_button_patch(Draw::Button type, int ver)
{
switch (type)
{
#define X(x) \
case Draw::Button::x:\
return kp_button_ ## x
X(a)[ver];
X(b)[ver];
X(c)[ver];
X(x)[ver];
X(y)[ver];
X(z)[ver];
X(start);
X(l);
X(r);
X(up);
X(down);
X(right);
X(left);
#undef X
}
return nullptr;
};
}; // namespace
void Chain::button(Button type, int ver, std::optional<bool> press) const
{
const auto _ = Clipper(*this);
if (press)
{
K_drawButton(FloatToFixed(x_), FloatToFixed(y_), flags_, get_button_patch(type, ver), *press);
}
else
{
K_drawButtonAnim(x_, y_, flags_, get_button_patch(type, ver), I_GetTime());
}
}
Chain::Clipper::Clipper(const Chain& chain)
{
V_SetClipRect(

View file

@ -54,6 +54,23 @@ public:
kBoth,
};
enum class Button
{
a,
b,
c,
x,
y,
z,
start,
l,
r,
up,
down,
right,
left,
};
class TextElement
{
public:
@ -146,6 +163,9 @@ public:
void fill(UINT8 color) const;
void button(Button type, std::optional<bool> press = {}) const { button(type, 0, press); }
void small_button(Button type, std::optional<bool> press = {}) const { button(type, 1, press); }
private:
constexpr Chain() {}
explicit Chain(float x, float y) : x_(x), y_(y) {}
@ -178,6 +198,7 @@ public:
const UINT8* colormap_ = nullptr;
void string(const char* str, INT32 flags, Font font) const;
void button(Button type, int ver, std::optional<bool> press = {}) const;
friend Draw;
};
@ -222,6 +243,7 @@ public:
VOID_METHOD(patch);
VOID_METHOD(thumbnail);
VOID_METHOD(fill);
VOID_METHOD(button);
#undef VOID_METHOD