// SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- // Copyright (C) 1993-1996 by id Software, Inc. // Copyright (C) 1998-2000 by DooM Legacy Team. // Copyright (C) 1999-2020 by Sonic Team Junior. // // 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 f_finale.c /// \brief Title screen, intro, game evaluation, and credits. #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 "lua_hud.h" #include "lua_hook.h" // SRB2Kart #include "k_menu.h" #include "k_grandprix.h" #include "music.h" // Stage of animation: // 0 = text, 1 = art screen INT32 finalecount; INT32 titlescrollxspeed = 16; INT32 titlescrollyspeed = 0; 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; static boolean keypressed = false; static INT32 menuanimtimer; // Title screen: background animation timing altview_t titlemapcam = {0}; // menu presentation state char curbgname[9]; SINT8 curfadevalue; INT32 curbgcolor = -1; // Please stop assaulting my eyes. INT32 curbgxspeed; INT32 curbgyspeed; boolean curbghide; boolean hidetitlemap; // WARNING: set to false by M_SetupNextMenu and M_ClearMenus #if 0 static UINT8 laststaff = 0; #endif //static UINT8 curDemo = 0; static UINT32 demoDelayLeft; static UINT32 demoIdleLeft; // customizable title screen graphics ttmode_enum ttmode = TTMODE_RINGRACERS; UINT8 ttscale = 1; // FRACUNIT / ttscale // ttmode user vars char ttname[9]; INT16 ttx = 0; INT16 tty = 0; INT16 ttloop = -1; UINT16 tttics = 1; boolean curhidepics; ttmode_enum curttmode; UINT8 curttscale; // ttmode user vars char curttname[9]; INT16 curttx; INT16 curtty; INT16 curttloop; UINT16 curtttics; // ttmode old /* 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 */ static patch_t *kts_bumper; // DR ROBOTNIKS RING RACERS static patch_t *kts_eggman; // dr. robotnik himself static patch_t *kts_tails; // tails himself static patch_t *kts_tails_tails; // tails' tails static patch_t *kts_electricity[6]; // ring o' electricity static patch_t *kts_copyright; // (C) SEGA #define NOWAY #ifdef NOWAY static patch_t *driver[2]; // Driving character on the waiting screen static UINT8 *waitcolormap; // colormap for the spinning character #endif // ttmode user static patch_t *ttuser[TTMAX_USER]; static INT32 ttuser_count = 0; // // PROMPT STATE // boolean promptactive = false; static mobj_t *promptmo; static INT16 promptpostexectag; static boolean promptblockcontrols; static char *promptpagetext = NULL; static INT32 callpromptnum = INT32_MAX; static INT32 callpagenum = INT32_MAX; static INT32 callplayer = INT32_MAX; // // CUTSCENE TEXT WRITING // static const char *cutscene_basetext = NULL; static char cutscene_disptext[1024]; static INT32 cutscene_baseptr = 0; static INT32 cutscene_writeptr = 0; static INT32 cutscene_textcount = 0; static INT32 cutscene_textspeed = 0; static UINT8 cutscene_boostspeed = 0; static tic_t cutscene_lasttextwrite = 0; // STJR Intro char stjrintro[9] = "STJRI000"; static huddrawlist_h luahuddrawlist_title; // // This alters the text string cutscene_disptext. // Use the typical string drawing functions to display it. // Returns 0 if \0 is reached (end of input) // static UINT8 F_WriteText(void) { INT32 numtowrite = 1; const char *c; tic_t ltw = I_GetTime(); if (cutscene_lasttextwrite == ltw) return 1; // singletics prevention cutscene_lasttextwrite = ltw; if (cutscene_boostspeed) { // for custom cutscene speedup mode numtowrite = 8; } else { // Don't draw any characters if the count was 1 or more when we started if (--cutscene_textcount >= 0) return 1; if (cutscene_textspeed < 7) numtowrite = 8 - cutscene_textspeed; } for (;numtowrite > 0;++cutscene_baseptr) { c = &cutscene_basetext[cutscene_baseptr]; if (!c || !*c || *c=='#') return 0; // \xA0 - \xAF = change text speed if ((UINT8)*c >= 0xA0 && (UINT8)*c <= 0xAF) { cutscene_textspeed = (INT32)((UINT8)*c - 0xA0); continue; } // \xB0 - \xD2 = delay character for up to one second (35 tics) else if ((UINT8)*c >= 0xB0 && (UINT8)*c <= (0xB0+TICRATE-1)) { cutscene_textcount = (INT32)((UINT8)*c - 0xAF); numtowrite = 0; continue; } cutscene_disptext[cutscene_writeptr++] = *c; // Ignore other control codes (color) if ((UINT8)*c < 0x80) --numtowrite; } // Reset textcount for next tic based on speed // if it wasn't already set by a delay. if (cutscene_textcount < 0) { cutscene_textcount = 0; if (cutscene_textspeed > 7) cutscene_textcount = cutscene_textspeed - 7; } return 1; } static void F_NewCutscene(const char *basetext) { cutscene_basetext = basetext; memset(cutscene_disptext,0,sizeof(cutscene_disptext)); cutscene_writeptr = cutscene_baseptr = 0; cutscene_textspeed = 9; cutscene_textcount = TICRATE/2; } // // F_TitleBGScroll // /* static void F_TitleBGScroll(INT32 scrollspeed) { INT32 x, y, w; patch_t *pat, *pat2; INT32 anim2 = 0; pat = W_CachePatchName("TITLEBG1", PU_CACHE); pat2 = W_CachePatchName("TITLEBG2", PU_CACHE); w = vid.width / vid.dupx; animtimer = ((finalecount*scrollspeed)/16) % SHORT(pat->width); anim2 = SHORT(pat2->width) - (((finalecount*scrollspeed)/16) % SHORT(pat2->width)); // SRB2Kart: F_DrawPatchCol is over-engineered; recoded to be less shitty and error-prone if (rendermode != render_none) { V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 0); x = -((INT32)animtimer); y = 0; while (x < w) { V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, V_SNAPTOTOP|V_SNAPTOLEFT, pat, NULL); x += SHORT(pat->width); } x = -anim2; y = BASEVIDHEIGHT - SHORT(pat2->height); while (x < w) { V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, pat2, NULL); x += SHORT(pat2->width); } } W_UnlockCachedPatch(pat); W_UnlockCachedPatch(pat2); } */ // ============= // INTRO SCENE // ============= #define NUMINTROSCENES 1 INT32 intro_scenenum = 0; INT32 intro_curtime = 0; const char *introtext[NUMINTROSCENES]; static tic_t introscenetime[NUMINTROSCENES] = { 4*TICRATE, // KART KR(eW }; // custom intros void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean resetplayer); void F_StartIntro(void) { if (gamestate) { F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); F_RunWipe(wipe_intro_toblack, wipedefs[wipe_intro_toblack], false, "FADEMAP0", false, false); } Music_StopAll(); S_StopSounds(); if (introtoplay) { if (!cutscenes[introtoplay - 1]) D_StartTitle(); else F_StartCustomCutscene(introtoplay - 1, false, false); return; } introtext[0] = " #"; G_SetGamestate(GS_INTRO); gameaction = ga_nothing; paused = false; CON_ToggleOff(); F_NewCutscene(introtext[0]); intro_scenenum = 0; finalecount = animtimer = skullAnimCounter = stoptimer = 0; timetonext = introscenetime[intro_scenenum]; Music_StopAll(); } // // F_IntroDrawScene // static void F_IntroDrawScene(void) { boolean highres = true; INT32 cx = 8, cy = 128; patch_t *background = NULL; INT32 bgxoffs = 0; // DRAW A FULL PIC INSTEAD OF FLAT! if (intro_scenenum == 0) { background = W_CachePatchName("KARTKREW", PU_CACHE); highres = true; } V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 0); if (background) { if (highres) V_DrawSmallScaledPatch(bgxoffs, 0, 0, background); else V_DrawScaledPatch(bgxoffs, 0, 0, background); } W_UnlockCachedPatch(background); V_DrawString(cx, cy, 0, cutscene_disptext); } // // F_IntroDrawer // void F_IntroDrawer(void) { // Used to be this whole thing, but now... F_IntroDrawScene(); } // // F_IntroTicker // void F_IntroTicker(void) { // advance animation finalecount++; timetonext--; if (intro_scenenum == 0) { if (timetonext <= 0) { #if 0 // The necessary apparatus for constructing more elaborate intros... intro_scenenum++; F_NewCutscene(introtext[intro_scenenum]); timetonext = introscenetime[intro_scenenum]; wipegamestate = -1; animtimer = stoptimer = 0; #endif if (rendermode != render_none) { F_WipeStartScreen(); F_WipeColorFill(31); F_WipeEndScreen(); F_RunWipe(wipe_intro_toblack, 99, true, "FADEMAP0", false, false); } // Stay on black for a bit. =) { tic_t nowtime, quittime, lasttime; nowtime = lasttime = I_GetTime(); quittime = nowtime + NEWTICRATE*2; // Shortened the quit time, used to be 2 seconds while (quittime > nowtime) { while (!((nowtime = I_GetTime()) - lasttime)) { I_Sleep(cv_sleep.value); I_UpdateTime(cv_timescale.value); } lasttime = nowtime; I_OsPolling(); I_UpdateNoBlit(); #ifdef HAVE_THREADS I_lock_mutex(&k_menu_mutex); #endif M_Drawer(); // menu is drawn even on top of wipes #ifdef HAVE_THREADS I_unlock_mutex(k_menu_mutex); #endif I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001 #ifdef HWRENDER if (moviemode && rendermode == render_opengl) // make sure we save frames for the white hold too M_LegacySaveFrame(); else #endif if (moviemode && rendermode != render_none) I_CaptureVideoFrame(); } } D_StartTitle(); return; } if (finalecount == 8) S_StartSound(NULL, sfx_vroom); else if (finalecount == 47) { // Need to use M_Random otherwise it always uses the same sound UINT32 rskin = R_GetLocalRandomSkin(); UINT8 rtaunt = M_RandomKey(2); sfxenum_t rsound = skins[rskin].soundsid[SKSKBST1+rtaunt]; S_StartSound(NULL, rsound); } } intro_curtime = introscenetime[intro_scenenum] - timetonext; F_WriteText(); // check for skipping if (keypressed) keypressed = false; if (animtimer > 0) animtimer--; } // // F_IntroResponder // boolean F_IntroResponder(event_t *event) { INT32 key = event->data1; // remap virtual keys (mouse & joystick buttons) if (event->type == ev_gamepad_axis && key >= JOYANALOGS && (abs(event->data2) > JOYAXISRANGE/2 || abs(event->data3) > JOYAXISRANGE/2)) { key = KEY_ENTER; } else { switch (key) { case KEY_MOUSE1: key = KEY_ENTER; break; case KEY_MOUSE1 + 1: key = KEY_BACKSPACE; break; case KEY_JOY1: case KEY_JOY1 + 2: key = KEY_ENTER; break; case KEY_JOY1 + 3: key = 'n'; break; case KEY_JOY1 + 1: key = KEY_BACKSPACE; break; case KEY_HAT1: key = KEY_UPARROW; break; case KEY_HAT1 + 1: key = KEY_DOWNARROW; break; case KEY_HAT1 + 2: key = KEY_LEFTARROW; break; case KEY_HAT1 + 3: key = KEY_RIGHTARROW; break; } if (event->type != ev_keydown && key != 301) return false; } if (key != 27 && key != KEY_ENTER && key != KEY_SPACE && key != KEY_BACKSPACE) return false; if (keypressed) return false; keypressed = true; 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 // ============ typedef enum { EVAL_NOTHING, EVAL_CHAOS, EVAL_SUPER, EVAL_PERFECT, EVAL_MAX } evaluationtype_t; #define EVALLEN_NORMAL (14*TICRATE) #define EVALLEN_HALFWAY (EVALLEN_NORMAL/2) // tyron made something perfect and i would sooner // smite everyone in this room starting with myself // over the idea of cutting it ~toast 250623 #define EVALLEN_PERFECT (18*TICRATE) static evaluationtype_t evaluationtype; UINT16 finaleemeralds = 0; void F_StartGameEvaluation(void) { Music_DelayEnd("credits", TICRATE/4); Music_Tick(); // it needs to fade out right now S_StopMusicCredit(); // Credits option in extras menu if ( grandprixinfo.gp == false #ifdef DEVELOP && cv_soundtest.value == 0 #endif ) { F_StartGameEnd(); return; } G_SetGamestate(GS_EVALUATION); // Just in case they're open ... somehow M_ClearMenus(true); UINT8 difficulty = KARTSPEED_NORMAL; if (grandprixinfo.gp == true) { if (grandprixinfo.masterbots == true) difficulty = KARTGP_MASTER; else difficulty = grandprixinfo.gamespeed; } finaleemeralds = M_CheckCupEmeralds(difficulty); #ifdef DEVELOP if (cv_soundtest.value != 0) evaluationtype = (cv_soundtest.value-1) % EVAL_MAX; else #endif if (difficulty == KARTSPEED_EASY) evaluationtype = EVAL_NOTHING; else if (!ALLCHAOSEMERALDS(finaleemeralds)) evaluationtype = EVAL_CHAOS; else if (!ALLSUPEREMERALDS(finaleemeralds)) evaluationtype = EVAL_SUPER; else evaluationtype = EVAL_PERFECT; timetonext = (evaluationtype == EVAL_PERFECT) ? EVALLEN_PERFECT : EVALLEN_NORMAL; gameaction = ga_nothing; paused = false; CON_ToggleOff(); keypressed = false; finalecount = -1; } void F_GameEvaluationDrawer(void) { INT32 x, y, i; angle_t fa; INT32 eemeralds_cur; const char *endingtext = NULL, *rankharder = NULL; if (marathonmode) { endingtext = "COOL RUN!"; } else switch (evaluationtype) { case EVAL_PERFECT: endingtext = "CONGRATULATIONS"; rankharder = "You're too cool!"; break; case EVAL_SUPER: rankharder = "Further challenge awaits!"; break; default: rankharder = "...push your rank harder"; break; case EVAL_NOTHING: rankharder = "Brave a higher difficulty"; break; } if (endingtext == NULL) endingtext = "TRY AGAIN..."; if (usedCheats) rankharder = "Cheated games can't unlock extras!"; V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); const INT32 gainaxtime = ((3*TICRATE)/2) - finalecount; const INT32 sealtime = finalecount - (4*TICRATE); INT32 crossfade = 0; // Draw all the good crap here. x = BASEVIDWIDTH<<(FRACBITS-1); y = (BASEVIDHEIGHT + 16)<<(FRACBITS-1); if (!useSeal) ; else if (evaluationtype == EVAL_PERFECT) { // Symmetrical slow fade in and out. if (finalecount > EVALLEN_PERFECT/2) crossfade = EVALLEN_PERFECT - finalecount; else crossfade = finalecount; crossfade = 10 - (crossfade * 10)/TICRATE; if (crossfade < 0) crossfade = 0; // Imagery of a shattered pink palanquin resting in the flowers // (abandoned cage for a gemstone far above Earth's station) // ~toast 240623 if (crossfade != 10) { V_DrawFixedPatch( x, y, FRACUNIT, crossfade< 0) { // Stage 1 - blank sealpat = W_CachePatchName( "K_FINB01", PU_PATCH_LOWPRIORITY ); } else if (sealtime < 0) { // Stage 2 - Catcher Glow sealpat = W_CachePatchName( va("K_FINB0%u", 2+(finalecount & 1)), PU_PATCH_LOWPRIORITY ); crossfade = 10 + sealtime/3; } else { // Stage 3 - Star Within The Seal sealpat = W_CachePatchName( "K_FINB05", PU_PATCH_LOWPRIORITY ); #define SEAL_PULSELEN (TICRATE) crossfade = (sealtime % (2*SEAL_PULSELEN)) - SEAL_PULSELEN; if (crossfade < 0) crossfade = -crossfade; crossfade = (crossfade * 10)/SEAL_PULSELEN; #undef SEAL_PULSELEN } if (crossfade != 10) { V_DrawFixedPatch( x, y, FRACUNIT, 0, sealpat, NULL ); } if (crossfade > 0) { sealpat = W_CachePatchName( "K_FINB04", PU_PATCH_LOWPRIORITY ); V_DrawFixedPatch( x, y, FRACUNIT, (10-crossfade)<numframes - 2); if (refframes < 0) ; // Not enough sprites else if (gainaxtime <= refframes) { // Animation in progress! INT32 gainaxframe; if (gainaxtime <= 0) { // Flicker gainaxframe = refframes + (finalecount & 1); } else { // Shwing in gainaxframe = (sprdef->numframes - 2) - gainaxtime; } spriteframe_t *sprframe = &sprdef->spriteframes[gainaxframe]; if (sprframe->lumppat[0] != LUMPERROR) { V_DrawFixedPatch( x, (y - (20*FRACUNIT)), FRACUNIT/2, V_ADD |(crossfade<flip & 1) ? V_FLIP : 0), W_CachePatchNum(sprframe->lumppat[0], PU_CACHE), NULL ); } } } } if ((evaluationtype == EVAL_CHAOS || evaluationtype == EVAL_SUPER) && finalecount > 0) { INT32 gemtrans; if (useSeal && sealtime > 0) { // Stage 3 - aggressive overexposure gemtrans = 3 + ((10 - crossfade)/3); } else if (useSeal && crossfade > 0) { // Stage 2 - some overexposure gemtrans = (crossfade/3); } else { // Stage 1 - initial fade in gemtrans = max(10-finalecount, 0); } gemtrans <<= V_ALPHASHIFT; eemeralds_cur = (finalecount % 360)<>ANGLETOFINESHIFT) & FINEMASK; V_DrawFixedPatch( x + (75*FINECOSINE(fa)), y + (75*FINESINE(fa)), FRACUNIT, gemtrans, empat, R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_CHAOSEMERALD1+i, GTC_CACHE) ); } } V_DrawCenteredGamemodeString( BASEVIDWIDTH/2, 15, 0, NULL, endingtext ); if (rankharder != NULL) { x = (BASEVIDWIDTH<<(FRACBITS-1)) - \ V_StringScaledWidth( FRACUNIT, FRACUNIT, FRACUNIT, 0, KART_FONT, rankharder )/2; V_DrawStringScaled( x, (BASEVIDHEIGHT - 15 - 14) * FRACUNIT, FRACUNIT, FRACUNIT, FRACUNIT, 0, NULL, KART_FONT, rankharder ); } if (marathonmode) { const char *rtatext, *cuttext; rtatext = (marathonmode & MA_INGAME) ? "In-game timer" : "RTA timer"; cuttext = (marathonmode & MA_NOCUTSCENES) ? "" : " w/ cutscenes"; endingtext = va("%s, %s%s", skins[players[consoleplayer].skin].realname, rtatext, cuttext); V_DrawCenteredString(BASEVIDWIDTH/2, 182, V_SNAPTOBOTTOM|(ultimatemode ? V_REDMAP : V_YELLOWMAP), endingtext); } Y_DrawIntermissionButton(EVALLEN_HALFWAY + TICRATE - finalecount, (finalecount + TICRATE) - timetonext); } void F_GameEvaluationTicker(void) { if (evaluationtype == EVAL_PERFECT) { if (finalecount == 1) { // sitting on that distant _shore Music_Remap("shore", "_SHORE"); Music_Play("shore"); } } else { if (finalecount == TICRATE/2) { // _drift across open waters Music_Remap("shore", "_DRIFT"); Music_Play("shore"); } } if (++finalecount == timetonext) { F_StartGameEnd(); return; } if (keypressed) ; else if (finalecount <= EVALLEN_HALFWAY + TICRATE) ; else if (finalecount >= (timetonext - TICRATE)) ; else if (!menuactive && M_MenuConfirmPressed(0)) { keypressed = true; timetonext = finalecount + TICRATE; } if (finalecount == EVALLEN_HALFWAY) { if (!usedCheats) { ++gamedata->timesBeaten; M_UpdateUnlockablesAndExtraEmblems(true, true); G_SaveGameData(); } } } #undef EVALLEN_NORMAL #undef EVALLEN_HALFWAY #undef EVALLEN_PERFECT // ========== // GAME END // ========== void F_StartGameEnd(void) { // Early fadeout to let the sound finish playing F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); nextmap = NEXTMAP_TITLE; G_EndGame(); } // ============== // TITLE SCREEN // ============== static void F_InitMenuPresValues(void) { menuanimtimer = 0; // Set defaults for presentation values strncpy(curbgname, "TITLESKY", 9); curfadevalue = 16; curbgcolor = -1; curbgxspeed = titlescrollxspeed; curbgyspeed = titlescrollyspeed; curbghide = true; curhidepics = hidetitlepics; curttmode = ttmode; curttscale = ttscale; strncpy(curttname, ttname, 9); curttx = ttx; curtty = tty; curttloop = ttloop; curtttics = tttics; LUA_HUD_DestroyDrawList(luahuddrawlist_title); luahuddrawlist_title = LUA_HUD_CreateDrawList(); } // // F_SkyScroll // void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname) { INT32 xscrolled, x, xneg = (scrollxspeed > 0) - (scrollxspeed < 0), tilex; INT32 yscrolled, y, yneg = (scrollyspeed > 0) - (scrollyspeed < 0), tiley; boolean xispos = (scrollxspeed >= 0), yispos = (scrollyspeed >= 0); INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy); INT16 patwidth, patheight; INT32 pw, ph; // scaled by dupz patch_t *pat; INT32 i, j; fixed_t fracmenuanimtimer, xscrolltimer, yscrolltimer; if (rendermode == render_none) return; V_DrawFill(0, 0, vid.width, vid.height, 31); if (!patchname || !patchname[0]) { return; } pat = W_CachePatchName(patchname, PU_PATCH_LOWPRIORITY); if (scrollxspeed == 0 && scrollyspeed == 0) { V_DrawPatchFill(pat); return; } patwidth = pat->width; patheight = pat->height; pw = patwidth * dupz; ph = patheight * dupz; tilex = max(FixedCeil(FixedDiv(vid.width, pw)) >> FRACBITS, 1)+2; // one tile on both sides of center tiley = max(FixedCeil(FixedDiv(vid.height, ph)) >> FRACBITS, 1)+2; fracmenuanimtimer = (menuanimtimer * FRACUNIT) - (FRACUNIT - rendertimefrac); xscrolltimer = ((fracmenuanimtimer*scrollxspeed)/16 + patwidth*xneg*FRACUNIT) % (patwidth * FRACUNIT); yscrolltimer = ((fracmenuanimtimer*scrollyspeed)/16 + patheight*yneg*FRACUNIT) % (patheight * FRACUNIT); // coordinate offsets xscrolled = FixedInt(xscrolltimer * dupz); yscrolled = FixedInt(yscrolltimer * dupz); for (x = (xispos) ? -pw*(tilex-1)+pw : 0, i = 0; i < tilex; x += pw, i++) { for (y = (yispos) ? -ph*(tiley-1)+ph : 0, j = 0; j < tiley; y += ph, j++) { V_DrawScaledPatch( (xispos) ? xscrolled - x : x + xscrolled, (yispos) ? yscrolled - y : y + yscrolled, V_NOSCALESTART, pat); } } } #define LOADTTGFX(arr, name, maxf) \ lumpnum = W_CheckNumForName(name); \ if (lumpnum != LUMPERROR) \ { \ arr[0] = W_CachePatchName(name, PU_PATCH_LOWPRIORITY); \ arr[min(1, maxf-1)] = 0; \ } \ else if (strlen(name) <= 6) \ { \ fixed_t cnt = strlen(name); \ strncpy(lumpname, name, 7); \ for (i = 0; i < maxf-1; i++) \ { \ sprintf(&lumpname[cnt], "%.2hu", (UINT16)(i+1)); \ lumpname[8] = 0; \ lumpnum = W_CheckNumForName(lumpname); \ if (lumpnum != LUMPERROR) \ arr[i] = W_CachePatchName(lumpname, PU_PATCH_LOWPRIORITY); \ else \ break; \ } \ arr[min(i, maxf-1)] = 0; \ } \ else \ arr[0] = 0; static void F_CacheTitleScreen(void) { UINT16 i; switch (curttmode) { case TTMODE_NONE: break; case TTMODE_RINGRACERS: { if (!M_SecretUnlocked(SECRET_ALTTITLE, true)) { CV_StealthSetValue(&cv_alttitle, 0); } kts_bumper = W_CachePatchName( (cv_alttitle.value ? "KTSJUMPR1" : "KTSBUMPR1"), PU_PATCH_LOWPRIORITY); kts_eggman = W_CachePatchName("KTSEGG01", PU_PATCH_LOWPRIORITY); kts_tails = W_CachePatchName("KTSTAL01", PU_PATCH_LOWPRIORITY); kts_tails_tails = W_CachePatchName("KTSTAL02", PU_PATCH_LOWPRIORITY); for (i = 0; i < 6; i++) { kts_electricity[i] = W_CachePatchName(va("KTSELCT%.1d", i+1), PU_PATCH_LOWPRIORITY); } kts_copyright = W_CachePatchName("KTSCR", PU_PATCH_LOWPRIORITY); break; } case TTMODE_USER: { lumpnum_t lumpnum; char lumpname[9]; LOADTTGFX(ttuser, curttname, TTMAX_USER) break; } } } void F_StartTitleScreen(void) { INT32 titleMapNum; setup_numplayers = 0; if (gamestate != GS_TITLESCREEN && gamestate != GS_WAITINGPLAYERS) { ttuser_count = 0; finalecount = 0; wipetypepost = 0; } else wipegamestate = GS_TITLESCREEN; if (titlemap && ((titleMapNum = G_MapNumber(titlemap)) < nummapheaders) && mapheaderinfo[titleMapNum] && mapheaderinfo[titleMapNum]->lumpnum != LUMPERROR) { mapthing_t *startpos; gamestate_t prevwipegamestate = wipegamestate; titlemapinaction = true; gamemap = titleMapNum+1; maptol = mapheaderinfo[titleMapNum]->typeoflevel; globalweather = mapheaderinfo[titleMapNum]->weather; G_DoLoadLevelEx(true, GS_TITLESCREEN); if (!titlemap) return; players[displayplayers[0]].playerstate = PST_DEAD; // Don't spawn the player in dummy (I'm still a filthy cheater) // Set Default Position if (playerstarts[0]) startpos = playerstarts[0]; else if (deathmatchstarts[0]) startpos = deathmatchstarts[0]; else startpos = NULL; if (startpos) { camera[0].x = startpos->x << FRACBITS; camera[0].y = startpos->y << FRACBITS; camera[0].subsector = R_PointInSubsector(camera[0].x, camera[0].y); camera[0].z = camera[0].subsector->sector->floorheight + (startpos->z << FRACBITS); camera[0].angle = (startpos->angle % 360)*ANG1; camera[0].aiming = 0; } else { camera[0].x = camera[0].y = camera[0].z = camera[0].angle = camera[0].aiming = 0; camera[0].subsector = NULL; // toast is filthy too } camera[0].chase = true; camera[0].height = 0; wipegamestate = prevwipegamestate; } else { G_SetGamestate(GS_TITLESCREEN); titlemapinaction = false; gamemap = 1; // g_game.c CON_ClearHUD(); } // IWAD dependent stuff. animtimer = skullAnimCounter = 0; demoDelayLeft = demoDelayTime; demoIdleLeft = demoIdleTime; F_InitMenuPresValues(); F_CacheTitleScreen(); } void F_VersionDrawer(void) { // An adapted thing from old menus - most games have version info on the title screen now... INT32 texty = vid.height - 10*vid.dupy; #define addtext(f, str) {\ V_DrawThinString(vid.dupx, texty, V_NOSCALESTART|f, str);\ texty -= 10*vid.dupy;\ } if (customversionstring[0] != '\0') { addtext(0, customversionstring); addtext(0, "Mod version:"); } else { // Development -- show revision / branch info #if defined(TESTERS) addtext(V_SKYMAP, "Tester client"); addtext(V_TRANSLUCENT, va("%s", compdate)); #elif defined(DEVELOP) addtext(V_TRANSLUCENT, va("%s %s", comprevision, compnote)); addtext(V_TRANSLUCENT, D_GetFancyBranchName()); #else // Regular build addtext(V_TRANSLUCENT, va("%s", VERSIONSTRING)); #endif if (compoptimized) { addtext(V_TRANSLUCENT, va("%s build", comptype)); } else { addtext(V_ORANGEMAP, va("%s build (no optimizations)", comptype)); } if (compuncommitted) { addtext(V_REDMAP|V_STRINGDANCE, "! UNCOMMITTED CHANGES !"); } } #undef addtext } // (no longer) De-Demo'd Title Screen void F_TitleScreenDrawer(void) { boolean hidepics = false; #if 0 if (modeattacking) return; // We likely came here from retrying. Don't do a damn thing. #endif // Draw that sky! if (curbgcolor >= 0) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor); else if (!curbghide || !titlemapinaction || gamestate == GS_WAITINGPLAYERS) F_SkyScroll(curbgxspeed, curbgyspeed, curbgname); // Don't draw outside of the title screen, or if the patch isn't there. if (gamestate != GS_TITLESCREEN && gamestate != GS_WAITINGPLAYERS) return; // rei|miru: use title pics? hidepics = curhidepics; if (hidepics) { goto luahook; } switch (curttmode) { case TTMODE_NONE: { break; } case TTMODE_RINGRACERS: { const char *eggName = "eggman"; INT32 eggSkin = R_SkinAvailable(eggName); skincolornum_t eggColor = SKINCOLOR_RED; UINT8 *eggColormap = NULL; const char *tailsName = "tails"; INT32 tailsSkin = R_SkinAvailable(tailsName); skincolornum_t tailsColor = SKINCOLOR_ORANGE; UINT8 *tailsColormap = NULL; if (eggSkin != -1) { eggColor = skins[eggSkin].prefcolor; } eggColormap = R_GetTranslationColormap(TC_DEFAULT, eggColor, GTC_MENUCACHE); if (tailsSkin != -1) { tailsColor = skins[tailsSkin].prefcolor; } tailsColormap = R_GetTranslationColormap(TC_DEFAULT, tailsColor, GTC_MENUCACHE); V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_tails_tails, tailsColormap); V_DrawFixedPatch(0, 0, FRACUNIT, V_ADD, kts_electricity[finalecount % 6], NULL); V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_eggman, eggColormap); V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_tails, tailsColormap); V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_bumper, NULL); V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_copyright, NULL); break; } case TTMODE_USER: { if (!ttuser[max(0, ttuser_count)]) { if(curttloop > -1 && ttuser[curttloop]) ttuser_count = curttloop; else if (ttuser[max(0, ttuser_count-1)]) ttuser_count = max(0, ttuser_count-1); else break; // draw nothing } V_DrawSciencePatch(curttx< 0) M_DrawMenuMessage(); } // (no longer) De-Demo'd Title Screen void F_TitleScreenTicker(boolean run) { menuanimtimer++; // title sky if (run) { if (finalecount == 0) { if (!Music_Playing("title")) { // Now start the music Music_Loop("title", looptitle); Music_Play("title"); } } else if (menumessage.active) { M_MenuMessageTick(); } finalecount++; } // don't trigger if doing anything besides idling on title if (gameaction != ga_nothing || gamestate != GS_TITLESCREEN) return; #ifdef DEVELOP // Done here so lines don't get reflowed to 320x200 // IDK if it works most of the time. R_PrintTextureDuplicates(); #endif // Execute the titlemap camera settings if (titlemapinaction) { thinker_t *th; mobj_t *mo2; mobj_t *cameraref = NULL; // If there's a Line 422 Switch Cut-Away view, don't force us. if (titlemapcam.mobj == NULL || titlemapcam.mobj->type != MT_ALTVIEWMAN) { for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next) { if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed) continue; mo2 = (mobj_t *)th; if (!mo2) continue; if (mo2->type != MT_ALTVIEWMAN) continue; cameraref = mo2; break; } if (cameraref != NULL) { P_SetTarget(&titlemapcam.mobj, cameraref); } } else { cameraref = titlemapcam.mobj; } if (cameraref != NULL) { camera[0].x = cameraref->x; camera[0].y = cameraref->y; camera[0].z = cameraref->z; camera[0].angle = cameraref->angle; camera[0].aiming = cameraref->pitch; camera[0].subsector = cameraref->subsector; } else { // Default behavior: Do a lil' camera spin if a title map is loaded; camera[0].angle += titlescrollxspeed*ANG1/64; } } // no demos to play? or, are they disabled? if (!cv_rollingdemos.value) return; #if defined (TESTERS) return; #endif // Wait for a while (for the music to finish, preferably) // before starting demos if (demoDelayLeft) { --demoDelayLeft; return; } // Hold up for a bit if menu or console active if (menuactive || CON_Ready()) { demoIdleLeft = demoIdleTime; return; } // is it time? if (!(--demoIdleLeft)) { char dname[MAXMAPLUMPNAME+1+8+1]; UINT16 mapnum; UINT8 numstaff; static boolean use_netreplay = false; if ((use_netreplay = !use_netreplay)) { lumpnum_t l = LUMPERROR; numstaff = 0; while (numstaff < 99 && (l = W_CheckNumForName(va("TDEMO%03u", numstaff))) != LUMPERROR) numstaff++; if (numstaff) { numstaff = M_RandomKey(numstaff)+1; snprintf(dname, 9, "TDEMO%03u", numstaff); goto loadreplay; } } // prevent console spam if failed demoIdleLeft = demoIdleTime; mapnum = G_RandMap(TOL_RACE|TOL_BATTLE, UINT16_MAX-1, true, false, NULL); if (mapnum == 0) // gotta have ONE { return; } numstaff = M_RandomKey(mapheaderinfo[mapnum]->ghostCount)+1; // Setup demo name sprintf(dname, "%s/GHOST_%u", mapheaderinfo[mapnum]->lumpname, numstaff); loadreplay: demo.title = true; demo.ignorefiles = true; demo.loadfiles = false; G_DoPlayDemo(dname); } } void F_TitleDemoTicker(void) { keypressed = false; } // ================ // WAITINGPLAYERS // ================ void F_StartWaitingPlayers(void) { #ifdef NOWAY INT32 i; UINT32 randskin; spritedef_t *sprdef; spriteframe_t *sprframe; #endif wipegamestate = GS_TITLESCREEN; // technically wiping from title screen finalecount = 0; #ifdef NOWAY randskin = R_GetLocalRandomSkin(); if (waitcolormap) Z_Free(waitcolormap); waitcolormap = R_GetTranslationColormap(randskin, skins[randskin].prefcolor, 0); sprdef = &skins[randskin].sprites[P_GetSkinSprite2(&skins[randskin], SPR2_FSTN, NULL)]; for (i = 0; i < 2; i++) { sprframe = &sprdef->spriteframes[i]; driver[i] = W_CachePatchNum(sprframe->lumppat[0], PU_CACHE); } #endif } void F_WaitingPlayersTicker(void) { if (paused) return; finalecount++; // dumb hack, only start the music on the 1st tick so if you instantly go into the map you aren't hearing a tic of music if (finalecount == 2) Music_Play("wait"); } void F_WaitingPlayersDrawer(void) { #ifdef NOWAY UINT32 frame = (finalecount % 8) / 4; // The game only tics every other frame while waitingplayers #endif const char *waittext1 = "You will join"; const char *waittext2 = "the next race..."; V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); V_DrawCreditString((160 - (V_CreditStringWidth(waittext1)>>1))<>1))<width / 2, 150<= cutscenes[cutnum]->numscenes) { F_EndCutScene(); return; } ++scenenum; timetonext = 0; stoptimer = 0; picnum = 0; picxpos = cutscenes[cutnum]->scene[scenenum].xcoord[picnum]; picypos = cutscenes[cutnum]->scene[scenenum].ycoord[picnum]; // FIXME - port to new music system #if 0 if (cutscenes[cutnum]->scene[scenenum].musswitch[0]) S_ChangeMusicEx(cutscenes[cutnum]->scene[scenenum].musswitch, cutscenes[cutnum]->scene[scenenum].musswitchflags, cutscenes[cutnum]->scene[scenenum].musicloop, cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0); #endif // Fade to the next F_NewCutscene(cutscenes[cutnum]->scene[scenenum].text); picnum = 0; picxpos = cutscenes[cutnum]->scene[scenenum].xcoord[picnum]; picypos = cutscenes[cutnum]->scene[scenenum].ycoord[picnum]; textxpos = cutscenes[cutnum]->scene[scenenum].textxpos; textypos = cutscenes[cutnum]->scene[scenenum].textypos; animtimer = pictime = cutscenes[cutnum]->scene[scenenum].picduration[picnum]; } // See also G_AfterIntermission, the only other place which handles intra-map/ending transitions void F_EndCutScene(void) { cutsceneover = true; // do this first, just in case G_EndGame or something wants to turn it back false later if (runningprecutscene) { if (server) D_MapChange(gamemap, gametype, false, precutresetplayer, 0, true, false); } else { if (cutnum == creditscutscene-1) F_StartGameEvaluation(); else if (cutnum == introtoplay-1) D_StartTitle(); else G_NextLevel(); } } void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean resetplayer) { if (!cutscenes[cutscenenum]) return; G_SetGamestate(GS_CUTSCENE); if (wipegamestate == GS_CUTSCENE) wipegamestate = -1; gameaction = ga_nothing; paused = false; CON_ToggleOff(); F_NewCutscene(cutscenes[cutscenenum]->scene[0].text); cutsceneover = false; runningprecutscene = precutscene; precutresetplayer = resetplayer; scenenum = picnum = 0; cutnum = cutscenenum; picxpos = cutscenes[cutnum]->scene[0].xcoord[0]; picypos = cutscenes[cutnum]->scene[0].ycoord[0]; textxpos = cutscenes[cutnum]->scene[0].textxpos; textypos = cutscenes[cutnum]->scene[0].textypos; pictime = cutscenes[cutnum]->scene[0].picduration[0]; keypressed = false; finalecount = 0; timetonext = 0; animtimer = cutscenes[cutnum]->scene[0].picduration[0]; // Picture duration stoptimer = 0; // FIXME - port to new music system #if 0 if (cutscenes[cutnum]->scene[0].musswitch[0]) S_ChangeMusicEx(cutscenes[cutnum]->scene[0].musswitch, cutscenes[cutnum]->scene[0].musswitchflags, cutscenes[cutnum]->scene[0].musicloop, cutscenes[cutnum]->scene[scenenum].musswitchposition, 0, 0); else S_StopMusic(); #endif S_StopSounds(); } // // F_CutsceneDrawer // void F_CutsceneDrawer(void) { if (dofadenow && rendermode != render_none) { F_WipeStartScreen(); // Fade to any palette color you want. if (cutscenes[cutnum]->scene[scenenum].fadecolor) { V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, cutscenes[cutnum]->scene[scenenum].fadecolor); F_WipeEndScreen(); F_RunWipe(wipe_intro_toblack, cutscenes[cutnum]->scene[scenenum].fadeinid, true, NULL, false, false); F_WipeStartScreen(); } } V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); if (cutscenes[cutnum]->scene[scenenum].picname[picnum][0] != '\0') { if (cutscenes[cutnum]->scene[scenenum].pichires[picnum]) V_DrawSmallScaledPatch(picxpos, picypos, 0, W_CachePatchName(cutscenes[cutnum]->scene[scenenum].picname[picnum], PU_PATCH_LOWPRIORITY)); else V_DrawScaledPatch(picxpos,picypos, 0, W_CachePatchName(cutscenes[cutnum]->scene[scenenum].picname[picnum], PU_PATCH_LOWPRIORITY)); } if (dofadenow && rendermode != render_none) { F_WipeEndScreen(); F_RunWipe(wipe_intro_toblack, cutscenes[cutnum]->scene[scenenum].fadeoutid, true, NULL, false, false); } V_DrawString(textxpos, textypos, 0, cutscene_disptext); } void F_CutsceneTicker(void) { INT32 i; // Online clients tend not to instantly get the map change, so just wait // and don't send 30 of them. if (cutsceneover) return; // advance animation finalecount++; cutscene_boostspeed = 0; for (i = 0; i < MAXPLAYERS; i++) { if (netgame && i != serverplayer && !IsPlayerAdmin(i)) continue; if (players[i].cmd.buttons & BT_BRAKE || players[i].cmd.buttons & BT_ACCELERATE) // SRB2kart { keypressed = false; cutscene_boostspeed = 1; if (timetonext) timetonext = 2; } } if (animtimer) { animtimer--; if (animtimer <= 0) { if (picnum < 7 && cutscenes[cutnum]->scene[scenenum].picname[picnum+1][0] != '\0') { picnum++; picxpos = cutscenes[cutnum]->scene[scenenum].xcoord[picnum]; picypos = cutscenes[cutnum]->scene[scenenum].ycoord[picnum]; pictime = cutscenes[cutnum]->scene[scenenum].picduration[picnum]; animtimer = pictime; } else timetonext = 2; } } if (timetonext) --timetonext; if (++stoptimer > 2 && timetonext == 1) F_AdvanceToNextScene(); else if (!timetonext && !F_WriteText()) timetonext = 5*TICRATE + 1; } boolean F_CutsceneResponder(event_t *event) { if (cutnum == introtoplay-1) return F_IntroResponder(event); return false; } // ================== // TEXT PROMPTS // ================== static void F_GetPageTextGeometry(UINT8 *pagelines, boolean *rightside, INT32 *boxh, INT32 *texth, INT32 *texty, INT32 *namey, INT32 *chevrony, INT32 *textx, INT32 *textr) { // reuse: // cutnum -> promptnum // scenenum -> pagenum lumpnum_t iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname); *pagelines = textprompts[cutnum]->page[scenenum].lines ? textprompts[cutnum]->page[scenenum].lines : 4; *rightside = (iconlump != LUMPERROR && textprompts[cutnum]->page[scenenum].rightside); // Vertical calculations *boxh = *pagelines*2; *texth = textprompts[cutnum]->page[scenenum].name[0] ? (*pagelines-1)*2 : *pagelines*2; // name takes up first line if it exists *texty = BASEVIDHEIGHT - ((*texth * 4) + (*texth/2)*4); *namey = BASEVIDHEIGHT - ((*boxh * 4) + (*boxh/2)*4); *chevrony = BASEVIDHEIGHT - (((1*2) * 4) + ((1*2)/2)*4); // force on last line // Horizontal calculations // Shift text to the right if we have a character icon on the left side // Add 4 margin against icon *textx = (iconlump != LUMPERROR && !*rightside) ? ((*boxh * 4) + (*boxh/2)*4) + 4 : 4; *textr = *rightside ? BASEVIDWIDTH - (((*boxh * 4) + (*boxh/2)*4) + 4) : BASEVIDWIDTH-4; } static fixed_t F_GetPromptHideHudBound(void) { UINT8 pagelines; boolean rightside; INT32 boxh, texth, texty, namey, chevrony; INT32 textx, textr; if (cutnum == INT32_MAX || scenenum == INT32_MAX || !textprompts[cutnum] || scenenum >= textprompts[cutnum]->numpages || !textprompts[cutnum]->page[scenenum].hidehud || (splitscreen && textprompts[cutnum]->page[scenenum].hidehud != 2)) // don't hide on splitscreen, unless hide all is forced return 0; else if (textprompts[cutnum]->page[scenenum].hidehud == 2) // hide all return BASEVIDHEIGHT; F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr); // calc boxheight (see V_DrawPromptBack) boxh *= vid.dupy; boxh = (boxh * 4) + (boxh/2)*5; // 4 lines of space plus gaps between and some leeway // return a coordinate to check // if negative: don't show hud elements below this coordinate (visually) // if positive: don't show hud elements above this coordinate (visually) return 0 - boxh; // \todo: if prompt at top of screen (someday), make this return positive } boolean F_GetPromptHideHudAll(void) { if (cutnum == INT32_MAX || scenenum == INT32_MAX || !textprompts[cutnum] || scenenum >= textprompts[cutnum]->numpages || !textprompts[cutnum]->page[scenenum].hidehud || (splitscreen && textprompts[cutnum]->page[scenenum].hidehud != 2)) // don't hide on splitscreen, unless hide all is forced return false; else if (textprompts[cutnum]->page[scenenum].hidehud == 2) // hide all return true; else return false; } boolean F_GetPromptHideHud(fixed_t y) { INT32 ybound; boolean fromtop; fixed_t ytest; if (!promptactive) return false; ybound = F_GetPromptHideHudBound(); fromtop = (ybound >= 0); ytest = (fromtop ? ybound : BASEVIDHEIGHT + ybound); return (fromtop ? y < ytest : y >= ytest); // true means hide } static void F_PreparePageText(char *pagetext) { UINT8 pagelines; boolean rightside; INT32 boxh, texth, texty, namey, chevrony; INT32 textx, textr; F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr); if (promptpagetext) Z_Free(promptpagetext); if (pagetext && pagetext[0]) { promptpagetext = V_ScaledWordWrap( (textx - textr)<page[scenenum].textspeed ? textprompts[cutnum]->page[scenenum].textspeed : TICRATE/5; cutscene_textcount = 0; // no delay in beginning cutscene_boostspeed = 0; // don't print 8 characters to start // \todo update control hot strings on re-config // and somehow don't reset cutscene text counters } static void F_AdvanceToNextPage(void) { INT32 nextprompt = textprompts[cutnum]->page[scenenum].nextprompt ? textprompts[cutnum]->page[scenenum].nextprompt - 1 : INT32_MAX, nextpage = textprompts[cutnum]->page[scenenum].nextpage ? textprompts[cutnum]->page[scenenum].nextpage - 1 : INT32_MAX, oldcutnum = cutnum; if (textprompts[cutnum]->page[scenenum].nexttag[0]) F_GetPromptPageByNamedTag(textprompts[cutnum]->page[scenenum].nexttag, &nextprompt, &nextpage); // determine next prompt if (nextprompt != INT32_MAX) { if (nextprompt <= MAX_PROMPTS && textprompts[nextprompt]) cutnum = nextprompt; else cutnum = INT32_MAX; } // determine next page if (nextpage != INT32_MAX) { if (cutnum != INT32_MAX) { scenenum = nextpage; if (scenenum >= MAX_PAGES || scenenum > textprompts[cutnum]->numpages-1) scenenum = INT32_MAX; } } else { if (cutnum != oldcutnum) scenenum = 0; else if (scenenum + 1 < MAX_PAGES && scenenum < textprompts[cutnum]->numpages-1) scenenum++; else scenenum = INT32_MAX; } // close the prompt if either num is invalid if (cutnum == INT32_MAX || scenenum == INT32_MAX) F_EndTextPrompt(false, false); else { // on page mode, number of tics before allowing boost // on timer mode, number of tics until page advances timetonext = textprompts[cutnum]->page[scenenum].timetonext ? textprompts[cutnum]->page[scenenum].timetonext : TICRATE/10; F_PreparePageText(textprompts[cutnum]->page[scenenum].text); // gfx picnum = textprompts[cutnum]->page[scenenum].pictostart; numpics = textprompts[cutnum]->page[scenenum].numpics; picmode = textprompts[cutnum]->page[scenenum].picmode; pictoloop = textprompts[cutnum]->page[scenenum].pictoloop > 0 ? textprompts[cutnum]->page[scenenum].pictoloop - 1 : 0; picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum]; picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum]; animtimer = pictime = textprompts[cutnum]->page[scenenum].picduration[picnum]; // FIXME - port to new music system #if 0 // music change if (textprompts[cutnum]->page[scenenum].musswitch[0]) S_ChangeMusic(textprompts[cutnum]->page[scenenum].musswitch, textprompts[cutnum]->page[scenenum].musswitchflags, textprompts[cutnum]->page[scenenum].musicloop); #endif } } void F_EndTextPrompt(boolean forceexec, boolean noexec) { boolean promptwasactive = promptactive; promptactive = false; callpromptnum = callpagenum = callplayer = INT32_MAX; if (promptwasactive) { if (promptmo && promptmo->player && promptblockcontrols) promptmo->reactiontime = TICRATE/4; // prevent jumping right away // \todo account freeze realtime for this) // \todo reset frozen realtime? } // \todo net safety, maybe loop all player thinkers? if ((promptwasactive || forceexec) && !noexec && promptpostexectag) { if (tm.thing) // edge case where starting an invalid prompt immediately on level load will make P_MapStart fail P_LinedefExecute(promptpostexectag, promptmo, NULL); else { P_MapStart(); P_LinedefExecute(promptpostexectag, promptmo, NULL); P_MapEnd(); } } } void F_StartTextPrompt(INT32 promptnum, INT32 pagenum, mobj_t *mo, UINT16 postexectag, boolean blockcontrols, boolean freezerealtime) { INT32 i; // if splitscreen and we already have a prompt active, ignore. // \todo Proper per-player splitscreen support (individual prompts) if (promptactive && splitscreen && promptnum == callpromptnum && pagenum == callpagenum) return; // \todo proper netgame support if (netgame) { F_EndTextPrompt(true, false); // run the post-effects immediately return; } // We share vars, so no starting text prompts over cutscenes or title screens! keypressed = false; finalecount = 0; timetonext = 0; animtimer = 0; stoptimer = 0; skullAnimCounter = 0; // Set up state promptmo = mo; promptpostexectag = postexectag; promptblockcontrols = blockcontrols; (void)freezerealtime; // \todo freeze player->realtime, maybe this needs to cycle through player thinkers // Initialize current prompt and scene callpromptnum = promptnum; callpagenum = pagenum; cutnum = (promptnum < MAX_PROMPTS && textprompts[promptnum]) ? promptnum : INT32_MAX; scenenum = (cutnum != INT32_MAX && pagenum < MAX_PAGES && pagenum <= textprompts[cutnum]->numpages-1) ? pagenum : INT32_MAX; promptactive = (cutnum != INT32_MAX && scenenum != INT32_MAX); if (promptactive) { // on page mode, number of tics before allowing boost // on timer mode, number of tics until page advances timetonext = textprompts[cutnum]->page[scenenum].timetonext ? textprompts[cutnum]->page[scenenum].timetonext : TICRATE/10; F_PreparePageText(textprompts[cutnum]->page[scenenum].text); // gfx picnum = textprompts[cutnum]->page[scenenum].pictostart; numpics = textprompts[cutnum]->page[scenenum].numpics; picmode = textprompts[cutnum]->page[scenenum].picmode; pictoloop = textprompts[cutnum]->page[scenenum].pictoloop > 0 ? textprompts[cutnum]->page[scenenum].pictoloop - 1 : 0; picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum]; picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum]; animtimer = pictime = textprompts[cutnum]->page[scenenum].picduration[picnum]; // FIXME - port to new music system #if 0 // music change if (textprompts[cutnum]->page[scenenum].musswitch[0]) S_ChangeMusic(textprompts[cutnum]->page[scenenum].musswitch, textprompts[cutnum]->page[scenenum].musswitchflags, textprompts[cutnum]->page[scenenum].musicloop); #endif // get the calling player if (promptblockcontrols && mo && mo->player) { for (i = 0; i < MAXPLAYERS; i++) { if (players[i].mo == mo) { callplayer = i; break; } } } } else F_EndTextPrompt(true, false); // run the post-effects immediately } static boolean F_GetTextPromptTutorialTag(char *tag, INT32 length) { INT32 gcs = 0; boolean suffixed = true; if (!tag || !tag[0] || gametype == GT_TUTORIAL) return false; /* if (!strncmp(tag, "TAA", 3)) // Accelerate gcs = G_GetControlScheme(gamecontrol[0], gcl_accelerate, num_gcl_accelerate); else if (!strncmp(tag, "TAB", 3)) // Brake gcs = G_GetControlScheme(gamecontrol[0], gcl_brake, num_gcl_brake); else if (!strncmp(tag, "TAD", 3)) // Drift gcs = G_GetControlScheme(gamecontrol[0], gcl_drift, num_gcl_drift); else if (!strncmp(tag, "TAS", 3)) // Spindash gcs = G_GetControlScheme(gamecontrol[0], gcl_spindash, num_gcl_spindash); else if (!strncmp(tag, "TAM", 3)) // Movement gcs = G_GetControlScheme(gamecontrol[0], gcl_movement, num_gcl_movement); else if (!strncmp(tag, "TAI", 3)) // Item usage gcs = G_GetControlScheme(gamecontrol[0], gcl_item, num_gcl_item); else gcs = G_GetControlScheme(gamecontrol[0], gcl_full, num_gcl_full); */ switch (gcs) { default: strncat(tag, "CUSTOM", length); break; } return suffixed; } void F_GetPromptPageByNamedTag(const char *tag, INT32 *promptnum, INT32 *pagenum) { INT32 nosuffixpromptnum = INT32_MAX, nosuffixpagenum = INT32_MAX; INT32 tutorialpromptnum = (gametype == GT_TUTORIAL) ? TUTORIAL_PROMPT-1 : 0; boolean suffixed = false, found = false; char suffixedtag[33]; *promptnum = *pagenum = INT32_MAX; if (!tag || !tag[0]) return; strncpy(suffixedtag, tag, 33); suffixedtag[32] = 0; if (gametype == GT_TUTORIAL) suffixed = F_GetTextPromptTutorialTag(suffixedtag, 33); for (*promptnum = 0 + tutorialpromptnum; *promptnum < MAX_PROMPTS; (*promptnum)++) { if (!textprompts[*promptnum]) continue; for (*pagenum = 0; *pagenum < textprompts[*promptnum]->numpages && *pagenum < MAX_PAGES; (*pagenum)++) { if (suffixed && fastcmp(suffixedtag, textprompts[*promptnum]->page[*pagenum].tag)) { // this goes first because fastcmp ends early if first string is shorter found = true; break; } else if (nosuffixpromptnum == INT32_MAX && nosuffixpagenum == INT32_MAX && fastcmp(tag, textprompts[*promptnum]->page[*pagenum].tag)) { if (suffixed) { nosuffixpromptnum = *promptnum; nosuffixpagenum = *pagenum; // continue searching for the suffixed tag } else { found = true; break; } } } if (found) break; } if (suffixed && !found && nosuffixpromptnum != INT32_MAX && nosuffixpagenum != INT32_MAX) { found = true; *promptnum = nosuffixpromptnum; *pagenum = nosuffixpagenum; } if (!found) CONS_Debug(DBG_GAMELOGIC, "Text prompt: Can't find a page with named tag %s or suffixed tag %s\n", tag, suffixedtag); } void F_TextPromptDrawer(void) { // reuse: // cutnum -> promptnum // scenenum -> pagenum lumpnum_t iconlump; UINT8 pagelines; boolean rightside; INT32 boxh, texth, texty, namey, chevrony; INT32 textx, textr; // Data patch_t *patch; if (!promptactive) return; iconlump = W_CheckNumForName(textprompts[cutnum]->page[scenenum].iconname); F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr); // Draw gfx first if (picnum >= 0 && picnum < numpics && textprompts[cutnum]->page[scenenum].picname[picnum][0] != '\0') { if (textprompts[cutnum]->page[scenenum].pichires[picnum]) V_DrawSmallScaledPatch(picxpos, picypos, 0, W_CachePatchName(textprompts[cutnum]->page[scenenum].picname[picnum], PU_PATCH_LOWPRIORITY)); else V_DrawScaledPatch(picxpos,picypos, 0, W_CachePatchName(textprompts[cutnum]->page[scenenum].picname[picnum], PU_PATCH_LOWPRIORITY)); } // Draw background V_DrawPromptBack(boxh, textprompts[cutnum]->page[scenenum].backcolor); // Draw narrator icon if (iconlump != LUMPERROR) { INT32 iconx, icony, scale, scaledsize; patch = W_CachePatchName(textprompts[cutnum]->page[scenenum].iconname, PU_PATCH_LOWPRIORITY); // scale and center if (patch->width > patch->height) { scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->width); scaledsize = FixedMul(patch->height, scale); iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS; icony = ((namey-4) << FRACBITS) + FixedDiv(BASEVIDHEIGHT - namey + 4 - scaledsize, 2); // account for 4 margin } else if (patch->height > patch->width) { scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->height); scaledsize = FixedMul(patch->width, scale); iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS; icony = namey << FRACBITS; iconx += FixedDiv(FixedMul(patch->height, scale) - scaledsize, 2); } else { scale = FixedDiv(((boxh * 4) + (boxh/2)*4) - 4, patch->width); iconx = (rightside ? BASEVIDWIDTH - (((boxh * 4) + (boxh/2)*4)) : 4) << FRACBITS; icony = namey << FRACBITS; } if (textprompts[cutnum]->page[scenenum].iconflip) iconx += FixedMul(patch->width, scale) << FRACBITS; V_DrawFixedPatch(iconx, icony, scale, (V_SNAPTOBOTTOM|(textprompts[cutnum]->page[scenenum].iconflip ? V_FLIP : 0)), patch, NULL); W_UnlockCachedPatch(patch); } // Draw text V_DrawString(textx, texty, V_SNAPTOBOTTOM, cutscene_disptext); // Draw name // Don't use V_YELLOWMAP here so that the name color can be changed with control codes if (textprompts[cutnum]->page[scenenum].name[0]) V_DrawString(textx, namey, V_SNAPTOBOTTOM, textprompts[cutnum]->page[scenenum].name); // Draw chevron if (promptblockcontrols && !timetonext) V_DrawString(textr-8, chevrony + (skullAnimCounter/5), (V_SNAPTOBOTTOM|V_YELLOWMAP), "\x1B"); // down arrow } #define nocontrolallowed(j) {\ players[j].nocontrol = 1;\ if (players[j].mo)\ {\ if (players[j].mo->state == states+S_KART_STILL && players[j].mo->tics != -1)\ players[j].mo->tics++;\ }\ } void F_TextPromptTicker(void) { INT32 i; if (!promptactive || paused || P_AutoPause()) return; // advance animation finalecount++; cutscene_boostspeed = 0; // for the chevron if (--skullAnimCounter <= 0) skullAnimCounter = 8; // button handling if (textprompts[cutnum]->page[scenenum].timetonext) { if (promptblockcontrols) // same procedure as below, just without the button handling { for (i = 0; i < MAXPLAYERS; i++) { if (netgame && i != serverplayer && !IsPlayerAdmin(i)) continue; else { UINT8 j; // Both players' controls are locked, // But only consoleplayer can advance the prompt. // \todo Proper per-player splitscreen support (individual prompts) for (j = 0; j < g_localplayers[j]; j++) { if (i == g_localplayers[j]) nocontrolallowed(i) } } if (!splitscreen) break; } } if (timetonext >= 1) timetonext--; if (!timetonext) F_AdvanceToNextPage(); F_WriteText(); } else { if (promptblockcontrols) { for (i = 0; i < MAXPLAYERS; i++) { if (netgame && i != serverplayer && !IsPlayerAdmin(i)) continue; else { UINT8 j; players[i].nocontrol = 1; // Both players' controls are locked, // But only the triggering player can advance the prompt. for (j = 0; j <= splitscreen; j++) { if (callplayer == g_localplayers[j]) { if (i != callplayer) continue; } else if (i != consoleplayer) continue; } } if (players[i].cmd.buttons & (BT_ACCELERATE|BT_BRAKE|BT_DRIFT)) { if (timetonext > 1) timetonext--; else if (cutscene_baseptr) // don't set boost if we just reset the string cutscene_boostspeed = 1; // only after a slight delay if (keypressed) { if (!splitscreen) break; else continue; } if (!timetonext) // is 0 when finished generating text { F_AdvanceToNextPage(); if (promptactive) S_StartSound(NULL, sfx_menu1); } keypressed = true; // prevent repeat events } else if (!(players[i].cmd.buttons & (BT_ACCELERATE|BT_BRAKE|BT_DRIFT))) keypressed = false; if (!splitscreen) break; } } // generate letter-by-letter text if (scenenum >= MAX_PAGES || !textprompts[cutnum]->page[scenenum].text || !textprompts[cutnum]->page[scenenum].text[0] || !F_WriteText()) timetonext = !promptblockcontrols; // never show the chevron if we can't toggle pages } // gfx if (picnum >= 0 && picnum < numpics) { if (animtimer <= 0) { boolean persistanimtimer = false; if (picnum < numpics-1 && textprompts[cutnum]->page[scenenum].picname[picnum+1][0] != '\0') picnum++; else if (picmode == PROMPT_PIC_LOOP) picnum = pictoloop; else if (picmode == PROMPT_PIC_DESTROY) picnum = -1; else // if (picmode == PROMPT_PIC_PERSIST) persistanimtimer = true; if (!persistanimtimer && picnum >= 0) { picxpos = textprompts[cutnum]->page[scenenum].xcoord[picnum]; picypos = textprompts[cutnum]->page[scenenum].ycoord[picnum]; pictime = textprompts[cutnum]->page[scenenum].picduration[picnum]; animtimer = pictime; } } else animtimer--; } }