RingRacers/src/k_podium.cpp
2025-08-12 16:49:14 -05:00

1554 lines
35 KiB
C++

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Sally "TehRealSalt" Cochenour
// Copyright (C) 2025 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.
//-----------------------------------------------------------------------------
/// \file k_podium.c
/// \brief Grand Prix podium cutscene
#include "k_podium.h"
#include "core/string.h"
#include "doomdef.h"
#include "d_main.h"
#include "d_netcmd.h"
#include "f_finale.h"
#include "g_game.h"
#include "hu_stuff.h"
#include "r_local.h"
#include "s_sound.h"
#include "i_time.h"
#include "i_video.h"
#include "v_video.h"
#include "w_wad.h"
#include "z_zone.h"
#include "i_system.h"
#include "i_threads.h"
#include "dehacked.h"
#include "g_input.h"
#include "console.h"
#include "m_random.h"
#include "m_misc.h" // moviemode functionality
#include "y_inter.h"
#include "m_cond.h"
#include "p_local.h"
#include "p_saveg.h"
#include "p_setup.h"
#include "st_stuff.h" // hud hiding
#include "fastcmp.h"
#include "lua_hud.h"
#include "lua_hook.h"
#include "k_menu.h"
#include "k_grandprix.h"
#include "k_rank.h"
#include "v_draw.hpp"
#include "k_hud.h"
typedef enum
{
PODIUM_ST_CONGRATS_SLIDEIN,
PODIUM_ST_CONGRATS_SLIDEUP,
PODIUM_ST_DATA_SLIDEIN,
PODIUM_ST_DATA_PAUSE,
PODIUM_ST_LEVEL_APPEAR,
PODIUM_ST_LEVEL_PAUSE,
PODIUM_ST_TOTALS_SLIDEIN,
PODIUM_ST_TOTALS_PAUSE,
PODIUM_ST_GRADE_APPEAR,
PODIUM_ST_GRADE_VOICE,
PODIUM_ST_DONE,
PODIUM_ST_EXIT,
} podium_state_e;
static struct podiumData_s
{
boolean ranking;
gpRank_t rank;
gp_rank_e grade;
podium_state_e state;
INT32 delay;
INT32 transition, transitionTime;
UINT8 displayLevels;
sfxenum_t gradeVoice;
cupheader_t *cup;
UINT8 emeraldnum;
boolean fastForward;
char header[64];
char difficulty[64];
void Init(void);
void NextLevel(void);
void Tick(void);
void Draw(void);
} g_podiumData;
void podiumData_s::Init(void)
{
fastForward = false;
if (grandprixinfo.cup != nullptr)
{
rank = grandprixinfo.rank;
cup = grandprixinfo.cup;
emeraldnum = cup->emeraldnum;
}
else
{
// construct fake rank for testing podium
// directly from the editor
size_t cupID = M_RandomRange(0, numkartcupheaders-1);
cup = kartcupheaders;
for (size_t i = 0; i < cupID; i++)
{
if (cup == nullptr)
{
break;
}
cup = cup->next;
}
emeraldnum = 0;
memset(&rank, 0, sizeof(gpRank_t));
rank.skin = players[consoleplayer].skin;
rank.numPlayers = std::clamp<UINT8>(M_RandomRange(0, MAXSPLITSCREENPLAYERS + 1), 1, MAXSPLITSCREENPLAYERS);
rank.totalPlayers = K_GetGPPlayerCount(rank.numPlayers);
rank.position = M_RandomRange(1, 4);
rank.continuesUsed = M_RandomRange(0, 3);
// Fake totals
rank.numLevels = 8;
constexpr INT32 numRaces = 5;
for (INT32 i = 0; i < rank.numPlayers; i++)
{
rank.totalPoints += numRaces * K_CalculateGPRankPoints(EXP_MAX, i+1, rank.totalPlayers);
}
rank.totalRings = numRaces * rank.numPlayers * 20;
// Randomized winnings
INT32 rgs = 0;
INT32 exp = 0;
INT32 texp = 0;
INT32 prs = 0;
INT32 tprs = 0;
rank.winPoints = M_RandomRange(0, rank.totalPoints);
for (INT32 i = 0; i < rank.numLevels; i++)
{
gpRank_level_t *const lvl = &rank.levels[i];
UINT8 specialWinner = 0;
UINT16 pprs = 0;
UINT16 pexp = 0;
lvl->id = M_RandomRange(4, nummapheaders);
lvl->event = GPEVENT_NONE;
switch (i)
{
case 2:
case 5:
{
lvl->event = GPEVENT_BONUS;
lvl->totalPrisons = M_RandomRange(1, 10);
tprs += lvl->totalPrisons;
break;
}
case 7:
{
lvl->event = GPEVENT_SPECIAL;
specialWinner = M_RandomRange(0, rank.numPlayers);
break;
}
default:
{
lvl->totalExp = EXP_TARGET;
texp += lvl->totalExp * rank.numPlayers;
break;
}
}
lvl->time = M_RandomRange(50*TICRATE, 210*TICRATE);
lvl->continues = 0;
if (!M_RandomRange(0, 2))
lvl->continues = M_RandomRange(1, 3);
for (INT32 j = 0; j < rank.numPlayers; j++)
{
gpRank_level_perplayer_t *const dta = &lvl->perPlayer[j];
dta->position = M_RandomRange(1, rank.totalPlayers);
if (lvl->event == GPEVENT_NONE)
{
dta->rings = M_RandomRange(0, 20);
rgs += dta->rings;
dta->exp = M_RandomRange(EXP_MIN, EXP_MAX);
pexp += dta->exp;
}
if (lvl->event == GPEVENT_BONUS)
{
dta->prisons = M_RandomRange(0, lvl->totalPrisons);
pprs = std::max(pprs, dta->prisons);
}
if (lvl->event == GPEVENT_SPECIAL)
{
dta->gotSpecialPrize = (j+1 == specialWinner);
dta->grade = GRADE_E;
if (dta->gotSpecialPrize)
{
rank.specialWon = true;
}
}
else
{
dta->grade = static_cast<gp_rank_e>(M_RandomRange(static_cast<INT32>(GRADE_E), static_cast<INT32>(GRADE_A)));
}
}
exp += pexp;
prs += pprs;
}
rank.rings = rgs;
rank.exp = exp;
rank.totalExp = texp;
rank.prisons = prs;
rank.totalPrisons = tprs;
}
grade = K_CalculateGPGrade(&rank);
delay = TICRATE/2;
transition = 0;
transitionTime = TICRATE/2;
header[0] = '\0';
if (rank.position > RANK_NEUTRAL_POSITION)
{
snprintf(
header, sizeof header,
"NO GOOD..."
);
}
else
{
snprintf(
header, sizeof header,
"CONGRATULATIONS"
);
}
switch(grandprixinfo.gamespeed)
{
case KARTSPEED_EASY:
snprintf(difficulty, sizeof difficulty, "Relaxed");
break;
case KARTSPEED_NORMAL:
snprintf(difficulty, sizeof difficulty, "Intense");
break;
case KARTSPEED_HARD:
snprintf(difficulty, sizeof difficulty, "Vicious");
break;
default:
snprintf(difficulty, sizeof difficulty, "?");
}
if (grandprixinfo.masterbots)
snprintf(difficulty, sizeof difficulty, "Master");
if (cv_4thgear.value || cv_levelskull.value)
snprintf(difficulty, sizeof difficulty, "Extra");
header[sizeof header - 1] = '\0';
displayLevels = 0;
gradeVoice = sfx_None;
// It'd be neat to add all of the grade sounds,
// but not this close to release
if (rank.position > RANK_NEUTRAL_POSITION || grade < GRADE_C)
{
gradeVoice = skins[rank.skin]->soundsid[S_sfx[sfx_klose].skinsound];
}
else
{
gradeVoice = skins[rank.skin]->soundsid[S_sfx[sfx_kwin].skinsound];
}
}
void podiumData_s::NextLevel(void)
{
state = PODIUM_ST_LEVEL_APPEAR;
displayLevels++;
delay = TICRATE/7;
}
void podiumData_s::Tick(void)
{
if (delay > 0)
{
delay--;
return;
}
if (transition < FRACUNIT)
{
if (transitionTime <= 0)
{
transition = FRACUNIT;
return;
}
transition += FRACUNIT / transitionTime;
if (transition > FRACUNIT)
{
transition = FRACUNIT;
}
return;
}
switch (state)
{
case PODIUM_ST_CONGRATS_SLIDEIN:
{
state = PODIUM_ST_CONGRATS_SLIDEUP;
transition = 0;
transitionTime = TICRATE/2;
delay = TICRATE/2;
break;
}
case PODIUM_ST_CONGRATS_SLIDEUP:
{
state = PODIUM_ST_DATA_SLIDEIN;
transition = 0;
transitionTime = TICRATE/2;
delay = TICRATE/5;
break;
}
case PODIUM_ST_DATA_SLIDEIN:
{
state = PODIUM_ST_DATA_PAUSE;
delay = TICRATE/5;
break;
}
case PODIUM_ST_DATA_PAUSE:
{
NextLevel();
break;
}
case PODIUM_ST_LEVEL_APPEAR:
{
S_StopSoundByNum(sfx_mbs5b);
S_StartSound(nullptr, (displayLevels >= rank.numLevels) ? sfx_mbs70 : sfx_mbs5b);
state = PODIUM_ST_LEVEL_PAUSE;
delay = TICRATE/2;
break;
}
case PODIUM_ST_LEVEL_PAUSE:
{
if (displayLevels < rank.numLevels)
{
NextLevel();
}
else
{
state = PODIUM_ST_TOTALS_SLIDEIN;
transition = 0;
transitionTime = TICRATE/2;
delay = TICRATE/5;
}
break;
}
case PODIUM_ST_TOTALS_SLIDEIN:
{
state = PODIUM_ST_TOTALS_PAUSE;
delay = TICRATE/5;
break;
}
case PODIUM_ST_TOTALS_PAUSE:
{
state = PODIUM_ST_GRADE_APPEAR;
transition = 0;
transitionTime = TICRATE/7;
delay = TICRATE/2;
break;
}
case PODIUM_ST_GRADE_APPEAR:
{
S_StartSound(nullptr, sfx_rank);
if (K_CalculateGPGrade(&rank) >= GRADE_S)
S_StartSoundAtVolume(nullptr, sfx_srank, 200);
state = PODIUM_ST_GRADE_VOICE;
delay = TICRATE/2;
break;
}
case PODIUM_ST_GRADE_VOICE:
{
if (cv_kartvoices.value)
{
S_StartSound(nullptr, gradeVoice);
}
state = PODIUM_ST_DONE;
delay = 5*TICRATE;
break;
}
case PODIUM_ST_DONE:
{
if (menuactive == false && M_MenuConfirmPressed(0) == true)
{
state = PODIUM_ST_EXIT;
delay = 2*TICRATE;
}
break;
}
case PODIUM_ST_EXIT:
{
if (grandprixinfo.gp == true
&& grandprixinfo.cup != nullptr
&& grandprixinfo.cup->playcredits == true)
{
nextmap = NEXTMAP_CREDITS;
}
else
{
nextmap = NEXTMAP_TITLE;
}
G_EndGame();
return;
}
}
}
void podiumData_s::Draw(void)
{
INT32 i;
const float transition_f = FixedToFloat(transition);
const float transition_i = 1.0 - transition_f;
srb2::Draw drawer = srb2::Draw(0, 0);
INT32 fade = 5;
if (state == PODIUM_ST_CONGRATS_SLIDEIN)
{
fade = (5 * transition_f);
}
V_DrawFadeFill(
0, 0,
vid.width, vid.height,
V_NOSCALESTART,
31, fade
);
constexpr INT32 header_height = 36;
constexpr INT32 header_offset = -16;
constexpr INT32 header_centered = (BASEVIDHEIGHT * 0.5) - header_height - header_offset;
switch (state)
{
case PODIUM_ST_CONGRATS_SLIDEIN:
Y_DrawIntermissionHeader(
(BASEVIDWIDTH * transition_i * FRACUNIT),
(header_centered + header_offset) * FRACUNIT,
false, header, false, false
);
break;
case PODIUM_ST_CONGRATS_SLIDEUP:
Y_DrawIntermissionHeader(
0,
((header_centered * transition_i) + header_offset) * FRACUNIT,
false, header, false, false
);
break;
default:
Y_DrawIntermissionHeader(
0,
header_offset * FRACUNIT,
false, header, false, false
);
break;
}
const boolean singlePlayer = (rank.numPlayers == 1);
player_t *bestHuman = &players[consoleplayer];
if (singlePlayer == false)
{
UINT8 bestPos = UINT8_MAX;
for (i = 0; i < rank.numPlayers; i++)
{
// BLEH BLEH, skincolor isn't saved to GP results, so I can't use the same values that get set. ANNOYING.
if (players[i].position < bestPos)
{
bestHuman = &players[i];
bestPos = players[i].position;
}
}
}
srb2::Draw drawer_winner = drawer.xy(16, 16);
if (state >= PODIUM_ST_DATA_SLIDEIN)
{
if (state == PODIUM_ST_DATA_SLIDEIN)
{
drawer_winner = drawer_winner.x( transition_i * -BASEVIDWIDTH );
}
drawer_winner
.colormap(bestHuman->skin, static_cast<skincolornum_t>(bestHuman->skincolor))
.patch(faceprefix[bestHuman->skin][FACE_WANTED]);
drawer_winner
.xy(16, 28)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kMenu)
.text(difficulty);
drawer_winner
.xy(44, 31)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kZVote)
.text(va("%c%d", (rank.scorePosition > 0 ? '+' : ' '), rank.scorePosition));
// drawer_winner
// .xy(64, 19)
// .patch("K_POINT4");
// drawer_winner
// .xy(88, 21)
// .align(srb2::Draw::Align::kLeft)
// .font(srb2::Draw::Font::kPing)
// .colormap(TC_RAINBOW, SKINCOLOR_GOLD)
// .text(va("%d", rank.winPoints));
// drawer_winner
// .xy(75, 31)
// .align(srb2::Draw::Align::kCenter)
// .font(srb2::Draw::Font::kZVote)
// .text(va("%c%d", (rank.scoreGPPoints > 0 ? '+' : ' '), rank.scoreGPPoints));
srb2::Draw drawer_trophy = drawer.xy(272, 10);
if (state == PODIUM_ST_DATA_SLIDEIN)
{
drawer_trophy = drawer_trophy.x( transition_i * BASEVIDWIDTH );
}
if (cup != nullptr)
{
M_DrawCup(
cup, drawer_trophy.x() * FRACUNIT, drawer_trophy.y() * FRACUNIT,
0, true,
(rank.position >= 1 && rank.position <= 3) ? rank.position : 0
);
}
}
if (state >= PODIUM_ST_LEVEL_APPEAR)
{
srb2::Draw drawer_line = drawer_winner.xy(80, 28);
for (i = 0; i <= displayLevels; i++)
{
srb2::Draw drawer_perplayer = drawer_line;
gpRank_level_t *lvl = nullptr;
if (i > 0)
{
drawer_line
.xy(-88, 6)
.width(304)
.height(2)
.fill(31);
lvl = &rank.levels[i - 1];
if (lvl->id > 0)
{
char *title = G_BuildMapTitle(lvl->id);
if (title)
{
drawer_perplayer
.align(srb2::Draw::Align::kRight)
.font(srb2::Draw::Font::kThin)
.text(title);
Z_Free(title);
}
}
}
INT32 p;
for (p = 0; p < rank.numPlayers; p++)
{
player_t *const player = &players[displayplayers[p]];
if (lvl == nullptr)
{
if (singlePlayer == false)
{
drawer_perplayer
.xy(12, -2)
.colormap(player->skin, static_cast<skincolornum_t>(player->skincolor))
.patch(faceprefix[player->skin][FACE_MINIMAP]);
drawer_perplayer
.xy(26, 0)
.font(srb2::Draw::Font::kConsole)
.text(va("%c", ('A' + p)));
}
}
else
{
gpRank_level_perplayer_t *const dta = &lvl->perPlayer[p];
srb2::Draw drawer_rank = drawer_perplayer.xy(2, 0);
if (lvl->event != GPEVENT_SPECIAL && dta->grade != GRADE_INVALID)
{
drawer_rank
.xy(0, -1).flags(lvl->continues ? V_TRANSLUCENT : 0)
.colormap( static_cast<skincolornum_t>(K_GetGradeColor(dta->grade)) )
.patch(va("R_CUPRN%c", K_GetGradeChar(dta->grade)));
}
if (lvl->continues)
drawer_rank.xy(7, 1).align(srb2::Draw::Align::kCenter).font(srb2::Draw::Font::kPing).colorize(SKINCOLOR_RED).text(va("-%d", lvl->continues));
// Do not draw any stats for GAME OVERed player
if (dta->grade != GRADE_INVALID || lvl->event == GPEVENT_SPECIAL)
{
srb2::Draw drawer_gametype = drawer_rank.xy(18, 0);
switch (lvl->event)
{
case GPEVENT_BONUS:
{
drawer_gametype
.xy(0, 1)
.patch("K_CAPICO");
drawer_gametype
.xy(22, 1)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kPing)
.text(va("%d/%d", dta->prisons, lvl->totalPrisons));
break;
}
case GPEVENT_SPECIAL:
{
srb2::Draw drawer_emerald = drawer_gametype;
UINT8 emeraldNum = g_podiumData.emeraldnum;
boolean useWhiteFrame = ((leveltime & 1) || !dta->gotSpecialPrize);
patch_t *emeraldPatch = nullptr;
skincolornum_t emeraldColor = SKINCOLOR_NONE;
if (emeraldNum == 0)
{
// Prize -- todo, currently using fake Emerald
emeraldColor = SKINCOLOR_GOLD;
}
else
{
emeraldColor = static_cast<skincolornum_t>( SKINCOLOR_CHAOSEMERALD1 + ((emeraldNum - 1) % 7) );
}
{
srb2::String emeraldName;
if (emeraldNum > 7)
{
emeraldName = (useWhiteFrame ? "K_SUPER2" : "K_SUPER1");
}
else
{
emeraldName = (useWhiteFrame ? "K_EMERC" : "K_EMERW");
}
emeraldPatch = static_cast<patch_t*>( W_CachePatchName(emeraldName.c_str(), PU_CACHE) );
}
if (dta->gotSpecialPrize)
{
if (emeraldColor != SKINCOLOR_NONE)
{
drawer_emerald = drawer_emerald.colormap( emeraldColor );
}
}
else
{
drawer_emerald = drawer_emerald.colormap( TC_BLINK, SKINCOLOR_BLACK );
}
drawer_emerald
.xy(6 - (emeraldPatch->width * 0.5), 0)
.patch(emeraldPatch);
break;
}
default:
{
drawer_gametype
.xy(0, 1)
.colorize(static_cast<skincolornum_t>(SKINCOLOR_MUSTARD))
.patch("K_SPTEXP");
// Colorize the crystal, just like we do for hud
skincolornum_t overlaycolor = SKINCOLOR_MUSTARD;
fixed_t stablerateinverse = FRACUNIT - EXP_STABLERATE;
INT16 exp_range = EXP_MAX-EXP_MIN;
INT16 exp_offset = dta->exp-EXP_MIN;
fixed_t factor = (exp_offset*FRACUNIT) / exp_range; // 0.0 to 1.0 in fixed
// amount of blue is how much factor is above EXP_STABLERATE, and amount of red is how much factor is below
// assume that EXP_STABLERATE is within 0.0 to 1.0 in fixed
if (factor <= stablerateinverse)
{
overlaycolor = SKINCOLOR_RUBY;
factor = FixedDiv(factor, stablerateinverse);
}
else
{
overlaycolor = SKINCOLOR_ULTRAMARINE;
fixed_t bluemaxoffset = EXP_STABLERATE;
factor = factor - stablerateinverse;
factor = FRACUNIT - FixedDiv(factor, bluemaxoffset);
}
auto transflag = K_GetTransFlagFromFixed(factor);
drawer_gametype
.xy(0, 1)
.colorize(static_cast<skincolornum_t>(overlaycolor))
.flags(transflag)
.patch("K_SPTEXP");
drawer_gametype
.xy(23, 1)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kPing)
.text(va("%d", dta->exp));
break;
}
}
if (singlePlayer)
{
srb2::Draw drawer_rings = drawer_gametype.xy(36, 0);
if (lvl->event == GPEVENT_NONE)
{
drawer_rings
.xy(0, -1)
.patch("K_SRING1");
drawer_rings
.xy(22, 1)
.colormap(TC_RAINBOW, SKINCOLOR_YELLOW)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kPing)
.text(va("%d", dta->rings));
}
srb2::Draw drawer_timer = drawer_rings.xy(36, 0);
drawer_timer
.xy(0, 0)
.patch("K_STTIMS");
drawer_timer
.xy(32, 1)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kPing)
.text(lvl->time == UINT32_MAX ?
"--'--\"--" : va(
"%i'%02i\"%02i",
G_TicsToMinutes(lvl->time, true),
G_TicsToSeconds(lvl->time),
G_TicsToCentiseconds(lvl->time)
));
}
}
}
drawer_perplayer = drawer_perplayer.x(56);
}
drawer_line = drawer_line.y(12);
}
}
if (state >= PODIUM_ST_TOTALS_SLIDEIN)
{
srb2::Draw drawer_totals = drawer
.xy(BASEVIDWIDTH * 0.5, BASEVIDHEIGHT - 48.0);
srb2::Draw drawer_totals_left = drawer_totals
.x(-144.0);
srb2::Draw drawer_totals_right = drawer_totals
.x(72.0);
if (state == PODIUM_ST_TOTALS_SLIDEIN)
{
drawer_totals_left = drawer_totals_left.x( transition_i * -BASEVIDWIDTH );
drawer_totals_right = drawer_totals_right.x( transition_i * BASEVIDWIDTH );
}
drawer_totals_left
.xy(8.0, 8.0)
.patch("R_RTPBR");
skincolornum_t continuesColor = SKINCOLOR_NONE;
if (rank.continuesUsed == 0)
{
continuesColor = SKINCOLOR_GOLD;
}
else if (rank.scoreContinues < 0)
{
continuesColor = SKINCOLOR_RED;
}
drawer_totals_left
.y(24.0)
.patch("RANKCONT");
drawer_totals_left
.xy(44.0, 24.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kTimer)
.colormap( TC_RAINBOW, continuesColor )
.text(va("%d", rank.continuesUsed));
drawer_totals_left
.xy(44.0, 38.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kZVote)
.colormap( TC_RAINBOW, continuesColor )
.text(va("%c%d", (rank.scoreContinues >= 0 ? '+' : ' '), rank.scoreContinues));
drawer_totals_left
.patch("RANKRING");
drawer_totals_left
.xy(44.0, 0.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kThinTimer)
.text(va("%d / %d", rank.rings, rank.totalRings));
drawer_totals_left
.xy(44.0, 14.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kZVote)
.text(va("%c%d", (rank.scoreRings > 0 ? '+' : ' '), rank.scoreRings));
drawer_totals_right
.xy(16.0, 49.0)
.patch("CAPS_ZB");
drawer_totals_right
.xy(50.0, 24.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kThinTimer)
.text(va("%d / %d", rank.prisons, rank.totalPrisons));
drawer_totals_right
.xy(50.0, 38.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kZVote)
.text(va("%c%d", (rank.scorePrisons > 0 ? '+' : ' '), rank.scorePrisons));
drawer_totals_right
.colorize(static_cast<skincolornum_t>(SKINCOLOR_MUSTARD))
.patch("K_STEXP");
// Colorize the crystal for the totals, just like we do for in race hud
fixed_t extraexpfactor = (EXP_MAX*FRACUNIT) / EXP_TARGET;
INT16 totalExpMax = FixedMul(rank.totalExp*FRACUNIT, extraexpfactor) / FRACUNIT; // im just going to calculate it from target lol
INT16 totalExpMin = rank.numPlayers*EXP_MIN;
skincolornum_t overlaycolor = SKINCOLOR_MUSTARD;
fixed_t stablerateinverse = FRACUNIT - EXP_STABLERATE;
INT16 exp_range = totalExpMax-totalExpMin;
INT16 exp_offset = rank.exp-totalExpMin;
fixed_t factor = (exp_offset*FRACUNIT) / exp_range; // 0.0 to 1.0 in fixed
// amount of blue is how much factor is above EXP_STABLERATE, and amount of red is how much factor is below
// assume that EXP_STABLERATE is within 0.0 to 1.0 in fixed
if (factor <= stablerateinverse)
{
overlaycolor = SKINCOLOR_RUBY;
factor = FixedDiv(factor, stablerateinverse);
}
else
{
overlaycolor = SKINCOLOR_ULTRAMARINE;
fixed_t bluemaxoffset = EXP_STABLERATE;
factor = factor - stablerateinverse;
factor = FRACUNIT - FixedDiv(factor, bluemaxoffset);
}
auto transflag = K_GetTransFlagFromFixed(factor);
drawer_totals_right
.colorize(static_cast<skincolornum_t>(overlaycolor))
.flags(transflag)
.patch("K_STEXP");
drawer_totals_right
.xy(50.0, 0.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kThinTimer)
.text(va("%d / %d", rank.exp, rank.totalExp));
drawer_totals_right
.xy(50.0, 14.0)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kZVote)
.text(va("%c%d", (rank.scoreExp > 0 ? '+' : ' '), rank.scoreExp));
}
if ((state == PODIUM_ST_GRADE_APPEAR && delay == 0)
|| state >= PODIUM_ST_GRADE_VOICE)
{
char grade_letter = K_GetGradeChar( static_cast<gp_rank_e>(grade) );
patch_t *grade_img = static_cast<patch_t*>( W_CachePatchName(va("R_FINRN%c", grade_letter), PU_CACHE) );
srb2::Draw grade_drawer = drawer
.xy(BASEVIDWIDTH * 0.5, BASEVIDHEIGHT - 2.0 - (grade_img->height * 0.5))
.colormap( static_cast<skincolornum_t>(K_GetGradeColor( static_cast<gp_rank_e>(grade) )) );
if (rank.specialWon == true)
{
UINT8 emeraldNum = g_podiumData.emeraldnum;
const boolean emeraldBlink = (leveltime & 1);
patch_t *emeraldOverlay = nullptr;
patch_t *emeraldUnderlay = nullptr;
skincolornum_t emeraldColor = SKINCOLOR_NONE;
if (emeraldNum == 0)
{
// Prize -- todo, currently using fake Emerald
emeraldColor = SKINCOLOR_GOLD;
}
else
{
emeraldColor = static_cast<skincolornum_t>( SKINCOLOR_CHAOSEMERALD1 + ((emeraldNum - 1) % 7) );
}
{
if (emeraldNum > 7)
{
emeraldOverlay = static_cast<patch_t*>( W_CachePatchName("SEMRA0", PU_CACHE) );
emeraldUnderlay = static_cast<patch_t*>( W_CachePatchName("SEMRB0", PU_CACHE) );
}
else
{
emeraldOverlay = static_cast<patch_t*>( W_CachePatchName("EMRCA0", PU_CACHE) );
emeraldUnderlay = static_cast<patch_t*>( W_CachePatchName("EMRCB0", PU_CACHE) );
}
}
srb2::Draw emerald_drawer = grade_drawer
.xy(-48, 20)
.colormap(emeraldColor)
.scale(0.5);
if (emeraldBlink)
{
emerald_drawer
.flags(V_ADD)
.patch(emeraldOverlay);
if (emeraldUnderlay != nullptr)
{
emerald_drawer
.patch(emeraldUnderlay);
}
}
else
{
emerald_drawer
.patch(emeraldOverlay);
}
}
grade_drawer
.xy(48, -2)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kMenu)
.text("TOTAL");
grade_drawer
.xy(48, 8)
.align(srb2::Draw::Align::kCenter)
.font(srb2::Draw::Font::kThinTimer)
.text(va("%d", rank.scoreTotal));
float sc = 1.0;
if (state == PODIUM_ST_GRADE_APPEAR)
{
sc += transition_i * 8.0;
}
grade_drawer
.xy(-grade_img->width * 0.5 * sc, -grade_img->height * 0.5 * sc)
.scale(sc)
.patch(grade_img);
}
if (state >= PODIUM_ST_DATA_SLIDEIN)
{
K_DrawKartPositionNumXY(
rank.position, 1,
(drawer_winner.x() + 36) * FRACUNIT, (drawer_winner.y() + 2) * FRACUNIT,
FRACUNIT, drawer_winner.flags(),
leveltime,
((mapheaderinfo[gamemap - 1]->levelflags & LF_SUBTRACTNUM) == LF_SUBTRACTNUM),
true,
true,
(rank.position > 3)
);
if (state == PODIUM_ST_DONE)
{
Y_DrawIntermissionButton(delay, 0, true);
}
else if (state == PODIUM_ST_EXIT)
{
Y_DrawIntermissionButton(-1, (2*TICRATE) - delay, true);
}
}
}
/*--------------------------------------------------
boolean K_PodiumSequence(void)
See header file for description.
--------------------------------------------------*/
boolean K_PodiumSequence(void)
{
return (gamestate == GS_CEREMONY);
}
/*--------------------------------------------------
boolean K_PodiumRanking(void)
See header file for description.
--------------------------------------------------*/
boolean K_PodiumRanking(void)
{
return (gamestate == GS_CEREMONY && g_podiumData.ranking == true);
}
/*--------------------------------------------------
boolean K_PodiumGrade(void)
See header file for description.
--------------------------------------------------*/
gp_rank_e K_PodiumGrade(void)
{
if (K_PodiumSequence() == false)
{
return GRADE_E;
}
return g_podiumData.grade;
}
/*--------------------------------------------------
boolean K_PodiumHasEmerald(void)
See header file for description.
--------------------------------------------------*/
boolean K_PodiumHasEmerald(void)
{
if (K_PodiumSequence() == false)
{
return false;
}
return g_podiumData.rank.specialWon;
}
/*--------------------------------------------------
UINT8 K_GetPodiumPosition(player_t *player)
See header file for description.
--------------------------------------------------*/
UINT8 K_GetPodiumPosition(player_t *player)
{
UINT8 position = 1;
INT32 i;
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *other = NULL;
if (playeringame[i] == false)
{
continue;
}
other = &players[i];
if (other->bot == false && other->spectator == true)
{
continue;
}
if (other->score > player->score)
{
// Final score is the important part.
position++;
}
else if (other->score == player->score)
{
if (other->bot == false && player->bot == true)
{
// Bots are never as important as players.
position++;
}
else if (i < player - players)
{
// Port priority is the final tie breaker.
position++;
}
}
}
return position;
}
/*--------------------------------------------------
static void K_SetPodiumWaypoint(player_t *const player, waypoint_t *const waypoint)
Changes the player's current and next waypoints, for
use during the podium sequence.
Input Arguments:-
player - The player to update the waypoints of.
waypoint - The new current waypoint.
Return:-
None
--------------------------------------------------*/
static void K_SetPodiumWaypoint(player_t *const player, waypoint_t *const waypoint)
{
// Set the new waypoint.
player->currentwaypoint = waypoint;
if ((waypoint == NULL)
|| (waypoint->nextwaypoints == NULL)
|| (waypoint->numnextwaypoints == 0U))
{
// No waypoint, or no next waypoint.
player->nextwaypoint = NULL;
return;
}
// Simply use the first available next waypoint.
// No need for split paths in these cutscenes.
player->nextwaypoint = waypoint->nextwaypoints[0];
}
/*--------------------------------------------------
void K_InitializePodiumWaypoint(player_t *const player)
See header file for description.
--------------------------------------------------*/
void K_InitializePodiumWaypoint(player_t *const player)
{
if ((player != NULL) && (player->mo != NULL))
{
if (player->position == 0)
{
// Just in case a netgame scenario with a late joiner ocurrs.
player->position = K_GetPodiumPosition(player);
}
if (player->position > 0 && player->position <= MAXPLAYERS)
{
// Initialize our first waypoint to the one that
// matches our position.
K_SetPodiumWaypoint(player, K_GetWaypointFromID(player->position));
}
else
{
// None does, so remove it if we happen to have one.
K_SetPodiumWaypoint(player, NULL);
}
}
}
/*--------------------------------------------------
void K_UpdatePodiumWaypoints(player_t *const player)
See header file for description.
--------------------------------------------------*/
void K_UpdatePodiumWaypoints(player_t *const player)
{
if ((player != NULL) && (player->mo != NULL))
{
if (player->currentwaypoint != NULL)
{
const fixed_t xydist = P_AproxDistance(
player->mo->x - player->currentwaypoint->mobj->x,
player->mo->y - player->currentwaypoint->mobj->y
);
const fixed_t xyzdist = P_AproxDistance(
xydist,
player->mo->z - player->currentwaypoint->mobj->z
);
//const fixed_t speed = P_AproxDistance(player->mo->momx, player->mo->momy);
if (xyzdist <= player->mo->radius + player->currentwaypoint->mobj->radius)
{
// Reached waypoint, go to the next waypoint.
K_SetPodiumWaypoint(player, player->nextwaypoint);
}
}
}
}
/*--------------------------------------------------
boolean K_StartCeremony(void)
See header file for description.
--------------------------------------------------*/
boolean K_StartCeremony(void)
{
if (grandprixinfo.gp == false)
{
return false;
}
INT32 i;
INT32 podiumMapNum = NEXTMAP_INVALID;
if (grandprixinfo.cup != NULL
&& grandprixinfo.cup->cachedlevels[CUPCACHE_PODIUM] != NEXTMAP_INVALID)
{
podiumMapNum = grandprixinfo.cup->cachedlevels[CUPCACHE_PODIUM];
}
else if (podiummap)
{
podiumMapNum = G_MapNumber(podiummap);
}
if (podiumMapNum < nummapheaders
&& mapheaderinfo[podiumMapNum]
&& mapheaderinfo[podiumMapNum]->lumpnum != LUMPERROR)
{
gamemap = podiumMapNum+1;
g_reloadingMap = false;
encoremode = grandprixinfo.encore;
if (savedata.lives > 0)
{
K_LoadGrandPrixSaveGame();
savedata.lives = 0;
}
// Make sure all of the GAME OVER'd players can spawn
// and be present for the podium
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i])
{
if (players[i].lives < 1)
players[i].lives = 1;
if (players[i].bot)
players[i].spectator = false;
}
}
G_SetGametype(GT_RACE);
G_DoLoadLevelEx(false, GS_CEREMONY);
wipegamestate = GS_CEREMONY; // I don't know what else to do here
r_splitscreen = 0; // Only one screen for the ceremony
R_ExecuteSetViewSize();
return true;
}
return false;
}
/*--------------------------------------------------
void K_FinishCeremony(void)
See header file for description.
--------------------------------------------------*/
void K_FinishCeremony(void)
{
if (K_PodiumSequence() == false)
{
return;
}
g_podiumData.ranking = true;
g_fast_forward = 0;
}
/*--------------------------------------------------
void K_ResetCeremony(void)
See header file for description.
--------------------------------------------------*/
void K_ResetCeremony(void)
{
SINT8 i;
memset(&g_podiumData, 0, sizeof(struct podiumData_s));
if (K_PodiumSequence() == false)
{
return;
}
// Establish rank and grade for this play session.
g_podiumData.Init();
// Set up music for podium.
{
if (g_podiumData.rank.position == 1)
{
mapmusrng = 2;
}
else if (g_podiumData.rank.position <= RANK_NEUTRAL_POSITION)
{
mapmusrng = 1;
}
else
{
mapmusrng = 0;
}
UINT8 limit = (encoremode && mapheaderinfo[gamemap-1]->encoremusname_size)
? mapheaderinfo[gamemap-1]->encoremusname_size
: mapheaderinfo[gamemap-1]->musname_size;
if (limit < 1)
limit = 1;
while (mapmusrng >= limit)
{
mapmusrng--;
}
}
if (!grandprixinfo.cup)
{
return;
}
cupheader_t *emeraldcup = NULL;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| grandprixinfo.cup->id >= basenumkartcupheaders // custom content
|| M_SecretUnlocked(SECRET_SPECIALATTACK, false)) // true order
{
// Standard order.
emeraldcup = grandprixinfo.cup;
}
else
{
// Determine order from sealedswaps.
for (i = 0; i < GDMAX_SEALEDSWAPS; i++)
{
if (gamedata->sealedswaps[i] == NULL)
{
if (g_podiumData.rank.specialWon == true)
{
// First visit! Mark it off.
gamedata->sealedswaps[i] = grandprixinfo.cup;
}
break;
}
if (gamedata->sealedswaps[i] != grandprixinfo.cup)
continue;
// Repeat visit, grab the same ID.
break;
}
// If there's pending stars, apply them to the new cup order.
if (i < GDMAX_SEALEDSWAPS)
{
emeraldcup = kartcupheaders;
while (emeraldcup)
{
if (emeraldcup->id >= basenumkartcupheaders)
{
emeraldcup = NULL;
break;
}
if (emeraldcup->emeraldnum == i+1)
break;
emeraldcup = emeraldcup->next;
}
g_podiumData.emeraldnum = i+1;
}
}
// Write grade, position, and emerald-having-ness for later sessions!
i = (grandprixinfo.masterbots) ? KARTGP_MASTER : grandprixinfo.gamespeed;
// All results populate downwards in difficulty. This prevents someone
// who's just won on Normal from feeling obligated to complete Easy too.
for (; i >= 0; i--)
{
boolean anymerit = false;
if ((grandprixinfo.cup->windata[i].best_placement == 0) // First run
|| (g_podiumData.rank.position <= grandprixinfo.cup->windata[i].best_placement)) // Later, better run
{
grandprixinfo.cup->windata[i].best_placement = g_podiumData.rank.position;
// The following will not occur in unmodified builds, but pre-emptively sanitise gamedata if someone just changes MAXPLAYERS and calls it a day
if (grandprixinfo.cup->windata[i].best_placement > 0x0F)
{
grandprixinfo.cup->windata[i].best_placement = 0x0F;
}
anymerit = true;
}
if (g_podiumData.grade >= grandprixinfo.cup->windata[i].best_grade)
{
grandprixinfo.cup->windata[i].best_grade = g_podiumData.grade;
anymerit = true;
}
if (g_podiumData.rank.specialWon == true && emeraldcup)
{
emeraldcup->windata[i].got_emerald = true;
anymerit = true;
}
if (anymerit == true)
{
grandprixinfo.cup->windata[i].best_skin.id = g_podiumData.rank.skin;
grandprixinfo.cup->windata[i].best_skin.unloaded = NULL;
}
}
// Update visitation.
prevmap = gamemap-1;
G_UpdateVisited();
// will subsequently save in P_LoadLevel
}
/*--------------------------------------------------
void K_CeremonyTicker(boolean run)
See header file for description.
--------------------------------------------------*/
void K_CeremonyTicker(boolean run)
{
// don't trigger if doing anything besides idling
if (gameaction != ga_nothing || gamestate != GS_CEREMONY)
{
return;
}
P_TickAltView(&titlemapcam);
if (titlemapcam.mobj != NULL)
{
camera[0].x = titlemapcam.mobj->x;
camera[0].y = titlemapcam.mobj->y;
camera[0].z = titlemapcam.mobj->z;
camera[0].angle = titlemapcam.mobj->angle;
camera[0].aiming = titlemapcam.mobj->pitch;
camera[0].subsector = titlemapcam.mobj->subsector;
}
if (g_podiumData.ranking == false)
{
if (run == true)
{
if (g_podiumData.fastForward == true)
{
if (g_fast_forward == 0)
{
// Possibly an infinite loop, finalize even if we're still in the middle of the cutscene.
K_FinishCeremony();
}
}
else
{
if (menuactive == false && M_MenuConfirmPressed(0) == true)
{
if (!netgame)
{
constexpr tic_t kSkipToTime = 60 * TICRATE;
if (kSkipToTime > leveltime)
{
g_fast_forward = kSkipToTime - leveltime;
}
}
g_podiumData.fastForward = true;
}
}
}
return;
}
if (run == true)
{
g_podiumData.Tick();
}
}
/*--------------------------------------------------
void K_CeremonyDrawer(void)
See header file for description.
--------------------------------------------------*/
void K_CeremonyDrawer(void)
{
if (g_podiumData.ranking == false)
{
// not ready to draw.
return;
}
g_podiumData.Draw();
}