// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Vivian "toastergrl" Grannell.
// Copyright (C) 2025 by Kart Krew.
// Copyright (C) 2026 by "yama".
// Copyright (C) 2026 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 <chrono>

#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 "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<fixed_t>((static_cast<INT64>(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<fixed_t>(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, <Sryder> 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<patch_t*>(W_CachePatchName("TTKBANNR", PU_PATCH_LOWPRIORITY));
		ttkart = static_cast<patch_t*>(W_CachePatchName("TTKART", PU_PATCH_LOWPRIORITY));
		ttcheckers = static_cast<patch_t*>(W_CachePatchName("TTCHECK", PU_PATCH_LOWPRIORITY));
		ttkflash = static_cast<patch_t*>(W_CachePatchName("TTKFLASH", PU_PATCH_LOWPRIORITY));

        // Scenery patches
        dsground = static_cast<patch_t*>(W_CachePatchName("EXCDGND", PU_PATCH_LOWPRIORITY));
        dsgrass = static_cast<patch_t*>(W_CachePatchName("EXCDGRS", PU_PATCH_LOWPRIORITY));
        dssky = static_cast<patch_t*>(W_CachePatchName("EXCDSKY", PU_PATCH_LOWPRIORITY));
        tylergag = static_cast<patch_t*>(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<patch_t*>(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<<FRACBITS;
                break;
            case 1:
                kartcredits_height += 16<<FRACBITS;
                break;
            default:
                kartcredits_height += 20<<FRACBITS;
                break;
            }
        }

        for (i = 0; blancredits[i]; i++)
        {
            switch(blancredits[i][0])
            {
            case 0:
                blancredits_height += 50<<FRACBITS;
                break;
            case 1:
                blancredits_height += 16<<FRACBITS;
                break;
            default:
                blancredits_height += 20<<FRACBITS;
                break;
            }
        }
    }

    dscreditsticks++;

    INT32 w = vid.scaledwidth;
    INT32 h = (vid.height / vid.dup);

    const INT32 vidxdiff = (w - BASEVIDWIDTH) / 2;
    const INT32 vidydiff = (h - BASEVIDHEIGHT) / 2;

    if (dscreditsticks == CREDITSDELAY)
    {
        // Roll credits!
        dscredits_running = true;

        F_SpawnCreditsPlayer(player1_idx + MAXMISCOBJS,
                             (-vidxdiff) + w - 77,
                             (-vidydiff) + h - (SHORT(dsground->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<double> fp_ms = curr - start;

    // This is fucking stupid.
    lastelapsed = elapsed;
    elapsed = static_cast<longtic_t>(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<fixed_t>(FRACUNIT),
                                         static_cast<fixed_t>(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<longtic_t>(std::max(static_cast<INT64>(0), static_cast<INT64>(building_delay) - static_cast<INT64>(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<longtic_t>(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<longtic_t>(std::max(static_cast<INT64>(0), static_cast<INT64>(player_delay) - static_cast<INT64>(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<longtic_t>(M_RandomRange(8, 12)) * 250000;
                }
                else
                {
                    player_delay = static_cast<longtic_t>(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<longtic_t>(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<INT64>(0),
                        static_cast<INT64>(dscredits.creditsobj[rival_idx + MAXMISCOBJS].movestop) -
                            static_cast<INT64>(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<INT64>(0),
                        static_cast<INT64>(dscredits.creditsobj[i].movestop) -
                            static_cast<INT64>(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<INT64>(0),
                        static_cast<INT64>(dscredits.creditsobj[i].movestart) -
                            static_cast<INT64>(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<<FRACBITS) - 5*(animtimer<<FRACBITS)/8;

        // Draw credits text on top
        for (i = 0; credits[i]; i++)
        {
            switch(credits[i][0])
            {
                case 0: y += 80<<FRACBITS; break;
                case 1: y += 30<<FRACBITS; break;
                default: y += 12<<FRACBITS; break;
            }
            if (FixedMul(y,vid.dup) > 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<<FRACBITS && !finalecount)
	{
		timetonext = 5*TICRATE+1;
		finalecount = 5*TICRATE;
	}

	if (timetonext)
		timetonext--;
	else
		animtimer++;

	if (finalecount && --finalecount == 0)
		F_StartGameEvaluation();*/
}

#define sign(x) ((x < 0) ? -1 : ((x > 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.scaledwidth;
    fixed_t vidxdiff = ((w - BASEVIDWIDTH) << FRACBITS) / 2;
    fixed_t pw = (static_cast<fixed_t>(SHORT(patch->width)) * scale);

    fixed_t xx = ((-vidxdiff + absmod(x * FRACUNIT, pw)) - (pw << 1));
    
    while(xx < (w << FRACBITS))
    {
        V_DrawFixedPatch(xx, y<<FRACBITS, scale, scrn, patch, colormap);
        xx += pw;
    }
}

#undef sign

// returns false if the character couldn't be rendered
static boolean M_DrawCharacterSprite(INT16 x, INT16 y, fixed_t scale, UINT8 fade, INT16 skin, UINT8 spr2, UINT8 rotation, UINT32 frame, INT32 addflags, UINT8 *colormap)
{
	UINT8 spr;
	spritedef_t *sprdef;
	spriteframe_t *sprframe;
	patch_t *sprpatch;

	spr = P_GetSkinSprite2(&skins[skin], spr2, NULL);
	sprdef = &skins[skin].sprites[spr];

    UINT8 *fademap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE);
    INT16 fadealpha = 10 - fade;

	if (!sprdef->numframes) // No frames ??
		return false; // Can't render!

	frame %= sprdef->numframes;

	sprframe = &sprdef->spriteframes[frame];
	sprpatch = static_cast<patch_t*>(W_CachePatchNum(sprframe->lumppat[rotation], PU_CACHE));

	if (sprframe->flip & (1<<rotation)) // Only for first sprite
	{
		addflags ^= V_FLIP; // This sprite is left/right flipped!
	}

	if ((skins[skin].highresscale != FRACUNIT)||(scale != FRACUNIT))
	{
		V_DrawFixedPatch(x<<FRACBITS,
					y<<FRACBITS,
					FixedMul(scale, skins[skin].highresscale),
					addflags, sprpatch, colormap);

        if (fadealpha < 10)
        {
            V_DrawFixedPatch(x<<FRACBITS,
					y<<FRACBITS,
					FixedMul(scale, skins[skin].highresscale),
					addflags|(fadealpha << V_ALPHASHIFT), sprpatch, fademap);
        }
	}
	else
    {
		V_DrawMappedPatch(x, y, addflags, sprpatch, colormap);

        if (fadealpha < 10)
        {
            V_DrawMappedPatch(x, y, addflags|(fadealpha << V_ALPHASHIFT), sprpatch, fademap);
        }
    }

	return true;
}


static void F_DrawCreditsObjects()
{
    INT32 ticker = FixedMul(TICRATE, UsecToFixed(elapsed));

    INT32 i;
    for (i = 0; i < MAXDSOBJECTS + DSUNIQUEPLAYERS; i++)
    {
        if (!dscredits.creditsobj[i].active)
        {
            continue;
        }

        if (dscredits.creditsobj[i].skin != MAXSKINS)
        {
            // Player object.
            //char debug[256];
            UINT8 *colormap = R_GetTranslationColormap(dscredits.creditsobj[i].skin,
                                                       static_cast<skincolornum_t>(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<UINT8>(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<fixed_t>(SHORT(dsground->height) >> 1);
    ground_y = sky_y = y;

    sky_y -= static_cast<fixed_t>(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<fixed_t>(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.scaledwidth;
    h = (vid.height / vid.dup);

    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<<FRACBITS) - 18*(((20 - count)<<FRACBITS) - R_GetTimeFrac(RTF_MENU)), 87<<FRACBITS, 0, ttkart, FRACUNIT/2);
	}
	else if (count < 52)
	{
		V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, 0);
		V_DrawSmallScaledPatch(84, 36, 0, ttkflash);
	}
	else
	{
		INT32 transval = 0;

		if (count <= (50+(9<<1)))
			transval = (count - 50)>>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<<V_ALPHASHIFT, ttkflash);
	}
}

void F_SecretCreditsDrawer(void)
{
    INT32 alpha = 0, titlealpha = 0, titlefade = 0;
    INT32 w, h;
    UINT8 bgcolor = 31;

    w = vid.scaledwidth;
    h = (vid.height / vid.dup);

    const INT32 vidxdiff = (w - BASEVIDWIDTH) / 2;
    const INT32 vidydiff = (h - BASEVIDHEIGHT) / 2;

    if (!dscredits_running)
    {
        V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, bgcolor);
        return;
    }

    if ((dscredits.creditsscene > 2) && (dscredits.creditsscene < 19))
    {
        bgcolor = FULLCREDITSBGCOLOR;
    }

    V_DrawFill(-vidxdiff - 2, -vidydiff - 2, w + 2, h + 2, bgcolor);

    if (dscredits.creditsscene < 2)
    {
        V_DrawCenteredStringAtFixed(160<<FRACBITS, 100<<FRACBITS, 0, gamettl);
    }

    if (dscredits.creditsscene == 1)
    {
        alpha = std::min(static_cast<longtic_t>(10), std::max(static_cast<longtic_t>(0), elapsed - creditstimings[0]) / 150000); // 1.5 seconds for fadeout 
    }
    else if (dscredits.creditsscene == 3)
    {
        alpha = 10 - std::min(static_cast<longtic_t>(10), std::max(static_cast<longtic_t>(0), elapsed - creditstimings[2]) / 200000); // 2.0 seconds for fadein 
    }
    else if (dscredits.creditsscene >= 17)
    {
        alpha = std::min(static_cast<longtic_t>(10), std::max(static_cast<longtic_t>(0), elapsed - creditstimings[16]) / 200000); // 2.0 seconds for fadeout
        titlealpha = std::min(static_cast<longtic_t>(10), std::max(static_cast<longtic_t>(0), elapsed - creditstimings[16]) / 100000); // 1 second for banner fadein
    }

    if (dscredits.creditsscene >= 20)
    {
        titlefade = std::min(static_cast<longtic_t>(10), std::max(static_cast<longtic_t>(0), elapsed - creditstimings[19]) / 400000); // 4.0 seconds for fadeout 
    }
    
    if ((dscredits.creditsscene > 2) && (dscredits.creditsscene < 19))
    {
        F_DrawCreditsArea(static_cast<fixed_t>(-vidydiff + h), static_cast<fixed_t>(h), static_cast<fixed_t>(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<tic_t>(std::max(static_cast<longtic_t>(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;
                break;
            case 1:
                if (y>>FRACBITS > -(10 + vidydiff))
                    V_DrawString(8 - vidxdiff, y >> FRACBITS, V_ORANGEMAP, &credits[i][1]);
                y += 16<<FRACBITS;
                break;
            case 2:
                // Tyler spawn
                tylermemoriam = 1;
                if (y>>FRACBITS > -(10 + vidydiff))
                    V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, &credits[i][1]);
                y += 20<<FRACBITS;
                break;
            default:
                if (y>>FRACBITS > -(10 + vidydiff))
                    V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, credits[i]);
                y += 20<<FRACBITS;
                break;
            }
            if (((y>>FRACBITS) * vid.dup) > 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;
                break;
            case 1:
                if (y>>FRACBITS > -(10 + vidydiff))
                    V_DrawString(8 - vidxdiff, y >> FRACBITS, V_ORANGEMAP, &blancredits[i][1]);
                y += 16<<FRACBITS;
                break;
            default:
                if (y>>FRACBITS > -(10 + vidydiff))
                    V_DrawRightAlignedThinString(248 - vidxdiff, y >> FRACBITS, V_ALLOWLOWERCASE, blancredits[i]);
                y += 20<<FRACBITS;
                break;
            }
            if (((y>>FRACBITS) * vid.dup) > vid.height)
                break;
        }
    }

    /*char debug[256];

    sprintf(debug,"Tics: %d\n",static_cast<tic_t>(elapsed / MICROSEC_TICS));

    V_DrawRightAlignedThinString(-vidxdiff + w, -vidydiff, V_ALLOWLOWERCASE, debug);*/
}

#undef CREDSKINCOUNT

}
