// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) by Sally "TehRealSalt" Cochenour // Copyright (C) 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_credits.cpp /// \brief Credits sequence #include "k_credits.h" #include #include #include #include #include #include "doomdef.h" #include "doomstat.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_joy.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_setup.h" #include "st_stuff.h" // hud hiding #include "fastcmp.h" #include "r_fps.h" #include "lua_hud.h" #include "lua_hook.h" // SRB2Kart #include "k_menu.h" #include "k_grandprix.h" #include "music.h" #include "r_main.h" #include "m_easing.h" using nlohmann::json; enum credits_slide_types_e { CRED_TYPE_SCROLL, CRED_TYPE_SLIDE, CRED_TYPE_TITLEDROP, CRED_TYPE_TYLER52, CRED_TYPE_KARTKREW, CRED_TYPE_SONGS, CRED_TYPE__MAX }; struct credits_slide_s { credits_slide_types_e type; std::string label; std::vector strings; size_t strings_height; boolean play_demo_afterwards; }; static std::vector g_credits_slides; struct credits_star_s { fixed_t x, y; fixed_t vel_x, vel_y; INT32 frame; }; static struct credits_s { size_t current_slide; std::vector demo_maps; boolean skip; std::vector stars; fixed_t transition; fixed_t transition_prev; boolean transition_reverse; tic_t animation_timer; std::vector> split_slide_strings; size_t split_slide_id; tic_t split_slide_delay; UINT64 scroll_timer; UINT64 scroll_timer_prev; int tyler_fade; tic_t finish_counter; tic_t input_delay; tic_t demo_exit; boolean havent_ticked; } g_credits; constexpr const fixed_t kScrollFactor = FRACUNIT * 7 / 8; constexpr const int kSkipSpeed = 8; constexpr const int kScrollSkipSpeed = 4; void F_LoadCreditsDefinitions(void) { // Load credits definitions from bios.pk3 if (g_credits_slides.empty() == false) { // TODO: Allow custom credits definition files. // Either append the new data to the start or the end. Not sure which would be better. return; } lumpnum_t credits_lump_id = W_GetNumForLongName("credits_def"); size_t credits_lump_len = W_LumpLength(credits_lump_id); const char *credits_lump = static_cast( W_CacheLumpNum(credits_lump_id, PU_CACHE) ); json credits_array = json::parse(credits_lump, credits_lump + credits_lump_len); if (credits_array.is_array() == false) { I_Error("credits_def parse error: Not a JSON array"); return; } if (credits_array.size() == 0) { return; } try { for (json& slide_obj : credits_array) { struct credits_slide_s slide; std::string type_str = slide_obj.value("type", "scroll"); if (type_str == "scroll") { slide.type = CRED_TYPE_SCROLL; } else if (type_str == "slide") { slide.type = CRED_TYPE_SLIDE; } else if (type_str == "titledrop") { slide.type = CRED_TYPE_TITLEDROP; } else if (type_str == "tyler52") { slide.type = CRED_TYPE_TYLER52; } else if (type_str == "kartkrew") { slide.type = CRED_TYPE_KARTKREW; } else if (type_str == "songs") { #if 0 slide.type = CRED_TYPE_SONGS; #else // TODO continue; #endif } else { throw std::runtime_error("unexpected type name '" + type_str + "'"); } slide.label = slide_obj.value("label", ""); slide.strings_height = 0; if (slide_obj.contains("strings")) { json strings_array = slide_obj.at("strings"); if (strings_array.is_array() == true) { for (size_t i = 0; i < strings_array.size(); i++) { slide.strings.push_back( strings_array.at(i) ); if (slide.type == CRED_TYPE_SCROLL) { if (slide.strings[i].empty()) { slide.strings_height += 40; } else { if (slide.strings[i].at(0) == '*') { slide.strings_height += 30; } else { slide.strings_height += 12; } } } else if (slide.type == CRED_TYPE_SLIDE) { slide.strings_height += 30; } } } } slide.play_demo_afterwards = slide_obj.value("demo", false); g_credits_slides.push_back( slide ); } } catch (const std::exception& ex) { I_Error("credits_def parse error: %s", ex.what()); } } void F_CreditsReset(void) { g_credits.stars.clear(); g_credits.split_slide_strings.clear(); g_credits.split_slide_id = 0; g_credits.split_slide_delay = 0; g_credits.transition = g_credits.transition_prev = 0; g_credits.transition_reverse = false; g_credits.scroll_timer = g_credits.scroll_timer_prev = 0; g_credits.animation_timer = 0; g_credits.tyler_fade = 0; g_credits.finish_counter = 0; g_credits.demo_exit = 0; g_credits.havent_ticked = true; // fucking stupid bullshit } static void F_InitCreditsSlide(void) { struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; if (slide->type == CRED_TYPE_SLIDE) { // How many can be shown on one screen constexpr const size_t kMaxSlideStrings = 4; const size_t num_strings = slide->strings.size(); if (num_strings <= kMaxSlideStrings) { // Our job is easy. Simply copy what is already there. g_credits.split_slide_strings.push_back( slide->strings ); } else { // Try to divide it up relatively evenly into multiple sub-slides. const size_t num_sub_screens = (num_strings - 1) / kMaxSlideStrings + 1; size_t max_strings_per_screen = (num_strings - 1) / num_sub_screens + 1; size_t str_id = 0; std::vector screen_strings; if (max_strings_per_screen == kMaxSlideStrings && num_strings % kMaxSlideStrings == 1) { // 13 strings is unlucky and creates a page // that only has one name on the end. // This fixes it. screen_strings.emplace_back( slide->strings[str_id] ); str_id++; max_strings_per_screen--; } while (str_id < num_strings) { for (size_t i = 0; i < max_strings_per_screen; i++) { screen_strings.emplace_back( slide->strings[str_id] ); str_id++; if (str_id >= num_strings) { break; } } g_credits.split_slide_strings.push_back( screen_strings ); screen_strings.clear(); } } } #if 0 else if (slide->type == CRED_TYPE_SONGS) { // Auto fill out with music credits slide->strings.clear(); slide->strings.push_back("*MUSIC"); slide->strings_height += 30; // I do not even remotely understand the sequence code even a little bit // so SOMEONE ELSE can do it! I don't care! // --- // "I know how it work's" - toast 110124 musicdef_t *def = soundtest.sequence.next; while (def) { if (def->title #if 0 // Let's not make the credits variable-length && !S_SoundTestDefLocked(def) #endif ) { slide->strings.push_back("#" + std::string(def->title)); slide->strings_height += 12; if (def->author) { slide->strings.push_back("by " + std::string(def->author)); slide->strings_height += 12; } if (def->source) { slide->strings.push_back("from " + std::string(def->source)); slide->strings_height += 12; } if (def->composers) { slide->strings.push_back("originally by " + std::string(def->composers)); slide->strings_height += 12; } slide->strings.push_back(" "); slide->strings_height += 12; } def = def->sequence.next; } } #endif // Clear the console hud just to avoid anything getting in the way. CON_ClearHUD(); } void F_StartCredits(void) { G_SetGamestate(GS_CREDITS); // Just in case they're open ... somehow M_ClearMenus(true); if (g_credits_cutscene) { F_StartCustomCutscene(g_credits_cutscene - 1, false, false); return; } gameaction = ga_nothing; paused = false; CON_ToggleOff(); Music_StopAll(); S_StopSounds(); Music_Play("credits"); F_CreditsReset(); g_credits.demo_maps.clear(); g_credits.current_slide = 0; g_credits.input_delay = TICRATE; g_credits.skip = false; F_InitCreditsSlide(); } static void F_CreditsNextSlide(void) { F_CreditsReset(); g_credits.current_slide++; if (g_credits.current_slide >= g_credits_slides.size()) { // You watched all the credits? What a trooper! gamedata->everfinishedcredits = true; if (M_UpdateUnlockablesAndExtraEmblems(true, true)) { G_SaveGameData(); } F_StartGameEvaluation(); return; } F_InitCreditsSlide(); } void F_ContinueCredits(void) { G_SetGamestate(GS_CREDITS); F_CreditsReset(); // Returning from playing a demo. // Go to the next slide. F_CreditsNextSlide(); } static UINT16 F_PickRandomCreditsDemoMap(void) { std::vector allowedMaps; for (INT32 i = 0; i < basenummapheaders; i++) // Only take from the base game. { if (mapheaderinfo[i] == NULL || mapheaderinfo[i]->lumpnum == LUMPERROR) { // Doesn't exist? continue; } if ((mapheaderinfo[i]->typeoflevel & TOL_RACE) == 0) { // We want Race gametype demos, since they will be // the most suited to the "camera left behind" effect. continue; } if (mapheaderinfo[i]->ghostCount == 0) { // It doesn't have any demos... continue; } if ((mapheaderinfo[i]->menuflags & LF2_HIDEINMENU) == LF2_HIDEINMENU) { // Secret map. continue; } if (M_MapLocked(i + 1) == true) { // We haven't earned this one. continue; } if (std::find(g_credits.demo_maps.begin(), g_credits.demo_maps.end(), i) != g_credits.demo_maps.end()) { // Already was added. continue; } // Got past the gauntlet, so we can allow this one. allowedMaps.push_back(i); } if (allowedMaps.size() > 0) { return allowedMaps[ M_RandomKey(allowedMaps.size()) ]; } return UINT16_MAX; } static boolean F_CreditsPlayDemo(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; staffbrief_t *brief; if (slide->play_demo_afterwards == false) { return false; } if (g_credits.skip == true) { return false; } UINT16 map_id = F_PickRandomCreditsDemoMap(); if (map_id == UINT16_MAX) { return false; } g_credits.demo_maps.push_back(map_id); UINT8 ghost_id = M_RandomKey( mapheaderinfo[map_id]->ghostCount ); brief = mapheaderinfo[map_id]->ghostBrief[ghost_id]; std::string demo_name = static_cast(W_CheckNameForNumPwad(brief->wad, brief->lump)); demo.attract = DEMO_ATTRACT_CREDITS; demo.ignorefiles = true; demo.loadfiles = false; G_DoPlayDemo(demo_name.c_str()); g_fast_forward = 30 * TICRATE; g_credits.demo_exit = 0; return true; } constexpr const unsigned int kDemoExitTicCount = 10 * TICRATE; void F_TickCreditsDemoExit(void) { g_credits.demo_exit++; if (!menuactive && M_MenuConfirmPressed(0)) { g_credits.demo_exit = std::max(g_credits.demo_exit, kDemoExitTicCount - 64); } if (g_credits.demo_exit > kDemoExitTicCount) { G_CheckDemoStatus(); } } INT32 F_CreditsDemoExitFade(void) { return std::clamp( 31 - ((kDemoExitTicCount - static_cast(g_credits.demo_exit)) / 2), -1, 31 ); } static void F_CreditsSlideFinish(void) { if (F_CreditsPlayDemo() == true) { return; } if (g_credits.skip == true) { // FIXME: use shorter wipe, instead of no wipe wipegamestate = GS_CREDITS; } else { wipegamestate = GS_NULL; } F_CreditsNextSlide(); } static boolean F_TickCreditsScroll(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; UINT32 scroll_max = FixedDiv(slide->strings_height + BASEVIDHEIGHT, kScrollFactor); if (g_credits.skip == true) { g_credits.scroll_timer += kScrollSkipSpeed; } else { g_credits.scroll_timer++; } if (g_credits.scroll_timer > scroll_max) { g_credits.scroll_timer = scroll_max; } return (g_credits.scroll_timer >= scroll_max - (2 * TICRATE)); } static void F_CreditsStarParticle(fixed_t x, fixed_t y) { struct credits_star_s star; star.x = x; star.y = y; star.frame = 0; star.vel_x = M_RandomRange(-2, 2) * FRACUNIT / 4; g_credits.stars.push_back(star); } static boolean F_TickCreditsSlide(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; if (g_credits.split_slide_delay > 0) { g_credits.split_slide_delay--; return false; } if (g_credits.transition < FRACUNIT) { g_credits.transition = std::min(g_credits.transition + (FRACUNIT / TICRATE), FRACUNIT); if (g_credits.split_slide_id < g_credits.split_slide_strings.size()) { constexpr const fixed_t label_space = 30 * FRACUNIT; const fixed_t strings_height = g_credits.split_slide_strings[ g_credits.split_slide_id ].size() * 30 * FRACUNIT; fixed_t y = 0; if (slide->label.empty() == false) { y += label_space; } y += ((BASEVIDHEIGHT * FRACUNIT) - y) / 2; y -= strings_height / 2; UINT8 side = 0; UINT8 star_index = 0; for (auto& str : g_credits.split_slide_strings[ g_credits.split_slide_id ]) { const fixed_t str_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSLOW_FONT, str.c_str() ); fixed_t x = 32 * FRACUNIT; fixed_t slide_out = -BASEVIDWIDTH * FRACUNIT; if (side == 1) { x = (BASEVIDWIDTH * FRACUNIT) - x - str_width; slide_out = -slide_out; } else { x += str_width; } fixed_t ease = 0; if (g_credits.transition_reverse) { ease = Easing_InOutSine(g_credits.transition, 0, -slide_out); } else { ease = Easing_InOutSine(g_credits.transition, slide_out, 0); } if ((g_credits.animation_timer + star_index) % 2 == 0) { F_CreditsStarParticle( x + ease, y + (16 * FRACUNIT) ); } y += 30 * FRACUNIT; side = (side + 1) & 1; star_index++; } } return false; } if (g_credits.split_slide_id < g_credits.split_slide_strings.size() - 1) { if (g_credits.transition_reverse) { g_credits.split_slide_id++; } else { g_credits.split_slide_delay = 2*TICRATE; } g_credits.transition = g_credits.transition_prev = 0; g_credits.transition_reverse = !g_credits.transition_reverse; return false; } return true; } static boolean F_TickCreditsTyler52(void) { if (g_credits.animation_timer > TICRATE && g_credits.animation_timer < (2*TICRATE) - 17) { g_credits.tyler_fade++; } else { g_credits.tyler_fade--; } g_credits.tyler_fade = std::clamp(g_credits.tyler_fade, 0, 8); return true; } static void F_TickCreditsStars(void) { for (auto& star : g_credits.stars) { star.vel_y += FRACUNIT / 4; if (g_credits.animation_timer % 2 == 0) { star.frame++; } } g_credits.stars.erase( std::remove_if( g_credits.stars.begin(), g_credits.stars.end(), [](struct credits_star_s const &star) { return star.frame > 11; } ), g_credits.stars.end() ); } static void F_HandleCreditsTick(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; g_credits.animation_timer++; F_TickCreditsStars(); boolean finalize_slide = true; switch (slide->type) { case CRED_TYPE_SCROLL: { finalize_slide = F_TickCreditsScroll(); break; } case CRED_TYPE_SLIDE: { finalize_slide = F_TickCreditsSlide(); break; } case CRED_TYPE_TYLER52: { finalize_slide = F_TickCreditsTyler52(); break; } case CRED_TYPE_SONGS: { // TODO break; } default: { break; } } if (g_credits.finish_counter > 0) { g_credits.finish_counter--; if (g_credits.finish_counter == 0) { F_CreditsSlideFinish(); } return; } else if (finalize_slide) { if (g_credits.current_slide >= g_credits_slides.size() - 1) { g_credits.finish_counter = 5 * TICRATE; } else { g_credits.finish_counter = 2 * TICRATE; } } } void F_CreditTicker(void) { g_credits.havent_ticked = false; g_credits.transition_prev = g_credits.transition; g_credits.scroll_timer_prev = g_credits.scroll_timer; if (g_credits.input_delay > 0) { g_credits.input_delay--; } else { g_credits.skip = (!menuactive && M_MenuConfirmHeld(0)); } if (g_credits.current_slide >= g_credits_slides.size() - 1) { // Don't skip the last slide g_credits.skip = false; } if (g_credits.skip == true) { for (size_t i = 0; i < kSkipSpeed; i++) { F_HandleCreditsTick(); } } else { F_HandleCreditsTick(); } } static void F_DrawCreditsScroll(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; fixed_t y = (BASEVIDHEIGHT * FRACUNIT); y -= Easing_Linear( rendertimefrac, g_credits.scroll_timer_prev * kScrollFactor, g_credits.scroll_timer * kScrollFactor ); for (auto& str : slide->strings) { if (str.empty()) { y += 40 * FRACUNIT; } else { std::string new_str = str; if (new_str.at(0) == '*') { if (y > -20 * FRACUNIT) { new_str.erase(0, 1); const fixed_t str_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSLOW_FONT, new_str.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - str_width) / 2, y, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSLOW_FONT, new_str.c_str() ); } y += 30 * FRACUNIT; } else { if (y > -10 * FRACUNIT) { if (new_str.at(0) == '#') { new_str.erase(0, 1); const fixed_t str_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, MENU_FONT, new_str.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - str_width) / 2, y, FRACUNIT, FRACUNIT, FRACUNIT, V_YELLOWMAP, nullptr, MENU_FONT, new_str.c_str() ); } else { V_DrawStringScaled( 32 * FRACUNIT, y, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, MENU_FONT, new_str.c_str() ); } } y += 12 * FRACUNIT; } } if (((y / FRACUNIT) * vid.dupy) > vid.height) { break; } } if (slide->label.empty() == false) { const fixed_t label_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSHI_FONT, slide->label.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - label_width) / 2, 15 * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSHI_FONT, slide->label.c_str() ); } } static void F_DrawCreditsSlide(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; constexpr const fixed_t label_space = 30 * FRACUNIT; const fixed_t transition = Easing_Linear(rendertimefrac, g_credits.transition_prev, g_credits.transition); const fixed_t label_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSHI_FONT, slide->label.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - label_width) / 2, label_space / 2, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSHI_FONT, slide->label.c_str() ); if (g_credits.split_slide_id >= g_credits.split_slide_strings.size()) { return; } const std::vector *slide_strings = &g_credits.split_slide_strings[ g_credits.split_slide_id ]; const fixed_t strings_height = slide_strings->size() * 30 * FRACUNIT; fixed_t y = 0; if (slide->label.empty() == false) { y += label_space; } y += ((BASEVIDHEIGHT * FRACUNIT) - y) / 2; y -= strings_height / 2; UINT8 side = 0; for (auto& str : g_credits.split_slide_strings[ g_credits.split_slide_id ]) { const fixed_t str_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSLOW_FONT, str.c_str() ); fixed_t x = 32 * FRACUNIT; fixed_t slide_out = -BASEVIDWIDTH * FRACUNIT; if (side == 1) { x = (BASEVIDWIDTH * FRACUNIT) - x - str_width; slide_out = -slide_out; } fixed_t ease = 0; if (g_credits.transition_reverse) { ease = Easing_InOutSine(transition, 0, -slide_out); } else { ease = Easing_InOutSine(transition, slide_out, 0); } V_DrawStringScaled( x + ease, y, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSLOW_FONT, str.c_str() ); y += 30 * FRACUNIT; side = (side + 1) & 1; } } static void F_DrawCreditsTitleDrop(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; V_DrawFixedPatch( 0, -BASEVIDHEIGHT * (FRACUNIT / 2), FRACUNIT, 0, static_cast(W_CachePatchName( (M_UseAlternateTitleScreen() ? "KTSJUMPR1" : "KTSBUMPR1"), PU_CACHE )), nullptr ); const fixed_t label_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSHI_FONT, slide->label.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - label_width) / 2, 120 * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSHI_FONT, slide->label.c_str() ); } static void F_DrawCreditsTyler52(void) { patch_t *tyler_patch = static_cast(W_CachePatchName("TYLER52", PU_CACHE)); V_DrawStretchyFixedPatch( 0, 0, (BASEVIDWIDTH * FRACUNIT) / tyler_patch->width, (BASEVIDHEIGHT * FRACUNIT) / tyler_patch->height, V_90TRANS, tyler_patch, nullptr ); if (g_credits.tyler_fade < 8) { V_DrawFadeScreen(0xFF00, 31 - (g_credits.tyler_fade * 4)); } std::string memory_str = "In memory of"; const fixed_t memory_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSLOW_FONT, memory_str.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - memory_width) / 2, 60 * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSLOW_FONT, memory_str.c_str() ); std::string tyler_str = "Tyler52"; const fixed_t tyler_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSHI_FONT, tyler_str.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - tyler_width) / 2, 110 * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSHI_FONT, tyler_str.c_str() ); } static void F_DrawCreditsKartKrew(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; const fixed_t label_width = V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, LSLOW_FONT, slide->label.c_str() ); V_DrawStringScaled( ((BASEVIDWIDTH * FRACUNIT) - label_width) / 2, 40 * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, nullptr, LSLOW_FONT, slide->label.c_str() ); V_DrawFixedPatch( 116 * FRACUNIT, 70 * FRACUNIT, FRACUNIT / 2, 0, static_cast(W_CachePatchName( "KKLOGO_C", PU_CACHE )), nullptr ); V_DrawFixedPatch( 116 * FRACUNIT, 70 * FRACUNIT, FRACUNIT / 2, 0, static_cast(W_CachePatchName( "KKTEXT_C", PU_CACHE )), nullptr ); } void F_CreditDrawer(void) { const struct credits_slide_s *slide = &g_credits_slides[ g_credits.current_slide ]; V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); switch (slide->type) { case CRED_TYPE_SCROLL: { F_DrawCreditsScroll(); break; } case CRED_TYPE_SLIDE: { F_DrawCreditsSlide(); break; } case CRED_TYPE_TITLEDROP: { F_DrawCreditsTitleDrop(); break; } case CRED_TYPE_TYLER52: { F_DrawCreditsTyler52(); break; } case CRED_TYPE_KARTKREW: { F_DrawCreditsKartKrew(); break; } case CRED_TYPE_SONGS: { // TODO break; } default: { break; } } UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_JAWZ, GTC_CACHE); for (auto& star : g_credits.stars) { V_DrawFixedPatch( star.x, star.y, FRACUNIT / 2, V_ADD, static_cast(W_CachePatchName( va("KINB%c0", 'A' + star.frame), PU_CACHE )), colormap ); star.x += FixedMul(star.vel_x, renderdeltatics); star.y += FixedMul(star.vel_y, renderdeltatics); } }