diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c46dbf3ce..d304d0d81 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -158,6 +158,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 k_tally.cpp k_bans.cpp k_endcam.cpp + k_credits.cpp music.cpp music_manager.cpp ) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 72478f55b..c39c936db 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -1447,7 +1447,7 @@ static void CL_LoadReceivedSavegame(boolean reloading) paused = false; demo.playback = false; - demo.title = false; + demo.attract = DEMO_ATTRACT_OFF; titlemapinaction = false; tutorialchallenge = TUTORIALSKIP_NONE; automapactive = false; @@ -2424,7 +2424,7 @@ static void Command_connect(void) return; } - if (Playing() || demo.title) + if (Playing() || demo.attract) { CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n")); return; @@ -6597,15 +6597,22 @@ void NetUpdate(void) nowtime = I_GetTime(); realtics = nowtime - gametime; - if (realtics <= 0) // nothing new to update - return; - - if (realtics > 5) + if (!demo.playback && g_fast_forward > 0) { - if (server) - realtics = 1; - else - realtics = 5; + realtics = 1; + } + else + { + if (realtics <= 0) // nothing new to update + return; + + if (realtics > 5) + { + if (server) + realtics = 1; + else + realtics = 5; + } } #ifdef DEDICATEDIDLETIME diff --git a/src/d_main.cpp b/src/d_main.cpp index 3806109ee..a78e75a6e 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -89,6 +89,7 @@ #include "music.h" #include "k_dialogue.h" #include "k_bans.h" +#include "k_credits.h" #ifdef HWRENDER #include "hardware/hw_main.h" // 3D View Rendering @@ -144,9 +145,11 @@ static char *startuppwads[MAX_WADFILES]; boolean devparm = false; // started game with -devparm -boolean singletics = false; // timedemo +boolean g_singletics = false; // timedemo boolean lastdraw = false; +tic_t g_fast_forward = 0; + postimg_t postimgtype[MAXSPLITSCREENPLAYERS]; INT32 postimgparam[MAXSPLITSCREENPLAYERS]; @@ -630,6 +633,15 @@ static bool D_Display(void) V_DrawCustomFadeScreen(((levelfadecol == 0) ? "FADEMAP1" : "FADEMAP0"), 31-(lt_fade*2)); } + if (demo.attract == DEMO_ATTRACT_CREDITS) + { + INT32 val = F_CreditsDemoExitFade(); + if (val >= 0) + { + V_DrawCustomFadeScreen("FADEMAP0", val); + } + } + VID_DisplaySoftwareScreen(); } @@ -950,7 +962,7 @@ void D_SRB2Loop(void) rendertimefrac_unpaused = FRACUNIT; } - if ((interp || doDisplay) && !frameskip) + if ((interp || doDisplay) && !frameskip && g_fast_forward == 0) { if (!renderisnewtic) P_ResetInterpHudRandSeed(false); @@ -1077,7 +1089,7 @@ void D_ClearState(void) memset(displayplayers, 0, sizeof(displayplayers)); memset(g_localplayers, 0, sizeof g_localplayers); consoleplayer = 0; - demo.title = false; + demo.attract = DEMO_ATTRACT_OFF; G_SetGametype(GT_RACE); // SRB2kart paused = false; @@ -1610,6 +1622,9 @@ void D_SRB2Main(void) #endif //ifndef DEVELOP mainwads++; // shaders.pk3 + // Load credits_def lump + F_LoadCreditsDefinitions(); + // Do it before P_InitMapData because PNG patch // conversion sometimes needs the palette V_ReloadPalette(); diff --git a/src/deh_soc.c b/src/deh_soc.c index 9d9ff8bf3..0ddb0c5c7 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3539,10 +3539,10 @@ void readmaincfg(MYFILE *f, boolean mainfile) } else if (fastcmp(word, "CREDITSCUTSCENE")) { - creditscutscene = (UINT8)get_number(word2); + g_credits_cutscene = (UINT8)get_number(word2); // range check, you morons. - if (creditscutscene > 128) - creditscutscene = 128; + if (g_credits_cutscene > 128) + g_credits_cutscene = 128; } else if (fastcmp(word, "USESEAL")) { diff --git a/src/discord.c b/src/discord.c index 516172a67..861069bd4 100644 --- a/src/discord.c +++ b/src/discord.c @@ -154,7 +154,7 @@ static void DRPC_HandleJoin(const char *secret) char *ip = DRPC_XORIPString(secret); CONS_Printf("Connecting to %s via Discord\n", ip); M_ClearMenus(true); //Don't have menus open during connection screen - if (demo.playback && demo.title) + if (demo.playback && demo.attract) G_CheckDemoStatus(); //Stop the title demo, so that the connect command doesn't error if a demo is playing COM_BufAddText(va("connect \"%s\"\n", ip)); free(ip); @@ -482,7 +482,7 @@ void DRPC_UpdatePresence(void) // Offline info if (Playing()) discordPresence.state = "Offline"; - else if (demo.playback && !demo.title) + else if (demo.playback && !demo.attract) discordPresence.state = "Watching Replay"; else discordPresence.state = "Menu"; @@ -507,7 +507,7 @@ void DRPC_UpdatePresence(void) } if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) // Map info - && !(demo.playback && demo.title)) + && !(demo.playback && demo.attract)) { #ifdef USEMAPIMG if ((gamemap >= 1 && gamemap <= 60) // supported race maps diff --git a/src/doomstat.h b/src/doomstat.h index 333bfd6f0..139f78bc5 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -786,7 +786,7 @@ extern INT32 wantedreduce; extern INT32 wantedfrequency; extern UINT8 introtoplay; -extern UINT8 creditscutscene; +extern UINT8 g_credits_cutscene; extern UINT8 useSeal; extern UINT8 use1upSound; @@ -914,7 +914,10 @@ extern INT16 wipetypepre; extern INT16 wipetypepost; // debug flag to cancel adaptiveness -extern boolean singletics; +extern boolean g_singletics; +extern tic_t g_fast_forward; + +#define singletics (g_singletics == true || g_fast_forward > 0) // ============= // Netgame stuff diff --git a/src/f_finale.c b/src/f_finale.c index 2975aec1d..ab978cf56 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -48,6 +48,7 @@ #include "k_menu.h" #include "k_grandprix.h" #include "music.h" +#include "k_credits.h" // Stage of animation: // 0 = text, 1 = art screen @@ -59,7 +60,6 @@ boolean titlemapinaction = false; static INT32 timetonext; // Delay between screen changes static tic_t animtimer; // Used for some animation timings -static tic_t credbgtimer; // Credits background static tic_t stoptimer; @@ -579,346 +579,6 @@ boolean F_IntroResponder(event_t *event) return true; } -// ========= -// CREDITS -// ========= -static const char *credits[] = { - "\1Dr. Robotnik's Ring Racers", - "\1Credits", - "", - "\1Game Design", - "Sally \"TehRealSalt\" Cochenour", - "Jeffery \"Chromatian\" Scott", - "\"VelocitOni\"", - "", - "\1Lead Programming", - "Sally \"TehRealSalt\" Cochenour", - "Vivian \"toaster\" Grannell", - "Sean \"Sryder\" Ryder", - "Ehab \"wolfs\" Saeed", - "\"ZarroTsu\"", - "", - "\1Support Programming", - "Colette \"fickleheart\" Bordelon", - "James R.", - "\"Lat\'\"", - "\"Monster Iestyn\"", - "\"Shuffle\"", - "\"SteelT\"", - "", - "\1Lead Artists", - "Desmond \"Blade\" DesJardins", - "\"VelocitOni\"", - "", - "\1Support Artists", - "Sally \"TehRealSalt\" Cochenour", - "Sherman \"CoatRack\" DesJardins", - "\"DrTapeworm\"", - "Jesse \"Jeck Jims\" Emerick", - "Wesley \"Charyb\" Gillebaard", - "Vivian \"toaster\" Grannell", - "James \"SeventhSentinel\" Hall", - "\"Lat\'\"", - "\"Tyrannosaur Chao\"", - "\"ZarroTsu\"", - "", - "\1External Artists", - "\"1-Up Mason\"", - "\"Chengi\"", - "\"Chrispy\"", - "\"DirkTheHusky\"", - "\"LJSTAR\"", - "\"MotorRoach\"", - "\"Nev3r\"", - "\"rairai104n\"", - "\"Ritz\"", - "\"Rob\"", - "\"SmithyGNC\"", - "\"Snu\"", - "\"Spherallic\"", - "\"TelosTurntable\"", - "\"VAdaPEGA\"", - "\"Virt\"", - "\"Voltrix\"", - "", - "\1Sound Design", - "James \"SeventhSentinel\" Hall", - "Sonic Team", - "\"VAdaPEGA\"", - "\"VelocitOni\"", - "", - "\1Music", - "\"DrTapeworm\"", - "Wesley \"Charyb\" Gillebaard", - "James \"SeventhSentinel\" Hall", - "", - "\1Lead Level Design", - "\"Blitz-T\"", - "Sally \"TehRealSalt\" Cochenour", - "Desmond \"Blade\" DesJardins", - "Jeffery \"Chromatian\" Scott", - "\"Tyrannosaur Chao\"", - "", - "\1Support Level Design", - "\"Chaos Zero 64\"", - "\"D00D64\"", - "\"DrTapeworm\"", - "Paul \"Boinciel\" Clempson", - "Sherman \"CoatRack\" DesJardins", - "Colette \"fickleheart\" Bordelon", - "Vivian \"toaster\" Grannell", - "\"Gunla\"", - "James \"SeventhSentinel\" Hall", - "\"Lat\'\"", - "\"MK\"", - "\"Ninferno\"", - "Sean \"Sryder\" Ryder", - "\"Ryuspark\"", - "\"Simsmagic\"", - "\"SP47\"", - "\"TG\"", - "\"Victor Rush Turbo\"", - "\"ZarroTsu\"", - "", - "\1Testing", - "\"CyberIF\"", - "\"Dani\"", - "Karol \"Fooruman\" D""\x1E""browski", // DÄ…browski, accents in srb2 :ytho: - "\"VirtAnderson\"", - "", - "\1Special Thanks", - "SEGA", - "Sonic Team", - "SRB2 & Sonic Team Jr. (www.srb2.org)", - "\"Chaos Zero 64\"", - "", - "\1Produced By", - "Kart Krew", - "", - "\1In Memory of", - "\"Tyler52\"", - "", - "", - "\1Thank you ", - "\1for playing! ", - NULL -}; - -#define CREDITS_LEFT 8 -#define CREDITS_RIGHT ((BASEVIDWIDTH) - 8) - -static struct { - UINT32 x, y; - const char *patch; - UINT8 colorize; -} credits_pics[] = { - // We don't have time to be fancy, let's just colorize some item sprites :V - {224, 80+(200* 1), "K_ITJAWZ", SKINCOLOR_CREAMSICLE}, - {224, 80+(200* 2), "K_ITSPB", SKINCOLOR_GARDEN}, - {224, 80+(200* 3), "K_ITBANA", SKINCOLOR_LILAC}, - {224, 80+(200* 4), "K_ITHYUD", SKINCOLOR_DREAM}, - {224, 80+(200* 5), "K_ITBHOG", SKINCOLOR_TANGERINE}, - {224, 80+(200* 6), "K_ITSHRK", SKINCOLOR_JAWZ}, - {224, 80+(200* 7), "K_ITSHOE", SKINCOLOR_MINT}, - {224, 80+(200* 8), "K_ITGROW", SKINCOLOR_RUBY}, - {224, 80+(200* 9), "K_ITPOGO", SKINCOLOR_SAPPHIRE}, - {224, 80+(200*10), "K_ITRSHE", SKINCOLOR_YELLOW}, - {224, 80+(200*11), "K_ITORB4", SKINCOLOR_DUSK}, - {224, 80+(200*12), "K_ITEGGM", SKINCOLOR_GREEN}, - {224, 80+(200*13), "K_ITMINE", SKINCOLOR_BRONZE}, - {224, 80+(200*14), "K_ITTHNS", SKINCOLOR_RASPBERRY}, - {224, 80+(200*15), "K_ITINV1", SKINCOLOR_GREY}, - // This Tyler52 gag is troublesome - // Alignment should be ((spaces+1 * 100) + (headers+1 * 38) + (lines * 15)) - // Current max image spacing: (200*17) - {112, (15*100)+(17*38)+(88*15), "TYLER52", SKINCOLOR_NONE} -}; - -#undef CREDITS_LEFT -#undef CREDITS_RIGHT - -static UINT32 credits_height = 0; -static const UINT8 credits_numpics = sizeof(credits_pics)/sizeof(credits_pics[0]) - 1; - -void F_StartCredits(void) -{ - G_SetGamestate(GS_CREDITS); - - // Just in case they're open ... somehow - M_ClearMenus(true); - - if (creditscutscene) - { - F_StartCustomCutscene(creditscutscene - 1, false, false); - return; - } - - gameaction = ga_nothing; - paused = false; - CON_ToggleOff(); - Music_StopAll(); - S_StopSounds(); - - Music_Play("credits"); - - finalecount = 0; - animtimer = 0; - timetonext = 2*TICRATE; - keypressed = false; -} - -void F_CreditDrawer(void) -{ - UINT16 i; - fixed_t y = (80<>1); - - //V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); - - // Draw background - V_DrawSciencePatch(0, 0 - FixedMul(32<>1; - - if (credits_pics[i].colorize != SKINCOLOR_NONE) - { - colormap = R_GetTranslationColormap(TC_RAINBOW, credits_pics[i].colorize, GTC_MENUCACHE); - sc = FRACUNIT; // quick hack so I don't have to add another field to credits_pics - } - - V_DrawFixedPatch(credits_pics[i].x<>FRACBITS > -20) - V_DrawCreditString((160 - (V_CreditStringWidth(&credits[i][1])>>1))<>FRACBITS > -10) - V_DrawStringAtFixed((BASEVIDWIDTH-V_StringWidth(&credits[i][1], V_YELLOWMAP))<>1, y, V_YELLOWMAP, &credits[i][1]); - y += 12<>FRACBITS > -10) - V_DrawStringAtFixed(32<>FRACBITS) * vid.dupy) > vid.height) - break; - } - - if (finalecount) - { - Y_DrawIntermissionButton(-1, (timetonext ? 5*TICRATE : TICRATE) - finalecount); - } - else - { - Y_DrawIntermissionButton(timetonext, 0); - } -} - -void F_CreditTicker(void) -{ - // "Simulate" the drawing of the credits so that dedicated mode doesn't get stuck - UINT16 i; - fixed_t y = (80<>1); - - // Calculate credits height to display art properly - if (credits_height == 0) - { - for (i = 0; credits[i]; i++) - { - switch(credits[i][0]) - { - case 0: credits_height += 80; break; - case 1: credits_height += 30; break; - default: credits_height += 12; break; - } - } - credits_height = 131*credits_height/80; // account for scroll speeds. This is a guess now, so you may need to update this if you change the credits length. - } - - // Draw credits text on top - for (i = 0; credits[i]; i++) - { - switch(credits[i][0]) - { - case 0: y += 80< vid.height) - break; - } - - if (timetonext) - timetonext--; - else - animtimer++; - - credbgtimer++; - - // Do this here rather than in the drawer you doofus! (this is why dedicated mode broke at credits) - - const boolean reachedbottom = (!credits[i] && y <= 120<everfinishedcredits = true; - if (M_UpdateUnlockablesAndExtraEmblems(true, true)) - G_SaveGameData(); - } - else if (timetonext) - ; - /*else if (!(gamedata->timesBeaten) && !(netgame || multiplayer) && !cht_debug) - ;*/ - else if (!menuactive && M_MenuConfirmPressed(0)) - { - finalecount = TICRATE; - - if (netgame - && (server || IsPlayerAdmin(consoleplayer)) - ) - { - SendNetXCmd(XD_EXITLEVEL, NULL, 0); - return; - } - } -} - // ============ // EVALUATION // ============ @@ -1925,14 +1585,14 @@ void F_TitleScreenTicker(boolean run) strlcpy(dname, lumpname, sizeof(dname)); loadreplay: - demo.title = true; + demo.attract = DEMO_ATTRACT_TITLE; demo.ignorefiles = true; demo.loadfiles = false; G_DoPlayDemo(dname); } } -void F_TitleDemoTicker(void) +void F_AttractDemoTicker(void) { keypressed = false; } @@ -2058,7 +1718,7 @@ void F_EndCutScene(void) } else { - if (cutnum == creditscutscene-1) + if (cutnum == g_credits_cutscene-1) F_StartGameEvaluation(); else if (cutnum == introtoplay-1) D_StartTitle(); diff --git a/src/f_finale.h b/src/f_finale.h index b3e4b4545..5b72790b7 100644 --- a/src/f_finale.h +++ b/src/f_finale.h @@ -35,7 +35,7 @@ boolean F_CutsceneResponder(event_t *ev); void F_IntroTicker(void); void F_TitleScreenTicker(boolean run); void F_CutsceneTicker(void); -void F_TitleDemoTicker(void); +void F_AttractDemoTicker(void); void F_TextPromptTicker(void); // Called by main loop. @@ -51,9 +51,6 @@ void F_GameEvaluationDrawer(void); void F_StartGameEvaluation(void); void F_GameEvaluationTicker(void); -void F_CreditTicker(void); -void F_CreditDrawer(void); - void F_VersionDrawer(void); void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean resetplayer); @@ -71,7 +68,6 @@ void F_StartGameEnd(void); void F_StartIntro(void); void F_StartTitleScreen(void); void F_StartEnding(void); -void F_StartCredits(void); extern INT32 finalecount; extern INT32 titlescrollxspeed; diff --git a/src/g_demo.c b/src/g_demo.c index 4065c9304..6fd166db1 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -55,6 +55,7 @@ #include "k_color.h" #include "k_follower.h" #include "k_vote.h" +#include "k_credits.h" boolean nodrawers; // for comparative timing purposes boolean noblit; // for comparative timing purposes @@ -3301,7 +3302,7 @@ void G_DoPlayDemo(const char *defdemoname) numlaps = READUINT8(demobuf.p); - if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running. + if (demo.attract) // Attract demos should always play and ought to always be compatible with whatever wadlist is running. G_SkipDemoExtraFiles(&demobuf.p); else if (demo.loadfiles) G_LoadDemoExtraFiles(&demobuf.p); @@ -3614,7 +3615,7 @@ void G_DoPlayDemo(const char *defdemoname) splitscreen = 0; - if (demo.title) + if (demo.attract == DEMO_ATTRACT_TITLE) { splitscreen = M_RandomKey(6)-1; splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views @@ -4020,7 +4021,7 @@ void G_TimeDemo(const char *name) if (cv_vidwait.value) CV_Set(&cv_vidwait, "0"); demo.timing = true; - singletics = true; + g_singletics = true; framecount = 0; demostarttime = I_GetTime(); G_DeferedPlayDemo(name); @@ -4218,7 +4219,7 @@ void G_StopDemo(void) demobuf.buffer = NULL; demo.playback = false; demo.timing = false; - singletics = false; + g_singletics = false; { UINT8 i; @@ -4260,7 +4261,7 @@ boolean G_CheckDemoStatus(void) if (demo.quitafterplaying) I_Quit(); - if (multiplayer && !demo.title) + if (multiplayer && !demo.attract) G_FinishExitLevel(); else { @@ -4270,6 +4271,8 @@ boolean G_CheckDemoStatus(void) COM_ImmedExecute("quit"); else if (modeattacking) M_EndModeAttackRun(); + else if (demo.attract == DEMO_ATTRACT_CREDITS) + F_ContinueCredits(); else D_StartTitle(); } diff --git a/src/g_demo.h b/src/g_demo.h index dcfd920fc..f102e8517 100644 --- a/src/g_demo.h +++ b/src/g_demo.h @@ -43,7 +43,7 @@ struct demovars_s { char titlename[65]; boolean recording, playback, timing; UINT16 version; // Current file format of the demo being played - boolean title; // Title Screen demo can be cancelled by any key + UINT8 attract; // Attract demo can be cancelled by any key boolean rewinding; // Rewind in progress boolean loadfiles, ignorefiles; // Demo file loading options @@ -209,6 +209,13 @@ boolean G_DemoTitleResponder(event_t *ev); boolean G_CheckDemoTitleEntry(void); +typedef enum +{ + DEMO_ATTRACT_OFF = 0, + DEMO_ATTRACT_TITLE, + DEMO_ATTRACT_CREDITS +} demoAttractMode_t; + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/g_game.c b/src/g_game.c index 88effe9d8..1dd9df721 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -72,6 +72,7 @@ #include "music.h" #include "k_roulette.h" #include "k_objects.h" +#include "k_credits.h" #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -271,7 +272,7 @@ UINT8 use1upSound = 0; UINT8 maxXtraLife = 2; // Max extra lives from rings UINT8 introtoplay; -UINT8 creditscutscene; +UINT8 g_credits_cutscene; UINT8 useSeal = 1; tic_t racecountdown, exitcountdown, musiccountdown; // for racing @@ -1284,6 +1285,10 @@ void G_PreLevelTitleCard(void) // boolean G_IsTitleCardAvailable(void) { + // Don't show for attract demos + if (demo.attract) + return false; + // Overwrites all other title card exceptions. if (K_CheckBossIntro() == true) return true; @@ -1316,10 +1321,26 @@ boolean G_Responder(event_t *ev) { //INT32 i; - // any other key pops up menu if in demos - if (gameaction == ga_nothing && !demo.quitafterplaying && - ((demo.playback && !modeattacking && !demo.title && !multiplayer) || gamestate == GS_TITLESCREEN)) + if (demo.playback && demo.attract) { + if (demo.attract == DEMO_ATTRACT_TITLE) + { + // Title demo uses intro responder + if (F_IntroResponder(ev)) + { + // stop the title demo + G_CheckDemoStatus(); + return true; + } + } + + return false; + } + else if (gameaction == ga_nothing + && !demo.quitafterplaying + && ((demo.playback && !modeattacking && !multiplayer) || gamestate == GS_TITLESCREEN)) + { + // any other key pops up menu if in demos if (ev->type == ev_keydown || (ev->type == ev_gamepad_axis && ev->data1 >= JOYANALOGS && ((abs(ev->data2) > JOYAXISRANGE/2 @@ -1333,17 +1354,7 @@ boolean G_Responder(event_t *ev) M_StartControlPanel(); return true; } - return false; - } - else if (demo.playback && demo.title) - { - // Title demo uses intro responder - if (F_IntroResponder(ev)) - { - // stop the title demo - G_CheckDemoStatus(); - return true; - } + return false; } @@ -1861,8 +1872,8 @@ void G_Ticker(boolean run) switch (gamestate) { case GS_LEVEL: - if (demo.title) - F_TitleDemoTicker(); + if (demo.attract) + F_AttractDemoTicker(); P_Ticker(run); // tic the game F_TextPromptTicker(); AM_Ticker(); @@ -1985,6 +1996,16 @@ void G_Ticker(boolean run) K_TickMidVote(); } + + if (g_fast_forward == 0 && demo.attract == DEMO_ATTRACT_CREDITS) + { + F_TickCreditsDemoExit(); + } + + if (g_fast_forward > 0) + { + g_fast_forward--; + } } } diff --git a/src/k_credits.cpp b/src/k_credits.cpp new file mode 100644 index 000000000..fc09f04e1 --- /dev/null +++ b/src/k_credits.cpp @@ -0,0 +1,1182 @@ +// 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 +} + +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( + (cv_alttitle.value ? "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); + } +} diff --git a/src/k_credits.h b/src/k_credits.h new file mode 100644 index 000000000..6711af378 --- /dev/null +++ b/src/k_credits.h @@ -0,0 +1,43 @@ +// 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.h +/// \brief Grand Prix podium cutscene + +#ifndef __K_CREDITS__ +#define __K_CREDITS__ + +#include "doomtype.h" +#include "d_event.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void F_LoadCreditsDefinitions(void); + +void F_CreditsReset(void); + +void F_StartCredits(void); + +void F_ContinueCredits(void); + +void F_TickCreditsDemoExit(void); + +INT32 F_CreditsDemoExitFade(void); + +void F_CreditTicker(void); + +void F_CreditDrawer(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __K_CREDITS__ diff --git a/src/k_hud.cpp b/src/k_hud.cpp index 5622e3abe..ebd065a53 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -5259,6 +5259,11 @@ static void K_drawChallengerScreen(void) static void K_drawLapStartAnim(void) { + if (demo.attract == DEMO_ATTRACT_CREDITS) + { + return; + } + // This is an EVEN MORE insanely complicated animation. const UINT8 t = stplyr->karthud[khud_lapanimation]; const UINT8 progress = 80 - t; @@ -5928,7 +5933,7 @@ void K_drawKartHUD(void) K_drawEmeraldWin(false); } - if (!demo.title) + if (!demo.attract) { // Draw the CHECK indicator before the other items, so it's overlapped by everything else if (LUA_HudEnabled(hud_check)) // delete lua when? @@ -5952,7 +5957,7 @@ void K_drawKartHUD(void) K_drawKartMinimap(); } - if (demo.title) + if (demo.attract) ; else if (gametype == GT_TUTORIAL) { @@ -6002,19 +6007,22 @@ void K_drawKartHUD(void) 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 + if (demo.attract) { - INT32 x = BASEVIDWIDTH - 8, y = BASEVIDHEIGHT-8, snapflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SLIDEIN; - patch_t *pat = static_cast(W_CachePatchName((cv_alttitle.value ? "MTSJUMPR1" : "MTSBUMPR1"), PU_CACHE)); - - if (r_splitscreen == 3) + if (demo.attract == DEMO_ATTRACT_TITLE) // Draw logo on title screen demos { - x = BASEVIDWIDTH/2; - y = BASEVIDHEIGHT/2; - snapflags = 0; - } + INT32 x = BASEVIDWIDTH - 8, y = BASEVIDHEIGHT-8, snapflags = V_SNAPTOBOTTOM|V_SNAPTORIGHT|V_SLIDEIN; + patch_t *pat = static_cast(W_CachePatchName((cv_alttitle.value ? "MTSJUMPR1" : "MTSBUMPR1"), PU_CACHE)); - V_DrawScaledPatch(x-(SHORT(pat->width)), y-(SHORT(pat->height)), snapflags, pat); + if (r_splitscreen == 3) + { + x = BASEVIDWIDTH/2; + y = BASEVIDHEIGHT/2; + snapflags = 0; + } + + V_DrawScaledPatch(x-(SHORT(pat->width)), y-(SHORT(pat->height)), snapflags, pat); + } } else { diff --git a/src/k_menufunc.c b/src/k_menufunc.c index 3ba20e388..0b9003cb1 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -270,7 +270,7 @@ boolean M_Responder(event_t *ev) boolean menuKeyJustChanged = false; if (dedicated - || (demo.playback && demo.title) + || (demo.playback && demo.attract) || M_GamestateCanOpenMenu() == false) { return false; diff --git a/src/k_podium.cpp b/src/k_podium.cpp index fab819594..09a8fa3f4 100644 --- a/src/k_podium.cpp +++ b/src/k_podium.cpp @@ -82,6 +82,8 @@ static struct podiumData_s cupheader_t *cup; + boolean fastForward; + char header[64]; void Init(void); @@ -92,6 +94,8 @@ static struct podiumData_s void podiumData_s::Init(void) { + fastForward = false; + if (grandprixinfo.cup != nullptr) { rank = grandprixinfo.rank; @@ -117,7 +121,7 @@ void podiumData_s::Init(void) memset(&rank, 0, sizeof(gpRank_t)); rank.skin = players[consoleplayer].skin; - rank.numPlayers = 1; //M_RandomRange(1, MAXSPLITSCREENPLAYERS); + rank.numPlayers = std::clamp(M_RandomRange(0, MAXSPLITSCREENPLAYERS + 1), 1, MAXSPLITSCREENPLAYERS); rank.totalPlayers = K_GetGPPlayerCount(rank.numPlayers); rank.position = M_RandomRange(1, 4); @@ -473,6 +477,23 @@ void podiumData_s::Draw(void) .colormap(bestHuman->skin, static_cast(bestHuman->skincolor)) .patch(faceprefix[bestHuman->skin][FACE_WANTED]); + UINT32 continuesColor = 0; + + if (rank.continuesUsed == 0) + { + continuesColor = V_YELLOWMAP; + } + else if (rank.continuesUsed > 2) + { + continuesColor = V_REDMAP; + } + + drawer_winner + .xy(64, 18) + .flags(continuesColor) + .font(srb2::Draw::Font::kThin) + .text(va("Continues used ... %d", rank.continuesUsed)); + if (cup != nullptr) { srb2::Draw drawer_trophy = drawer.xy(272, 10); @@ -993,6 +1014,7 @@ void K_FinishCeremony(void) } g_podiumData.ranking = true; + g_fast_forward = 0; } /*-------------------------------------------------- @@ -1116,6 +1138,34 @@ void K_CeremonyTicker(boolean run) 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; } diff --git a/src/m_misc.cpp b/src/m_misc.cpp index f3340eb58..7c993ca29 100644 --- a/src/m_misc.cpp +++ b/src/m_misc.cpp @@ -1357,7 +1357,7 @@ void M_StartMovie(moviemode_t mode) } #endif - //singletics = (moviemode != MM_OFF); + //g_singletics = (moviemode != MM_OFF); #endif } diff --git a/src/menus/extras-1.c b/src/menus/extras-1.c index 9d1ab4b55..4961d3f8d 100644 --- a/src/menus/extras-1.c +++ b/src/menus/extras-1.c @@ -7,6 +7,7 @@ #include "../m_cheat.h" #include "../s_sound.h" #include "../f_finale.h" +#include "../k_credits.h" static void M_Credits(INT32 choice) { diff --git a/src/menus/transient/pause-replay.c b/src/menus/transient/pause-replay.c index 6eca1ca4e..410d2118a 100644 --- a/src/menus/transient/pause-replay.c +++ b/src/menus/transient/pause-replay.c @@ -8,6 +8,7 @@ #include "../../r_main.h" // R_ExecuteSetViewSize #include "../../p_local.h" // P_InitCameraCmd #include "../../d_main.h" // D_StartTitle +#include "../../k_credits.h" static void M_PlaybackTick(void); @@ -58,10 +59,14 @@ void M_EndModeAttackRun(void) modeattacking = ATTACKING_NONE; // Kept until now because of Command_ExitGame_f - if (demo.title == true) + if (demo.attract == DEMO_ATTRACT_TITLE) { D_StartTitle(); } + else if (demo.attract == DEMO_ATTRACT_CREDITS) + { + F_ContinueCredits(); + } else { D_ClearState(); diff --git a/src/p_setup.cpp b/src/p_setup.cpp index f70f552ca..32e62d59e 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -8259,11 +8259,16 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) P_InitLevelSettings(); - if (!demo.title) + if (demo.attract != DEMO_ATTRACT_TITLE) { Music_Stop("title"); } + if (demo.attract != DEMO_ATTRACT_CREDITS) + { + Music_Stop("credits"); + } + for (i = 0; i <= r_splitscreen; i++) postimgtype[i] = postimg_none; @@ -8378,7 +8383,11 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) FixedDiv((F_GetWipeLength(wipedefs[wipe_level_toblack])-2)*NEWTICRATERATIO, NEWTICRATE), MUSICRATE)); #endif - if (K_PodiumSequence()) + if (demo.attract) + { + ; // Leave the music alone! We're already playing what we want! + } + else if (K_PodiumSequence()) { // mapmusrng is set by local player position in K_ResetCeremony P_LoadLevelMusic(); diff --git a/src/p_tick.c b/src/p_tick.c index a01808db1..4fa43d1e1 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -1075,7 +1075,10 @@ void P_Ticker(boolean run) } } - timeinmap++; + if (g_fast_forward == 0) + { + timeinmap++; + } if (G_GametypeHasTeams()) P_DoTeamStuff(); diff --git a/src/p_user.c b/src/p_user.c index 2ec456914..11cf2473e 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -68,6 +68,7 @@ #include "k_tally.h" #include "k_objects.h" #include "k_endcam.h" +#include "k_credits.h" #ifdef HWRENDER #include "hardware/hw_light.h" @@ -3190,7 +3191,7 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall mo = player->mo; - if (P_MobjIsFrozen(mo) || player->playerstate == PST_DEAD) + if (P_MobjIsFrozen(mo) || player->playerstate == PST_DEAD || F_CreditsDemoExitFade() >= 0) { // Do not move the camera while in hitlag! // The camera zooming out after you got hit makes it hard to focus on the vibration. @@ -4003,6 +4004,11 @@ DoABarrelRoll (player_t *player) fixed_t smoothing; + if (player->exiting || F_CreditsDemoExitFade() >= 0) + { + return; + } + if (player->loop.radius) { return; @@ -4014,11 +4020,6 @@ DoABarrelRoll (player_t *player) return; } - if (player->exiting) - { - return; - } - slope = InvAngle(R_GetPitchRollAngle(player->mo, player)); if (AbsAngle(slope) < ANGLE_11hh) diff --git a/src/s_sound.c b/src/s_sound.c index 885f6d1c2..93a0ecff9 100644 --- a/src/s_sound.c +++ b/src/s_sound.c @@ -249,7 +249,8 @@ boolean S_SoundDisabled(void) { return ( sound_disabled || - ( window_notinfocus && ! (cv_bgaudio.value & 2) ) + ( window_notinfocus && ! (cv_bgaudio.value & 2) ) || + (g_fast_forward > 0) ); } @@ -1227,6 +1228,10 @@ void S_AttemptToRestoreMusic(void) case GS_MENU: M_PlayMenuJam(); break; + case GS_CREDITS: + Music_Loop("credits", true); + Music_Play("credits"); + break; default: break; } diff --git a/src/st_stuff.c b/src/st_stuff.c index 82be792c5..50e3ba29f 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -749,7 +749,7 @@ patch_t *ST_getRoundPicture(boolean small) // void ST_runTitleCard(void) { - boolean run = !(paused || P_AutoPause()); + boolean run = !(paused || P_AutoPause() || g_fast_forward > 0); INT32 auxticker; boolean doroundicon = (ST_getRoundPicture(false) != NULL); @@ -1246,7 +1246,7 @@ static void ST_overlayDrawer(void) { if (cv_showviewpointtext.value) { - if (!demo.title && !P_IsLocalPlayer(stplyr) && !camera[viewnum].freecam) + if (!demo.attract && !P_IsLocalPlayer(stplyr) && !camera[viewnum].freecam) { if (!r_splitscreen) {