// BLANKART //----------------------------------------------------------------------------- // Copyright (C) 2025 by Vivian "toastergrl" Grannell. // Copyright (C) 2025 by Kart Krew. // Copyright (C) 2025 by "Anonimus". // Copyright (C) 2025 Blankart Team. // // 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_dscredits.cpp /// \brief (Mostly) secret MKDS credits :) #include #include "f_dscredits.hpp" #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_threads.h" #include "m_menu.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" // R_GetTimeFrac #include "r_things.h" // numskins #include "r_skins.h" #include "lua_hud.h" #include "lua_hook.h" //static INT16 dscredits_idx = 0; static longtic_t elapsed = 0; static longtic_t lastelapsed = 0; static longtic_t elapsed_diff; static longtic_t kartcredits_start = 13335000; static longtic_t kartcredits_end = 186384000; static longtic_t kartcredits_length = (kartcredits_end - kartcredits_start); static fixed_t kartcredits_dividend = kartcredits_length / FRACUNIT; static fixed_t kartcredits_height; static longtic_t blancredits_start = 195977000; static longtic_t blancredits_end = 294593000; static longtic_t blancredits_length = (blancredits_end - blancredits_start); static fixed_t blancredits_dividend = blancredits_length / FRACUNIT; static fixed_t blancredits_height; static longtic_t finalescene; static INT32 finalesecs, finalespeed; static boolean dscredits_running = false; // Title Screen static patch_t *ttbanner; // SONIC ROBO BLAST 2 static patch_t *ttkart; // *vroom* KART static patch_t *ttcheckers; // *vroom* KART static patch_t *ttkflash; // flash screen // Scenery #define NUMDSBUILDINGS 6 static patch_t *dsground; // Ground static patch_t *dsgrass; // Grass static patch_t *dssky; // Sky static patch_t *tylergag; // Tyler static patch_t *dsbuildings[NUMDSBUILDINGS]; std::chrono::high_resolution_clock timer; std::chrono::high_resolution_clock::time_point start, curr; #define FULLCREDITSBGCOLOR (254) #define CREDSKINCOUNT (MAXPLAYERS + DSUNIQUEPLAYERS) #define MAXMISCOBJS (MAXDSOBJECTS - CREDSKINCOUNT) #define KARTMOVE (884736) // 183 pixels in 20.5 seconds #define RIVALAPPROACH (509542) #define SPECIALAPPROACH (3396949) #define FINALESPD (1791317) #define MICROSECS(x) (x * 1000000) #define FINALESECDELAY (MICROSECS(1)) // "Overconfident" character's initial speed. Dynamically adapts to screen resolution. #define TOODAMNFAST(w) ((w + 256) * FRACUNIT) #define MICROSEC_TICS (MICROSECS(1) / TICRATE) #define LASTSCENE (21) // Converts microseconds to fixed-point values. #define UsecToFixed(x) (static_cast((static_cast(x / 1000) * FRACUNIT) / 1000)) static fixed_t scroll_slow = (5033164); // 192 pixels in 2.5 seconds static fixed_t scroll_fast = (12582912); // 192 pixels in 1 second // Generic scrolling multiplier // 0 = current, 1 = last, 2 = offset static fixed_t scene_scroll[3]; static fixed_t tylerscroll; static longtic_t hityler; static longtic_t building_delay; static longtic_t player_delay; static UINT16 numbuildings; static UINT16 numplayerobjs; static boolean spawnplayers; static INT32 playersadded; static INT32 player_iterate; static UINT16 specialiterate; static inline INT64 LongFixedMul(fixed_t a, fixed_t b) { // Need to cast to unsigned before shifting to avoid undefined behaviour // for negative integers return (INT64)(((UINT64)((INT64)a * b)) >> FRACBITS); } // Handles interpolating microseconds into a scroll offset. #define CreditsScrollFx(x,y) (static_cast(LongFixedMul(UsecToFixed(x), y))) #define CreditsScroll(x,y) (CreditsScrollFx(x,y) >> FRACBITS) static UINT16 credits_skins[CREDSKINCOUNT]; static UINT16 credits_colors[CREDSKINCOUNT]; static UINT16 player1_idx, rival_idx, special_idx, specialp, tylermemoriam; static const char* gamettl = "SRB2KART"; dscredits_t dscredits; extern "C" { // These timings are in microseconds (1 second is 1000000 microseconds) longtic_t creditstimings[] = { // ACT 1 (Kart credits) 7335000, // Title screen text fades out (fadeout is 1.5 seconds) 13335000, // Credits begin to scroll 32005000, // Screen fades into sunset scene (fadein is 2 seconds) 77350000, // Characters begin to randomly appear // (spawn rate should be betwen 2.5 and 5 seconds) 134528000, // Stop spawning characters for some time 160036000, // If they're not an RR character, spawn in Tails. // If they ARE an RR character, spawn in one of 3 random rivals. // They should drive up for around 20 seconds, before stopping. 180036000, // Stop the rival here. // ACT 2 (Blan credits) 195977000, // Both players speed up! The HOPE-O-METER is at maximum! 210489000, // Spawn a random "special" character. These stop in place for a bit. // (Drive ahead for 6 seconds, then stop for 4, then drive off). 218154000, // Spawn another special character. This one acts the same as the previous. 226260000, // Spawn a third special character. This one's overconfident and ends up spinning out. // * Drive ahead for 1 second (really fast!) // * Pause offscreen on the left for 2.5 seconds // * Spin around all the way back for 2.75 seconds 234260000, // Back to normalcy, mostly. Spawn a random character. 239260000, // The "overconfident" one returns, this time properly ready and // moving at a decent pace. // They lead the pack of randomly spawning racers. // Back to normalcy (for real this time)! 254688000, // Stop spawning characters for some time 267874000, // The big finish! All racers come in in a line... 296536000, // ...and they all drive off into the sunset. 298978000, // Fade the screen. 300226428, // SONIC ROBO BLAST 2 301374285, // *vroom* KART 303311000, // One more fadeout... 308203000, // (scene 21) This should return you to the title screen. INFLONGTICS // Dummy (Let's *not* crash the game...) }; // Basically just trims the "Thanks for playing!" text static const char *credits[] = { "\1SRB2Kart", "\1Credits", "", "\1Game Design", "Sally \"TehRealSalt\" Cochenour", "Jeffery \"Chromatian\" Scott", "\"VelocitOni\"", "", "\1Lead Programming", "Sally \"TehRealSalt\" Cochenour", "Vivian \"toaster\" Grannell", "Ronald \"Eidolon\" Kinard", "James Robert Roman", "Sean \"Sryder\" Ryder", "Ehab \"wolfs\" Saeed", "\"ZarroTsu\"", "", "\1Support Programming", "\"Lach\"", "\"Lat\'\"", "AJ \"Tyron\" Martinez", "\"Monster Iestyn\"", "\"SteelT\"", "", "\1External Programming", "Alam Ed Arias", "\"alphaRexJames\"", "\"Ashnal\"", "\"filpAM\"", "\"FlykeSpice\"", "\"Hannu Hanhi\"", "\"himie\"", "\"JugadorXEI\"", "\"Kimberly\"", "\"Lighto97\"", "\"Lonsfor\"", "\"mazmazz\"", "\"minenice\"", "\"Shuffle\"", "\"Snu\"", "\"X.organic\"", "", "\1Lead Artists", "Desmond \"Blade\" DesJardins", "\"VelocitOni\"", "", "\1Support Artists", "Sally \"TehRealSalt\" Cochenour", "\"Chengi\"", "\"Chrispy\"", "Sherman \"CoatRack\" DesJardins", "\"DrTapeworm\"", "Jesse \"Jeck Jims\" Emerick", "Wesley \"Charyb\" Gillebaard", "\"Nev3r\"", "Vivian \"toaster\" Grannell", "James \"SeventhSentinel\" Hall", "\"Lat\'\"", "\"rairai104n\"", "\"Tyrannosaur Chao\"", "\"ZarroTsu\"", "", "\1External Artists", "\"1-Up Mason\"", "\"DirkTheHusky\"", "\"LJSTAR\"", "\"MotorRoach\"", "\"Ritz\"", "\"Rob\"", "\"SmithyGNC\"", "\"Snu\"", "\"Spherallic\"", "\"TelosTurntable\"", "\"VAdaPEGA\"", "\"Virt\"", "\"Voltrix\"", "", "\1Sound Design", "James \"SeventhSentinel\" Hall", "Sonic Team", "\"VAdaPEGA\"", "\"VelocitOni\"", "", "\1Original Music", "\"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", "Vivian \"toaster\" Grannell", "\"Gunla\"", "James \"SeventhSentinel\" Hall", "\"Lat\'\"", "\"MK\"", "\"Ninferno\"", "Sean \"Sryder\" Ryder", "\"Ryuspark\"", "\"Simsmagic\"", "Ivo Solarin", "\"SP47\"", "\"TG\"", "\"Victor Rush Turbo\"", "\"ZarroTsu\"", "", "\1Testing", "RKH License holders", "The KCS", "\"CyberIF\"", "\"Dani\"", "Karol \"Fooruman\" D""\x1E""browski", // DÄ…browski, accents in srb2 :ytho: "\"Virt\"", "", "\1Special Thanks", "SEGA", "Sonic Team", "SRB2 & Sonic Team Jr. (www.srb2.org)", "\"Chaos Zero 64\"", "", "\1Produced By", "Kart Krew", "", "\1In Memory of", "\2\"Tyler52\"", NULL }; tic_t dscreditsticks = 0; static INT32 F_FindFirstFreeObj() { INT32 i = 0; for (i = 0; i < MAXMISCOBJS; i++) { // Found one. if (!dscredits.creditsobj[i].active) { break; } } return i; } static void F_SpawnCreditsObject(INT32 id, INT32 x, INT32 y, fixed_t scale, patch_t *patch, INT64 stopat) { dscredits.creditsobj[id].active = true; dscredits.creditsobj[id].position[0] = x << FRACBITS; dscredits.creditsobj[id].position[1] = y << FRACBITS; dscredits.creditsobj[id].scale = scale; dscredits.creditsobj[id].patch = patch; dscredits.creditsobj[id].movestop = stopat; } static INT32 F_FindFirstFreePlayer() { INT32 i = 0; for (i = MAXMISCOBJS; i < MAXDSOBJECTS; i++) { // Found one. if (!dscredits.creditsobj[i].active) { break; } } return i; } static void F_SpawnCreditsPlayer(INT32 id, INT32 x, INT32 y, fixed_t scale, UINT16 skin, UINT16 color, INT64 stopat) { dscredits.creditsobj[id].active = true; dscredits.creditsobj[id].position[0] = x << FRACBITS; dscredits.creditsobj[id].position[1] = y << FRACBITS; dscredits.creditsobj[id].scale = scale; dscredits.creditsobj[id].skin = skin; dscredits.creditsobj[id].color = color; dscredits.creditsobj[id].movestop = stopat; dscredits.creditsobj[id].movestart = stopat; } static fixed_t F_FixedLerp(fixed_t v0, fixed_t v1, fixed_t t) { return FixedMul(v1,t) + FixedMul(FRACUNIT - t, v0); } void F_SecretCreditsTicker(void) { UINT16 i; INT32 credskins; UINT16 pskin, pcol; if (dscreditsticks < 1) { dscredits.creditsscene = 0; tylermemoriam = 0; hityler = 0; building_delay = 0; numbuildings = 0; player_delay = 0; player_iterate = 0; specialiterate = 0; playersadded = 0; finalescene = 0; finalespeed = 0; finalesecs = -1; spawnplayers = false; dscredits_running = false; } if (dscreditsticks == 1) { // Use the second tic to load in all necessary content. // Timer and scene counter. elapsed = 0; dscredits.creditsscene = 0; credskins = CREDSKINCOUNT - 1; // SRB2K logo ttbanner = static_cast(W_CachePatchName("TTKBANNR", PU_PATCH_LOWPRIORITY)); ttkart = static_cast(W_CachePatchName("TTKART", PU_PATCH_LOWPRIORITY)); ttcheckers = static_cast(W_CachePatchName("TTCHECK", PU_PATCH_LOWPRIORITY)); ttkflash = static_cast(W_CachePatchName("TTKFLASH", PU_PATCH_LOWPRIORITY)); // Scenery patches dsground = static_cast(W_CachePatchName("EXCDGND", PU_PATCH_LOWPRIORITY)); dsgrass = static_cast(W_CachePatchName("EXCDGRS", PU_PATCH_LOWPRIORITY)); dssky = static_cast(W_CachePatchName("EXCDSKY", PU_PATCH_LOWPRIORITY)); tylergag = static_cast(W_CachePatchName("TYLER52", PU_PATCH_LOWPRIORITY)); const char* patchNames[] = { "EXCDBD1", "EXCDBD2", "EXCDBD3", "EXCDBD4", "EXCDBD5", "EXCDBD6" }; for (i = 0; i < NUMDSBUILDINGS; i++) { dsbuildings[i] = static_cast(W_CachePatchName(patchNames[i], PU_PATCH_LOWPRIORITY)); } // HUD object array for (i = 0; i < MAXDSOBJECTS; i++) { dscredits.creditsobj[i].active = false; dscredits.creditsobj[i].skin = MAXSKINS; dscredits.creditsobj[i].scale = FRACUNIT; } // Allocate skins for the local players. for (i = 0; i < (splitscreen + 1); i++) { pskin = R_SkinAvailable(cv_skin[i].string); if (pskin >= MAXSKINS) { pskin = 0; } if (!R_SkinUsable(-1, pskin)) { pskin = 0; } if (i == 0) { // Player 1 always gets priority. player1_idx = credskins; } pcol = cv_playercolor[i].value; credits_skins[credskins] = pskin; credits_colors[credskins] = pcol; credskins--; } // Allocate the rival's skin, if possible. UINT16 grabskins[MAXSKINS]; UINT16 rand, rivalskin; grabskins[0] = R_SkinAvailable(skins[pskin].rivals[0]); grabskins[1] = R_SkinAvailable(skins[pskin].rivals[1]); grabskins[2] = R_SkinAvailable(skins[pskin].rivals[2]); // Pick a random initial point to find the rival. rand = M_RandomKey(SKINRIVALS); // Start verifying and looking for the first valid rival. for (i = 0; i < 5; i++) { rivalskin = grabskins[(i + rand) % SKINRIVALS]; if ((rivalskin >= MAXSKINS)||(!R_SkinUsable(-1, rivalskin))) { // Not valid (likely -1, or can't be used) continue; } // Got one, I think! break; } if (rivalskin >= MAXSKINS) { // Couldn't find a rival. Get Tails! rivalskin = R_SkinAvailable("tails"); if ((rivalskin >= MAXSKINS)||(!R_SkinUsable(-1, rivalskin))) { // ...goddammit. rivalskin = 0; } } // Add in the rival. rival_idx = credskins; credits_skins[credskins--] = rivalskin; UINT16 usableskins = 0; UINT16 j; for (i = 0; i < numskins; i++) { if (!R_SkinUsable(-1, i)) continue; for (j = 0; j < (splitscreen + 1); j++) { if (i == credits_skins[(CREDSKINCOUNT - 1) - j]) { // Already used by a player. continue; } } if (i == rivalskin) { // Already used by the rival. continue; } grabskins[usableskins++] = i; } if (!usableskins) { I_Error("F_SecretCreditsTicker: No valid skins to pick from!?"); } // Pick a random initial point to load in the usable skins rand = M_RandomKey(usableskins); // Pre-allocate the "special character" index. This can be random. special_idx = credskins; while(credskins >= 0) { // Allocate the remaining skins from the skin list. credits_skins[credskins] = grabskins[((rand + credskins) % usableskins)]; credits_colors[credskins] = skins[credits_skins[credskins]].prefcolor; credskins--; if (credskins < 0) { // Break if we're out of range! break; } } // Finally, get the height of the credits scroll. Consistent timing is nice. :) kartcredits_height = 0; blancredits_height = 0; for (i = 0; credits[i]; i++) { switch(credits[i][0]) { case 0: kartcredits_height += 50<height) >> 1) - 1, FRACUNIT >> 1, credits_skins[player1_idx], credits_colors[player1_idx], INFLONGTICS); start = timer.now(); S_ChangeMusicInternal("BLNCDS", false); S_ShowMusicCredit(0, 5*TICRATE, 0); } if (!dscredits_running) { return; } scene_scroll[1] = scene_scroll[0]; curr = timer.now(); const std::chrono::duration fp_ms = curr - start; // This is fucking stupid. lastelapsed = elapsed; elapsed = static_cast(fp_ms.count() * 1000000); if ((tylermemoriam) && (!hityler)) { hityler = elapsed; } if (elapsed > creditstimings[dscredits.creditsscene]) { // Increment the scene count by 1. dscredits.creditsscene++; } fixed_t lerptime = std::max(0, std::min(static_cast(FRACUNIT), static_cast(elapsed - creditstimings[7]) / 8)); fixed_t scroll_mul = scroll_slow; elapsed_diff = elapsed - lastelapsed; if (dscredits.creditsscene > 1) { scroll_mul = F_FixedLerp(scroll_slow, scroll_fast, lerptime); if ((elapsed > creditstimings[2]) && (dscredits.creditsscene < 16)) { scene_scroll[0] += CreditsScroll((elapsed_diff), scroll_mul); } else if (dscredits.creditsscene >= 16) { finalespeed = CreditsScrollFx((elapsed_diff), scroll_mul) * -1; if (specialiterate == 7) { for (i = MAXMISCOBJS; i < MAXMISCOBJS + MAXPLAYERS; i++) { if (!dscredits.creditsobj[i].active) { continue; } dscredits.creditsobj[i].speed[0] = -scroll_mul; } specialiterate++; dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0] = -scroll_mul; } dscredits.creditsobj[player1_idx + MAXMISCOBJS].position[0] += finalespeed; } scene_scroll[2] = (scene_scroll[0] - scene_scroll[1]); if (hityler) { tylerscroll = CreditsScroll((elapsed - hityler), scroll_mul); } } if (dscredits.creditsscene > 2) { INT16 idx; numbuildings = 0; numplayerobjs = 0; building_delay = static_cast(std::max(static_cast(0), static_cast(building_delay) - static_cast(elapsed_diff))); if (!building_delay) { idx = F_FindFirstFreeObj(); patch_t *bld = dsbuildings[M_RandomKey(NUMDSBUILDINGS)]; F_SpawnCreditsObject(idx, (-vidxdiff) - SHORT(bld->width), (-vidydiff) + h - (SHORT(dsground->height) >> 1) - SHORT(bld->height), FRACUNIT, bld, INFLONGTICS); // 5 or 9 seconds, random delay building_delay = static_cast(M_RandomRange(5, 9)) * F_FixedLerp(1000000,400000,lerptime); } // Dynamically do this for consistent timing. fixed_t vidwidthmul = 205 * w; fixed_t approach = FixedMul(SPECIALAPPROACH, vidwidthmul); if (spawnplayers) { player_delay = static_cast(std::max(static_cast(0), static_cast(player_delay) - static_cast(elapsed_diff))); if (!player_delay) { idx = F_FindFirstFreePlayer(); F_SpawnCreditsPlayer(idx, (-vidxdiff) + w + 128, (-vidydiff) + h - (SHORT(dsground->height) >> 1) - 1, FRACUNIT >> 1, credits_skins[player_iterate], credits_colors[player_iterate], INFLONGTICS); dscredits.creditsobj[idx].speed[0] = -KARTMOVE; // 8 or 12 seconds, random delay if (dscredits.creditsscene >= 12) { dscredits.creditsobj[idx].speed[0] = (dscredits.creditsobj[idx].speed[0] * 9) / 2; player_delay = static_cast(M_RandomRange(8, 12)) * 250000; } else { player_delay = static_cast(M_RandomRange(8, 12)) * 1000000; } playersadded++; player_iterate++; player_iterate %= MAXPLAYERS; } } // Set up players for the second set of spawns. if (dscredits.creditsscene == 12) { player_delay = static_cast(M_RandomRange(8, 12)) * 250000; } if ((dscredits.creditsscene == 4) || (dscredits.creditsscene == 13)) { spawnplayers = true; } else { spawnplayers = false; } // Rival handling. if (dscredits.creditsscene == 6) { if (!specialiterate) { F_SpawnCreditsPlayer(rival_idx + MAXMISCOBJS, (-vidxdiff) + w + 128, (-vidydiff) + h - (SHORT(dsground->height) >> 1) - 1, FRACUNIT >> 1, credits_skins[rival_idx], skins[credits_skins[rival_idx]].prefcolor, INFLONGTICS); dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0] = -RIVALAPPROACH; dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop = (40 * 1000000); specialiterate++; } } if (dscredits.creditsobj[rival_idx + MAXMISCOBJS].active) { if (dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop > 0) { dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop = std::max( static_cast(0), static_cast(dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop) - static_cast(elapsed_diff)); if (!dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop) { dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0] = 0; dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop = -1; } } // Apply movements. if (dscredits.creditsscene == 8) { // Scrolls with the FG dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[0] += (scene_scroll[2] << FRACBITS); } // Moves by speed. dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[0] += CreditsScrollFx( (elapsed_diff), dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0]); dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[1] += CreditsScrollFx( (elapsed_diff), dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[1]); // Clamp to "hide" the rival at points fixed_t xpos = (dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[0] >> FRACBITS); fixed_t max_xpos = (-vidxdiff + w + 128); fixed_t min_xpos = (-vidxdiff - 128); if (xpos < min_xpos) { dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[0] = (min_xpos << FRACBITS); } else if (xpos > max_xpos) { dscredits.creditsobj[rival_idx + MAXMISCOBJS].position[0] = (max_xpos << FRACBITS); } } // Special characters. if (((dscredits.creditsscene == 9) && (specialiterate < 2)) || ((dscredits.creditsscene == 10) && (specialiterate < 3))) { idx = F_FindFirstFreePlayer(); F_SpawnCreditsPlayer(idx, (-vidxdiff) + w + 128, (-vidydiff) + h - (SHORT(dsground->height) >> 1) - 1, FRACUNIT >> 1, credits_skins[player_iterate], credits_colors[player_iterate], INFLONGTICS); dscredits.creditsobj[idx].speed[0] = -approach; dscredits.creditsobj[idx].movestop = (6 * 1000000); dscredits.creditsobj[idx].movestart = (8 * 1000000); dscredits.creditsobj[idx].movestartspd[0] = -(approach * 2); specialiterate++; player_iterate++; player_iterate %= MAXPLAYERS; playersadded++; } else if ((dscredits.creditsscene == 11) && (specialiterate < 4)) { idx = F_FindFirstFreePlayer(); F_SpawnCreditsPlayer(idx, (-vidxdiff) + w + 128, (-vidydiff) + h - (SHORT(dsground->height) >> 1) - 1, FRACUNIT >> 1, credits_skins[special_idx], credits_colors[special_idx], INFLONGTICS); dscredits.creditsobj[idx].speed[0] = -TOODAMNFAST(w); dscredits.creditsobj[idx].movestop = (1150000); dscredits.creditsobj[idx].movestart = (3500000); dscredits.creditsobj[idx].movestartspd[0] = scroll_mul; specialp = idx; specialiterate++; } // Miscellaneous interactions. if ((dscredits.creditsobj[specialp].movestop <= 0) && (specialiterate == 4)) { dscredits.creditsobj[specialp].spinout = true; specialiterate++; } if ((dscredits.creditsscene == 12) && (specialiterate == 5)) { dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0] = -((KARTMOVE * 9) / 2); specialiterate++; } if ((dscredits.creditsscene == 13) && (specialiterate == 6)) { dscredits.creditsobj[specialp].spinout = false; dscredits.creditsobj[specialp].speed[0] = -((KARTMOVE * 9) / 2); specialiterate++; } // Finale! if (dscredits.creditsscene == 15) { if (!finalescene) { finalescene = elapsed; } if ((((elapsed - finalescene) / FINALESECDELAY) != (longtic_t)finalesecs)) { finalesecs = ((elapsed - finalescene) / FINALESECDELAY); if (playersadded > 0) { playersadded--; player_iterate--; while (player_iterate < 0) { player_iterate += MAXPLAYERS; } idx = F_FindFirstFreePlayer(); F_SpawnCreditsPlayer(idx, (-vidxdiff) - 128, (-vidydiff) + h - (SHORT(dsground->height) >> 1) - 1, FRACUNIT >> 1, credits_skins[player_iterate], credits_colors[player_iterate], INFLONGTICS); dscredits.creditsobj[idx].speed[0] = FixedMul(vidwidthmul,FINALESPD); } else { if (playersadded > -1) { playersadded--; dscredits.creditsobj[specialp].speed[0] = FixedMul(vidwidthmul,FINALESPD); } else if (playersadded > -2) { playersadded--; dscredits.creditsobj[rival_idx + MAXMISCOBJS].speed[0] = FixedMul(vidwidthmul,FINALESPD); } } } } // Handle scenery. for (i = 0; i < MAXMISCOBJS; i++) { if (!dscredits.creditsobj[i].active) { continue; } numbuildings++; // Scroll along the background. dscredits.creditsobj[i].position[0] += ((scene_scroll[2] << FRACBITS) >> 1); if ((dscredits.creditsobj[i].position[0] >> FRACBITS) > ((-vidxdiff + w) + 48)) { // Offscreen, unload ourselves. dscredits.creditsobj[i].active = false; continue; } } // Handle players. Unique objects are handled on a case-by-case basis. for (i = MAXMISCOBJS; i < MAXMISCOBJS + MAXPLAYERS; i++) { if (!dscredits.creditsobj[i].active) { continue; } numplayerobjs++; if (dscredits.creditsobj[i].movestop > 0) { dscredits.creditsobj[i].movestop = std::max( static_cast(0), static_cast(dscredits.creditsobj[i].movestop) - static_cast(elapsed_diff)); if (!dscredits.creditsobj[i].movestop) { dscredits.creditsobj[i].speed[0] = 0; dscredits.creditsobj[i].movestop = -1; } } if (dscredits.creditsobj[i].movestart > 0) { dscredits.creditsobj[i].movestart = std::max( static_cast(0), static_cast(dscredits.creditsobj[i].movestart) - static_cast(elapsed_diff)); if (!dscredits.creditsobj[i].movestart) { dscredits.creditsobj[i].speed[0] = dscredits.creditsobj[i].movestartspd[0]; dscredits.creditsobj[i].movestart = -1; } } // Apply movements. dscredits.creditsobj[i].position[0] += CreditsScrollFx( (elapsed_diff), dscredits.creditsobj[i].speed[0]); dscredits.creditsobj[i].position[1] += CreditsScrollFx( (elapsed_diff), dscredits.creditsobj[i].speed[1]); if (dscredits.creditsscene < 15) // For the finale, no offscreen handling allowed! { if (((dscredits.creditsobj[i].position[0] >> FRACBITS) < (-vidxdiff - 128)) && (i != specialp)) // The special player needs to be handled differently. { // Offscreen, unload ourselves. dscredits.creditsobj[i].active = false; continue; } } if (i == specialp) { // Handle offscreening VERY differently fixed_t xpos = (dscredits.creditsobj[i].position[0] >> FRACBITS); fixed_t max_xpos = (-vidxdiff + w + 128); fixed_t min_xpos = (-vidxdiff - 128); if (xpos < min_xpos) { dscredits.creditsobj[i].position[0] = (min_xpos << FRACBITS); } else if ((xpos > max_xpos) && (dscredits.creditsscene < 15)) { dscredits.creditsobj[i].position[0] = (max_xpos << FRACBITS); } } } } // After ALL THAT, check if the credits are finished. if (dscredits.creditsscene >= LASTSCENE) { F_StartGameEvaluation(); } /*if (dscredits.creditsscene > 1) { // "Simulate" the drawing of the credits so that dedicated mode doesn't get stuck fixed_t y = (80< vid.height) break; } } // Do this here rather than in the drawer you doofus! (this is why dedicated mode broke at credits) if (!credits[i] && y <= 120< 0) ? 1 : 0)) #define absmod(x,y) ((abs(x) % y) * sign(x)) // Repeats horizontally until it reaches across the screen. static void F_DrawBannerPatch(fixed_t x, fixed_t y, fixed_t scale, INT32 scrn, patch_t *patch, const UINT8 *colormap) { fixed_t w = (vid.width / vid.dupx); fixed_t vidxdiff = ((w - BASEVIDWIDTH) << FRACBITS) / 2; fixed_t pw = (static_cast(SHORT(patch->width)) * scale); fixed_t xx = ((-vidxdiff + absmod(x * FRACUNIT, pw)) - (pw << 1)); while(xx < (w << FRACBITS)) { V_DrawFixedPatch(xx, y<numframes) // No frames ?? return false; // Can't render! frame %= sprdef->numframes; sprframe = &sprdef->spriteframes[frame]; sprpatch = static_cast(W_CachePatchNum(sprframe->lumppat[rotation], PU_CACHE)); if (sprframe->flip & (1<(dscredits.creditsobj[i].color), GTC_MENUCACHE); /*sprintf(debug, "xspd: %f\nstopat: %d\nspinout: %d\n", FIXED_TO_FLOAT(CreditsScrollFx((elapsed_diff), dscredits.creditsobj[i].speed[0])), dscredits.creditsobj[i].movestop, static_cast(dscredits.creditsobj[i].spinout));*/ M_DrawCharacterSprite(dscredits.creditsobj[i].position[0] >> FRACBITS, dscredits.creditsobj[i].position[1] >> FRACBITS, dscredits.creditsobj[i].scale, 4, dscredits.creditsobj[i].skin, dscredits.creditsobj[i].spinout ? SPR2_SPIN : SPR2_FSTN, dscredits.creditsobj[i].spinout ? (((ticker * 5)/8) & 7) : 2, ticker + i, 0, colormap); /*V_DrawThinString((dscredits.creditsobj[i].position[0] >> FRACBITS) - 128, (dscredits.creditsobj[i].position[1] >> FRACBITS) - 96, V_ALLOWLOWERCASE, debug);*/ } else if (dscredits.creditsobj[i].patch) { V_DrawFixedPatch((dscredits.creditsobj[i].position[0] >> FRACBITS) << FRACBITS, (dscredits.creditsobj[i].position[1] >> FRACBITS) << FRACBITS, dscredits.creditsobj[i].scale, 0, dscredits.creditsobj[i].patch, NULL); } } } static void F_DrawCreditsArea(fixed_t y_root, fixed_t height, fixed_t xdiff) { fixed_t y, sky_y, ground_y; y = y_root; // Store the ground area y -= static_cast(SHORT(dsground->height) >> 1); ground_y = sky_y = y; sky_y -= static_cast(SHORT(dssky->height) >> 1); // Draw the sky F_DrawBannerPatch(scene_scroll[0] >> 1, sky_y, FRACUNIT >> 1, 0, dssky, NULL); // Draw Tyler if (tylermemoriam) { V_DrawPatch(-xdiff - SHORT(tylergag->width) + ((tylerscroll * 3) >> 1), ground_y - (height >> 1), V_TRANSLUCENT, tylergag); } // Draw the game objects (buildings, players) F_DrawCreditsObjects(); // Draw the ground and grass F_DrawBannerPatch(scene_scroll[0], ground_y, FRACUNIT >> 1, 0, dsground, NULL); ground_y -= static_cast(SHORT(dsgrass->height) >> 1); F_DrawBannerPatch(scene_scroll[0], ground_y, FRACUNIT >> 1, 0, dsgrass, NULL); } // Not really "fake" considering this is the actual title sequence. static void F_FakeTitleScreen(tic_t count) { INT32 w, h; w = (vid.width / vid.dupx); h = (vid.height / vid.dupy); const INT32 vidxdiff = (w - BASEVIDWIDTH) / 2; const INT32 vidydiff = (h - BASEVIDHEIGHT) / 2; if (count < 50) { V_DrawSmallScaledPatch(84, 36, 0, ttbanner); if (count >= 20) V_DrawSmallScaledPatch(84, 87, 0, ttkart); else if (count >= 10) V_DrawSciencePatch((84<>1; if (transval) { V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, ((10 - transval) << V_ALPHASHIFT)|0); } V_DrawSmallScaledPatch(84, 36, 0, ttbanner); V_DrawSmallScaledPatch(84, 87, 0, ttkart); if (!transval) return; V_DrawSmallScaledPatch(84, 36, transval< 2) && (dscredits.creditsscene < 19)) { bgcolor = FULLCREDITSBGCOLOR; } V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, bgcolor); if (dscredits.creditsscene < 2) { V_DrawCenteredStringAtFixed(160<(10), std::max(static_cast(0), elapsed - creditstimings[0]) / 150000); // 1.5 seconds for fadeout } else if (dscredits.creditsscene == 3) { alpha = 10 - std::min(static_cast(10), std::max(static_cast(0), elapsed - creditstimings[2]) / 200000); // 2.0 seconds for fadein } else if (dscredits.creditsscene >= 17) { alpha = std::min(static_cast(10), std::max(static_cast(0), elapsed - creditstimings[16]) / 200000); // 2.0 seconds for fadeout titlealpha = std::min(static_cast(10), std::max(static_cast(0), elapsed - creditstimings[16]) / 100000); // 1 second for banner fadein } if (dscredits.creditsscene >= 20) { titlefade = std::min(static_cast(10), std::max(static_cast(0), elapsed - creditstimings[19]) / 400000); // 4.0 seconds for fadeout } if ((dscredits.creditsscene > 2) && (dscredits.creditsscene < 19)) { F_DrawCreditsArea(static_cast(-vidydiff + h), static_cast(h), static_cast(vidxdiff)); } if (alpha) { V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, ((10 - alpha) << V_ALPHASHIFT)|31); } if ((titlealpha) && (dscredits.creditsscene < 18)) { V_DrawSmallScaledPatch(84, 36, ((10 - titlealpha) << V_ALPHASHIFT), ttbanner); } else if (dscredits.creditsscene >= 18) { tic_t fakefinalecount = static_cast(std::max(static_cast(0), elapsed - creditstimings[17]) / MICROSEC_TICS); F_FakeTitleScreen(fakefinalecount); if (titlefade) { V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, ((10 - titlefade) << V_ALPHASHIFT)|31); } } INT32 y, i; // The credits text overlays everything. if ((dscredits.creditsscene > 1) && (dscredits.creditsscene < 8)) { y = ((BASEVIDHEIGHT + vidydiff) << FRACBITS) - FixedMul(kartcredits_height + (h << FRACBITS), std::min(std::max(kartcredits_start, elapsed) - kartcredits_start, kartcredits_end) / kartcredits_dividend); // Draw credits text on top for (i = 0; credits[i]; i++) { switch(credits[i][0]) { case 0: y += 50<>FRACBITS > -(10 + vidydiff)) V_DrawString(8 - vidxdiff, y >> FRACBITS, V_ORANGEMAP, &credits[i][1]); y += 16<>FRACBITS > -(10 + vidydiff)) V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, &credits[i][1]); y += 20<>FRACBITS > -(10 + vidydiff)) V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, credits[i]); y += 20<>FRACBITS) * vid.dupy) > vid.height) break; } } else if (dscredits.creditsscene > 7) { y = ((BASEVIDHEIGHT + vidydiff) << FRACBITS) - FixedMul(blancredits_height + (h << FRACBITS), std::min(std::max(blancredits_start, elapsed) - blancredits_start, blancredits_end) / blancredits_dividend); // Draw credits text on top for (i = 0; blancredits[i]; i++) { switch(blancredits[i][0]) { case 0: y += 50<>FRACBITS > -(10 + vidydiff)) V_DrawString(8 - vidxdiff, y >> FRACBITS, V_ORANGEMAP, &blancredits[i][1]); y += 16<>FRACBITS > -(10 + vidydiff)) V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, blancredits[i]); y += 20<>FRACBITS) * vid.dupy) > vid.height) break; } } /*char debug[256]; sprintf(debug,"Tics: %d\n",static_cast(elapsed / MICROSEC_TICS)); V_DrawRightAlignedThinString(-vidxdiff + w, -vidydiff, V_ALLOWLOWERCASE, debug);*/ } #undef CREDSKINCOUNT }