// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Sally "TehRealSalt" Cochenour // Copyright (C) 2024 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_tally.cpp /// \brief End of level tally screen animations #include #include "k_tally.h" #include "k_kart.h" #include "k_rank.h" #include "k_grandprix.h" #include "k_battle.h" #include "k_boss.h" #include "k_specialstage.h" #include "k_hud.h" #include "doomstat.h" #include "g_game.h" #include "p_local.h" #include "r_main.h" #include "r_skins.h" #include "v_video.h" #include "v_draw.hpp" #include "z_zone.h" #include "y_inter.h" #include "m_easing.h" #include "s_sound.h" #include "st_stuff.h" #include "r_fps.h" #include "g_party.h" #include "g_input.h" boolean level_tally_t::UseBonuses(void) { if ((gametyperules & GTR_SPECIALSTART) || (grandprixinfo.gp == true && grandprixinfo.eventmode == GPEVENT_SPECIAL)) { // Special Stage -- the only bonus for these is completing it or not. return false; } // No bonuses / ranking in FREE PLAY or Time Attack return (grandprixinfo.gp == true || K_TimeAttackRules() == false); } void level_tally_t::DetermineBonuses(void) { std::vector temp_bonuses; // Figure out a set of two bonuses to use // for this gametype's ranking, depending // on what rules are activated and how important // they are. // The end of the vector is prioritized // more than the beginning of the vector, // so this is basically ranked choice // starting from least important to // most important. if (UseBonuses() == true) { if (grandprixinfo.gp == true && (gametypes[gt]->rules & GTR_SPHERES) == 0) { // Ring is a throw-away bonus, just meant to // encourage people to earn more lives, // so only have it in GP and with lowest priority. temp_bonuses.push_back(TALLY_BONUS_RING); } if (totalPrisons == 0) // These are only for regular Battle, not Prison Break { if ((gametypes[gt]->rules & GTR_POWERSTONES) == GTR_POWERSTONES) { // Give a consolation bonus for people who // almost won by getting all Chaos Emeralds. temp_bonuses.push_back(TALLY_BONUS_POWERSTONES); } if ((gametypes[gt]->rules & GTR_POINTLIMIT) == GTR_POINTLIMIT) { // Give a consolation bonus for getting // close to the point limit. temp_bonuses.push_back(TALLY_BONUS_SCORE); } } if (totalLaps > 0) { // Give circuit gamemodes a consolation bonus // for getting good placements on each lap. temp_bonuses.push_back(TALLY_BONUS_LAP); } if (totalPrisons > 0) { // If prisons exist, then this means that it is // the entire point of the mode, so make it // the most important. temp_bonuses.push_back(TALLY_BONUS_PRISON); } } // Take the two most important, and put them in our actual list. for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (temp_bonuses.empty() == true) { bonuses[i] = TALLY_BONUS_NA; // No bonus to add... } else { bonuses[i] = temp_bonuses.back(); temp_bonuses.pop_back(); #if 0 switch (bonuses[i]) { default: displayBonus[i] = 0; break; } #else displayBonus[i] = 0; #endif } } } void level_tally_t::DetermineStatistics(void) { std::vector temp_stats; // Same thing as DetermineBonuses, but for the // for-fun stats that don't do anything. if (grandprixinfo.gp == true) { // Show the player's total rings. // This one is special and also // shows lives gained. temp_stats.push_back(TALLY_STAT_TOTALRINGS); } // Maybe there will be a situation in the // future where we DON'T want to show time? temp_stats.push_back(TALLY_STAT_TIME); // Take the two most important, and put them in our actual list. for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (temp_stats.empty() == true) { stats[i] = TALLY_STAT_NA; // No stat to add... } else { stats[i] = temp_stats.back(); temp_stats.pop_back(); switch (stats[i]) { case TALLY_STAT_TOTALRINGS: displayStat[i] = ringPool; break; default: displayStat[i] = 0; break; } } } } INT32 level_tally_t::CalculateGrade(void) { static const fixed_t gradePercents[GRADE_A] = { 7*FRACUNIT/20, // D: 35% or higher 10*FRACUNIT/20, // C: 50% or higher 14*FRACUNIT/20, // B: 70% or higher 17*FRACUNIT/20 // A: 85% or higher }; INT32 retGrade = GRADE_E; // gp_rank_e INT32 bonusWeights[TALLY_WINDOW_SIZE]; for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { switch (bonuses[i]) { case TALLY_BONUS_RING: { bonusWeights[i] = 20; break; } case TALLY_BONUS_SCORE: { bonusWeights[i] = ((pointLimit != 0) ? 100 : 0); break; } case TALLY_BONUS_LAP: case TALLY_BONUS_PRISON: case TALLY_BONUS_POWERSTONES: { bonusWeights[i] = 150; break; } default: { bonusWeights[i] = 0; break; } } } const INT32 positionWeight = (position > 0 && numPlayers > 2) ? 50 : 0; const INT32 total = positionWeight + bonusWeights[0] + bonusWeights[1]; INT32 ours = 0; fixed_t percent = 0; if (position > 0 && numPlayers > 2) { const INT32 sc = (position - 1); const INT32 loser = ((numPlayers + 1) / 2); // number of winner positions ours += ((loser - sc) * positionWeight) / loser; } for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { switch (bonuses[i]) { case TALLY_BONUS_RING: { ours += (rings * bonusWeights[i]) / 20; break; } case TALLY_BONUS_LAP: { // Use a special curve for this. // The difference between 0 and 1 lap points is an important difference in skill, // while the difference between 5 and 6 is not very notable. const fixed_t frac = (laps * FRACUNIT) / std::max(1, static_cast(totalLaps)); ours += Easing_OutSine(frac, 0, bonusWeights[i]); break; } case TALLY_BONUS_PRISON: { ours += (prisons * bonusWeights[i]) / std::max(1, static_cast(totalPrisons)); break; } case TALLY_BONUS_SCORE: { if (pointLimit != 0) { ours += (points * bonusWeights[i]) / std::max(1, static_cast(abs(pointLimit))); } break; } case TALLY_BONUS_POWERSTONES: { ours += (powerStones * bonusWeights[i]) / 7; break; } default: { break; } } } percent = FixedDiv(ours, total); for (retGrade = GRADE_E; retGrade < GRADE_A; retGrade++) { if (percent < gradePercents[retGrade]) { break; } } return retGrade; } void level_tally_t::Init(player_t *player) { if (active == true) { return; } active = true; owner = player; gt = gametype; const boolean game_over = ( G_GametypeUsesLives() ? ((player->pflags & PF_LOSTLIFE) == PF_LOSTLIFE) : (tutorialchallenge == TUTORIALSKIP_INPROGRESS && K_IsPlayerLosing(player)) ); time = std::min(static_cast(player->realtime), (100 * 60 * TICRATE) - 1); ringPool = player->totalring; livesAdded = 0; position = numPlayers = 0; rings = 0; laps = totalLaps = 0; points = pointLimit = 0; powerStones = 0; releasedFastForward = false; rank = GRADE_INVALID; if (player->spectator == false && player->bot == false && game_over == false) { if (K_TimeAttackRules() == false && K_Cooperative() == false) { position = (player->pflags & PF_NOCONTEST) ? (MAXPLAYERS+1) : player->position; for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == true && players[i].spectator == false) { numPlayers++; } } } if ((gametypes[gt]->rules & GTR_SPHERES) == 0) { if (player->hudrings > 0) // Don't count negative rings { rings = player->hudrings; } } if ((gametypes[gt]->rules & GTR_CIRCUIT) == GTR_CIRCUIT) { laps = player->lapPoints; totalLaps = numlaps; if (inDuel == false) { totalLaps *= 2; } } if (battleprisons) { prisons = numtargets; totalPrisons = maptargets; } if ((gametypes[gt]->rules & GTR_POINTLIMIT) == GTR_POINTLIMIT) { points = player->roundscore; pointLimit = g_pointlimit; if (pointLimit == 0) { // Get max from players in server. // This is set as a negative number // to communicate to not show it as x/y // when drawing it on the HUD. for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] == true && players[i].spectator == false) { pointLimit = std::min(pointLimit, static_cast(-players[i].roundscore)); } } } } if ((gametypes[gt]->rules & GTR_POWERSTONES) == GTR_POWERSTONES) { powerStones = K_NumEmeralds(player); } DetermineStatistics(); DetermineBonuses(); rank = CalculateGrade(); } header[0] = '\0'; gotThru = showRoundNum = false; if (player->spectator == false) { if (game_over == true) { if (tutorialchallenge == TUTORIALSKIP_INPROGRESS) { snprintf( header, sizeof header, "NICE TRY" ); } else if (G_GametypeUsesLives() && player->lives <= 0) { snprintf( header, sizeof header, "GAME OVER" ); } else { snprintf( header, sizeof header, "TRY AGAIN" ); } } else if ((player->pflags & PF_NOCONTEST) == 0) { gotThru = true; if (player->skin < numskins) { snprintf( header, sizeof header, "%s", R_CanShowSkinInDemo(player->skin) ? skins[player->skin].realname : "???" ); } showRoundNum = true; } else { snprintf( header, sizeof header, "NO CONTEST..." ); } } else { if (roundqueue.position > 0 && roundqueue.position <= roundqueue.size && (grandprixinfo.gp == false || grandprixinfo.eventmode == GPEVENT_NONE)) { snprintf( header, sizeof header, "ROUND" ); showRoundNum = true; } else if (K_CheckBossIntro() == true && bossinfo.enemyname) { snprintf( header, sizeof header, "%s", bossinfo.enemyname ); } else if (battleprisons == true) { snprintf( header, sizeof header, "PRISON BREAK" ); } else { snprintf( header, sizeof header, "%s STAGE", gametypes[gametype]->name ); } } header[sizeof header - 1] = '\0'; // Only show grade if there were any bonuses if (rank != GRADE_INVALID) { showGrade = (position > 0); if (showGrade == false) { for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (bonuses[i] != TALLY_BONUS_NA) { showGrade = true; break; } } } } if (showGrade == false) { rank = GRADE_INVALID; } lineCount = 0; for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (stats[i] != TALLY_STAT_NA) { lineCount++; } if (bonuses[i] != TALLY_BONUS_NA) { lineCount++; } } gradeVoice = sfx_None; if (showGrade == true) { // It'd be neat to add all of the grade sounds, // but not this close to release UINT8 skinid = player->skin; if (skinid >= numskins || R_CanShowSkinInDemo(skinid) == false) ; else if (rank < GRADE_C) { gradeVoice = skins[skinid].soundsid[S_sfx[sfx_klose].skinsound]; } else { gradeVoice = skins[skinid].soundsid[S_sfx[sfx_kwin].skinsound]; } } delay = K_TallyDelay(); // sync up with musiccountdown if (game_over == true) { // set up game over instead state = TALLY_ST_GAMEOVER_SLIDEIN; showGrade = false; } else { state = TALLY_ST_GOTTHRU_SLIDEIN; } hudSlide = 0; transition = 0; transitionTime = TICRATE/2; done = (player->spectator == true || player->bot == true); if (specialstageinfo.valid == true && (player->pflags & PF_NOCONTEST) == PF_NOCONTEST && // TODO: this leveltime check works, but checking // leveltime is kind of fragile in case order of // operations ever changes. There should be a better // way to tell if the player spawned GAME OVERed. (G_GametypeUsesLives() && player->lives <= 0 && leveltime == 0) == false) { // No tally when losing special stages // Except when entering from GAME OVER. state = TALLY_ST_IGNORE; delay = 0; } if (UINT8 pnum = player - players; G_IsPartyLocal(pnum)) { UINT8 view = G_PartyPosition(pnum); // Battle: if this player's viewpoint has changed // since being eliminated, set it back so they see // their own Tally and not someone else's. if (displayplayers[view] != pnum) { displayplayers[view] = pnum; G_FixCamera(1 + view); } } } void level_tally_t::NewLine(void) { state = TALLY_ST_TEXT_APPEAR; lines++; //S_StartSound(NULL, sfx_mbs5b); //transition = 0; //transitionTime = TICRATE/5; delay = TICRATE/5; } boolean level_tally_t::IncrementLine(void) { UINT8 count = lines; INT32 *value = nullptr; INT32 dest = 0; INT32 amount = 1; INT32 freq = 2; boolean lives_check = false; for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (count == 0) { break; } if (stats[i] == TALLY_STAT_NA) { break; } value = &displayStat[i]; lives_check = ( stats[i] == TALLY_STAT_TOTALRINGS // Rings also shows the Lives. && livesAdded < owner->xtralife // Don't check if we've maxxed out! ); switch (stats[i]) { case TALLY_STAT_TIME: dest = time; amount = 111; freq = 0; break; case TALLY_STAT_TOTALRINGS: dest = ringPool + rings; amount = 1; freq = 1; break; default: dest = 0; amount = 1; freq = 2; break; } count--; } for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (count == 0) { break; } if (bonuses[i] == TALLY_BONUS_NA) { break; } value = &displayBonus[i]; lives_check = false; switch (bonuses[i]) { case TALLY_BONUS_RING: dest = rings; amount = 1; freq = 1; break; case TALLY_BONUS_LAP: dest = laps; amount = 1; freq = 4; break; case TALLY_BONUS_PRISON: dest = prisons; amount = 1; freq = 4; break; case TALLY_BONUS_SCORE: dest = points; amount = 11; freq = 2; break; case TALLY_BONUS_POWERSTONES: dest = powerStones; amount = 1; freq = 4; break; default: dest = 0; amount = 1; freq = 2; break; } count--; } if (count > 0) { // No more lines to update. return true; } const boolean playSounds = P_IsDisplayPlayer(owner); if (*value == dest) { // We've reached our destination return true; } const INT32 prevVal = *value; if (playSounds == true && tickSound == 0) { S_StopSoundByNum(sfx_mbs5f); S_StartSound(NULL, sfx_mbs5f); tickSound = 3; } if (*value > dest) { *value -= amount; if (*value < dest) { *value = dest; } } else { *value += amount; if (*value > dest) { *value = dest; } } if (lives_check == true) { const UINT8 lifethreshold = 20; const UINT8 oldExtra = prevVal / lifethreshold; const UINT8 extra = *value / lifethreshold; // Handle extra life sound & blinking if (extra > oldExtra) { livesAdded++; xtraBlink = TICRATE; if (playSounds == true) { S_StopSoundByNum(sfx_cdfm73); S_StartSound(NULL, sfx_cdfm73); } } } delay = freq; return false; } void level_tally_t::Tick(void) { if (hudSlide < TICRATE/4) { hudSlide++; } if (tickSound > 0) { tickSound--; } if (xtraBlink > 0) { xtraBlink--; } if (delay > 0) { delay--; return; } if (transition < FRACUNIT) { if (transitionTime <= 0) { transition = FRACUNIT; return; } transition += FRACUNIT / transitionTime; if (transition > FRACUNIT) { transition = FRACUNIT; } return; } const boolean playSounds = P_IsDisplayPlayer(owner); switch (state) { case TALLY_ST_GOTTHRU_SLIDEIN: { state = TALLY_ST_GOTTHRU_SLIDEUP; transition = 0; transitionTime = TICRATE/2; delay = TICRATE/2; break; } case TALLY_ST_GOTTHRU_SLIDEUP: { state = TALLY_ST_BOXES_SLIDEIN; transition = 0; transitionTime = TICRATE/2; delay = TICRATE/5; break; } case TALLY_ST_BOXES_SLIDEIN: { NewLine(); break; } case TALLY_ST_TEXT_APPEAR: { if (IncrementLine() == true) { if (grandprixinfo.gp == true // In GP && lines >= lineCount // Finished the bonuses && livesAdded < owner->xtralife // Didn't max out by other causes ) { // This is only true if Rings alone aren't responsible for our added lives. // Generally for Prison Break, but could be earned in custom contexts too. livesAdded = owner->xtralife; xtraBlink = TICRATE; if (playSounds == true) { S_StopSoundByNum(sfx_cdfm73); S_StartSound(NULL, sfx_cdfm73); } } if (playSounds == true) { S_StopSoundByNum(sfx_mbs5b); S_StartSound(NULL, (lines >= lineCount) ? sfx_mbs70 : sfx_mbs5b); } state = TALLY_ST_TEXT_PAUSE; delay = TICRATE/2; } break; } case TALLY_ST_TEXT_PAUSE: { if (lines < lineCount) { NewLine(); } else { if (showGrade == true) { state = TALLY_ST_GRADE_APPEAR; transition = 0; transitionTime = TICRATE/7; delay = TICRATE/2; // for UCRP_FINISHGRADE owner->roundconditions.checkthisframe = true; } else { state = TALLY_ST_DONE; delay = (modeattacking) ? 2*TICRATE: 5*TICRATE; } } break; } case TALLY_ST_GRADE_APPEAR: { if (playSounds) { S_StartSound(NULL, sfx_rank); } state = TALLY_ST_GRADE_VOICE; delay = TICRATE/2; break; } case TALLY_ST_GRADE_VOICE: { if (playSounds && cv_kartvoices.value) { S_StartSound(NULL, gradeVoice); } state = TALLY_ST_DONE; delay = 5*TICRATE; break; } case TALLY_ST_GAMEOVER_SLIDEIN: { state = TALLY_ST_GAMEOVER_LIVES; delay = TICRATE; break; } case TALLY_ST_GAMEOVER_LIVES: { state = TALLY_ST_GAMEOVER_DONE; delay = 4*TICRATE; break; } case TALLY_ST_DONE: case TALLY_ST_GAMEOVER_DONE: case TALLY_ST_IGNORE: { done = true; break; } default: { // error occured, silently fix state = TALLY_ST_DONE; break; } } } void level_tally_t::Draw(void) { if (state == TALLY_ST_IGNORE) { return; } const float transition_f = FixedToFloat(transition); const float transition_i = 1.0 - transition_f; const float frac = (r_splitscreen ? 0.5 : 1.0); INT32 v_width = BASEVIDWIDTH; INT32 v_height = BASEVIDHEIGHT; if (r_splitscreen > 0) { v_height /= 2; } if (r_splitscreen > 1) { v_width /= 2; } SINT8 h_transition_sign = 1; if (r_splitscreen > 1) { if (!(R_GetViewNumber() & 1)) { h_transition_sign = -h_transition_sign; } } else if (r_splitscreen > 0) { if (R_GetViewNumber() == 1) { h_transition_sign = -h_transition_sign; } } srb2::Draw drawer = (srb2::Draw(0, 0)) .flags(V_SPLITSCREEN) .clipx(0, v_width) .clipy(0, v_height); INT32 fade = 5; if (state == TALLY_ST_GOTTHRU_SLIDEIN || state == TALLY_ST_GAMEOVER_SLIDEIN) { fade = (5 * transition_f); } V_DrawFadeFill( (vid.width / 2) * (r_splitscreen > 1 && R_GetViewNumber() & 1), (vid.height / 2) * (R_GetViewNumber() > (r_splitscreen > 1)), vid.width / (r_splitscreen > 1 ? 2 : 1), vid.height / (r_splitscreen ? 2 : 1), V_NOSCALESTART, 31, fade ); const INT32 header_width = (r_splitscreen ? (BASEVIDWIDTH * 0.5) : BASEVIDWIDTH); const INT32 header_x = (v_width - header_width) * 0.5; const INT32 header_height = 36 * frac; const INT32 header_centered = (v_height * 0.5) - header_height; switch (state) { case TALLY_ST_GAMEOVER_SLIDEIN: case TALLY_ST_GAMEOVER_LIVES: case TALLY_ST_GAMEOVER_DONE: case TALLY_ST_GOTTHRU_SLIDEIN: Y_DrawIntermissionHeader( (header_x * FRACUNIT) + (v_width * transition_i * FRACUNIT * h_transition_sign), header_centered * FRACUNIT, gotThru, header, showRoundNum, (r_splitscreen > 0) ); break; case TALLY_ST_GOTTHRU_SLIDEUP: Y_DrawIntermissionHeader( header_x * FRACUNIT, header_centered * transition_i * FRACUNIT, gotThru, header, showRoundNum, (r_splitscreen > 0) ); break; default: Y_DrawIntermissionHeader( header_x * FRACUNIT, 0, gotThru, header, showRoundNum, (r_splitscreen > 0) ); break; } if (state == TALLY_ST_GAMEOVER_SLIDEIN || state == TALLY_ST_GAMEOVER_LIVES || state == TALLY_ST_GAMEOVER_DONE) { if (G_GametypeUsesLives() && owner->lives > 0) { srb2::Draw lives_drawer = drawer .xy( (v_width * 0.5) + (v_width * transition_i * h_transition_sign) - 8.0, header_centered + (header_height * 1.5) + 4.0 ); const skincolornum_t color = static_cast(owner->skincolor); lives_drawer .colormap(owner->skin, color) .patch(faceprefix[owner->skin][FACE_RANK]); UINT8 lives_num = owner->lives; if (state == TALLY_ST_GAMEOVER_SLIDEIN) { lives_num++; } else if (state == TALLY_ST_GAMEOVER_LIVES) { lives_num++; if (((delay / 5) & 1) == 0) { lives_num = 0; } } if (lives_num > 0) { lives_drawer .xy(19.0, 1.0) .font(srb2::Draw::Font::kThinTimer) .text("{}", lives_num); } } return; } if (state != TALLY_ST_GOTTHRU_SLIDEIN && state != TALLY_ST_GOTTHRU_SLIDEUP) { UINT8 numBoxes = 0; boolean drawStats = false; if (stats[0] != TALLY_STAT_NA) { numBoxes++; drawStats = true; } if (bonuses[0] != TALLY_BONUS_NA) { numBoxes++; } patch_t *box_fg = static_cast( W_CachePatchName(va("RNKBLK%sA", (r_splitscreen ? "4" : "1")), PU_CACHE) ); patch_t *box_bg = static_cast( W_CachePatchName(va("RNKBLK%sB", (r_splitscreen ? "4" : "1")), PU_CACHE) ); patch_t *sticker = static_cast( W_CachePatchName((r_splitscreen ? "K_SPDMBG" : "K_STTIME"), PU_CACHE) ); const float sticker_offset = (r_splitscreen ? 0.0 : 3.0); patch_t *egg_sticker = static_cast( W_CachePatchName("EGGSTKR", PU_CACHE) ); srb2::Draw drawer_box = drawer.xy( (v_width * 0.5) - (box_bg->width * 0.5), ((v_height * 0.5) + (8.0 * frac)) - ((box_bg->height * 0.5) * numBoxes) ); UINT8 displayLines = lines; for (int b = 0; b < numBoxes; b++) { srb2::Draw drawer_box_offset = drawer_box; switch (state) { case TALLY_ST_BOXES_SLIDEIN: drawer_box_offset = drawer_box_offset.x( ((b & 1) ? 1 : -1) * transition_i * v_width * h_transition_sign ); break; default: break; } drawer_box_offset .colormap(SKINCOLOR_BLUE) .flags(V_ADD) .patch(box_bg); drawer_box_offset .patch(box_fg); srb2::Draw drawer_text = drawer_box_offset .xy(11.0 * frac, 6.0 * frac) .font(r_splitscreen ? srb2::Draw::Font::kPing : srb2::Draw::Font::kTimer); UINT8 boxLines = 0; if (drawStats == true) { for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (stats[i] != TALLY_STAT_NA) { boxLines++; } } } else { for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (bonuses[i] != TALLY_BONUS_NA) { boxLines++; } } } drawer_text = drawer_text.y(17.0 * frac * (TALLY_WINDOW_SIZE - boxLines) * 0.5); for (int i = 0; i < TALLY_WINDOW_SIZE; i++) { if (displayLines == 0) { break; } const char *bonus_code = "XX"; if (drawStats == true) { if (stats[i] == TALLY_STAT_NA) { continue; } switch (stats[i]) { case TALLY_STAT_TIME: bonus_code = "TM"; break; case TALLY_STAT_TOTALRINGS: bonus_code = "TR"; break; default: break; } } else { if (bonuses[i] == TALLY_BONUS_NA) { continue; } switch (bonuses[i]) { case TALLY_BONUS_RING: bonus_code = "RB"; break; case TALLY_BONUS_LAP: bonus_code = "LA"; break; case TALLY_BONUS_PRISON: bonus_code = "PR"; break; case TALLY_BONUS_SCORE: bonus_code = "ST"; break; case TALLY_BONUS_POWERSTONES: bonus_code = "EM"; break; default: break; } } if (r_splitscreen == 0) { drawer_text .xy(100.0 * frac, -2.0 * frac) .patch(egg_sticker); } drawer_text .y(-1.0 * frac) .patch(va("BNS%sP_%s", (r_splitscreen ? "4" : "1"), bonus_code)); drawer_text .xy((197.0 * frac) - (sticker->width * 0.5), -sticker_offset) .patch(sticker); if (drawStats == true) { switch (stats[i]) { case TALLY_STAT_TIME: { INT32 work_minutes = displayStat[i] / (60 * TICRATE); INT32 work_seconds = displayStat[i] / TICRATE % 60; INT32 work_tics = G_TicsToCentiseconds(displayStat[i]); drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va( "%d%d'%d%d\"%d%d", work_minutes / 10, work_minutes % 10, work_seconds / 10, work_seconds % 10, work_tics / 10, work_tics % 10 )); if (modeattacking && !demo.playback && (state == TALLY_ST_DONE || state == TALLY_ST_TEXT_PAUSE) && !K_IsPlayerLosing(&players[consoleplayer]) && players[consoleplayer].realtime < oldbest) { drawer_text .x(197.0 * frac) .y(13.0 * frac) .align(srb2::Draw::Align::kCenter) .font(srb2::Draw::Font::kMenu) .text((leveltime/2 % 2) ? "NEW RECORD!" : "\x82NEW RECORD!"); } break; } case TALLY_STAT_TOTALRINGS: { drawer_text .x(184.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d", displayStat[i])); srb2::Draw lives_drawer = drawer_text .xy(221.0 * frac, -1.0 * frac); const skincolornum_t color = static_cast(owner->skincolor); lives_drawer .x(r_splitscreen ? -7.0 : -2.0) .colormap(owner->skin, color) .patch(faceprefix[owner->skin][r_splitscreen ? FACE_MINIMAP : FACE_RANK]); UINT8 lives_num = std::min(owner->lives + livesAdded, 10); if (xtraBlink > 0 && (xtraBlink & 1) == 0 && livesAdded > 0) { lives_num = 0; } if (lives_num > 0) { if (r_splitscreen) { lives_drawer = lives_drawer .xy(6.0, 2.0) .align(srb2::Draw::Align::kLeft); } else { lives_drawer = lives_drawer .xy(17.0, 1.0) .font(srb2::Draw::Font::kThinTimer); } lives_drawer.text("{}", lives_num); } break; } default: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d", displayStat[i])); break; } } } else { switch (bonuses[i]) { case TALLY_BONUS_RING: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d / 20", displayBonus[i])); break; } case TALLY_BONUS_LAP: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d / %d", displayBonus[i], totalLaps)); break; } case TALLY_BONUS_PRISON: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d / %d", displayBonus[i], totalPrisons)); break; } case TALLY_BONUS_SCORE: { if (pointLimit > 0) { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d / %d", displayBonus[i], pointLimit)); } else { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d", displayBonus[i])); } break; } case TALLY_BONUS_POWERSTONES: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d / 7", displayBonus[i])); break; } default: { drawer_text .x(197.0 * frac) .align(srb2::Draw::Align::kCenter) .text(va("%d", displayBonus[i])); break; } } } drawer_text = drawer_text.y(17.0 * frac); displayLines--; } drawStats = false; drawer_box = drawer_box.y(box_bg->height); } } if (showGrade == false) { return; } if ((state == TALLY_ST_GRADE_APPEAR && delay == 0) || state == TALLY_ST_GRADE_VOICE || state == TALLY_ST_DONE) { char grade_letter = K_GetGradeChar( static_cast(rank) ); patch_t *grade_img = static_cast( W_CachePatchName(va("R_FINR%c%c", (r_splitscreen ? 'S' : 'N'), grade_letter), PU_CACHE) ); srb2::Draw grade_drawer = drawer .xy(v_width * 0.5, v_height - (2.0 * frac) - (grade_img->height * 0.5)) .colormap( static_cast(K_GetGradeColor( static_cast(rank) )) ); float sc = 1.0; if (state == TALLY_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); } } void K_InitPlayerTally(player_t *player) { player->tally.Init(player); } void K_TickPlayerTally(player_t *player) { boolean fastForwardInput = !demo.playback && P_IsMachineLocalPlayer(player) && G_PlayerInputDown(G_LocalSplitscreenPartyPosition(player - players), gc_a, 0); boolean allowFastForward = player->tally.state > TALLY_ST_GOTTHRU_SLIDEIN && player->tally.state <= TALLY_ST_DONE && player->tally.releasedFastForward // - Not allowed online so we don't have to do any // networking. // - Not allowed in replays because splitscreen party // doesn't exist and it's just simpler to not think // about. && (!netgame && !demo.playback) && player->tally.state != TALLY_ST_DONE; if ((fastForwardInput && allowFastForward) || // Skip tally in atract demos (demo.playback && demo.attract)) { do player->tally.Tick(); while (player->tally.state != TALLY_ST_DONE && player->tally.state != TALLY_ST_GAMEOVER_DONE); player->tally.delay = std::min(player->tally.delay, TICRATE); if (Y_ShouldDoIntermission()) musiccountdown = 2; // gets decremented to 1 in G_Ticker to immediately trigger intermission music [blows raspberry] } else { player->tally.Tick(); } if (!fastForwardInput) { player->tally.releasedFastForward = true; } else { player->tally.releasedFastForward = false; } } void K_DrawPlayerTally(void) { stplyr->tally.Draw(); } boolean K_PlayerTallyActive(player_t *player) { return player->tally.active; //(player->exiting || (player->pflags & PF_NOCONTEST)); } tic_t K_TallyDelay(void) { if (modeattacking) return TICRATE; return ((gametyperules & GTR_BUMPERS) ? 4 : 3) * TICRATE; }