diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp index 47502cb0f..8e598bef8 100644 --- a/src/acs/call-funcs.cpp +++ b/src/acs/call-funcs.cpp @@ -3589,13 +3589,15 @@ bool CallFunc_SetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, A INT32 value = 0; tag = argV[0]; - mobj = P_FindMobjFromTID(tag, mobj, info->mo); + mobj_t *next = P_FindMobjFromTID(tag, mobj, info->mo); property = argV[1]; value = argV[2]; - while (mobj != NULL) + while ((mobj = next) != NULL) { + // First in case of deletion. (Can't check for value == S_NULL because of A_ calls, etc) + next = P_FindMobjFromTID(tag, mobj, info->mo); #define PROP_READONLY(x, y) \ case x: \ @@ -3830,8 +3832,6 @@ bool CallFunc_SetThingProperty(ACSVM::Thread *thread, const ACSVM::Word *argV, A } } - mobj = P_FindMobjFromTID(tag, mobj, info->mo); - #undef PROP_FLAGS #undef PROP_SCALE #undef PROP_MOBJ diff --git a/src/core/vector.hpp b/src/core/vector.hpp index e6d66f91f..ec26334ea 100644 --- a/src/core/vector.hpp +++ b/src/core/vector.hpp @@ -149,6 +149,7 @@ public: Vector& operator=(const Vector& rhs) { + clear(); for (auto itr = rhs.begin(); itr != rhs.end(); itr++) { push_back(*itr); diff --git a/src/cvars.cpp b/src/cvars.cpp index 2ea673fdc..5bc1e5714 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -728,7 +728,7 @@ consvar_t cv_kartspeed = UnsavedNetVar("gamespeed", "Auto Gear").values(kartspee consvar_t cv_teamplay = UnsavedNetVar("teamplay", "Off").on_off(); -consvar_t cv_kartusepwrlv = UnsavedNetVar("usepwrlv", "Yes").yes_no(); +consvar_t cv_kartusepwrlv = UnsavedNetVar("mobiums", "Yes").yes_no(); void LiveStudioAudience_OnChange(void); #ifdef DEVELOP @@ -783,6 +783,8 @@ consvar_t cv_timelimit = UnsavedNetVar("timelimit", "Default").min_max(1, 30*60, consvar_t cv_votetime = UnsavedNetVar("votetime", "20").min_max(10, 3600); +consvar_t cv_dueltimelimit = UnsavedNetVar("dueltimelimit", "180").min_max(0, 3600); +consvar_t cv_duelscorelimit = UnsavedNetVar("duelscorelimit", "4").min_max(1, 9); // // Online cheats - synced in netgames. diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 33e9dceaa..767a63503 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -281,7 +281,7 @@ shouldsign_t ShouldSignChallenge(uint8_t *message) if ((max(now, then) - min(now, then)) > 60*15) return SIGN_BADTIME; - // ____ _____ ___ ____ _ + // ____ _____ ___ ____ _ // / ___|_ _/ _ \| _ \| | // \___ \ | || | | | |_) | | // ___) || || |_| | __/|_| @@ -2436,6 +2436,11 @@ static void CL_ConnectToServer(void) joinedIP[0] = '\0'; // And empty this for good measure regardless of whether or not we actually used it. + // Enable sound input/microphone in netgames, activating the microphone device. + if (netgame) + { + S_SoundInputSetEnabled(true); + } } static void Command_connect(void) @@ -2502,6 +2507,8 @@ static void Command_connect(void) { CONS_Alert(CONS_ERROR, M_GetText("There is no server identification with this network driver\n")); D_CloseConnection(); + + S_SoundInputSetEnabled(false); return; } } @@ -3659,6 +3666,7 @@ void D_QuitNetGame(void) K_ClearClientPowerLevels(); G_ObliterateParties(); K_ResetMidVote(); + S_SoundInputSetEnabled(false); DEBFILE("===========================================================================\n" " Log finish\n" @@ -7376,9 +7384,6 @@ void NetVoiceUpdate(void) return; } - // This necessarily runs every frame, not every tic - S_SoundInputSetEnabled(true); - UINT32 bytes_dequed = 0; do { @@ -7555,66 +7560,6 @@ tic_t GetLag(INT32 node) return gametic - nettics[node]; } -#define REWIND_POINT_INTERVAL 4*TICRATE + 16 -rewind_t *rewindhead; - -void CL_ClearRewinds(void) -{ - rewind_t *head; - while ((head = rewindhead)) - { - rewindhead = rewindhead->next; - free(head); - } -} - -rewind_t *CL_SaveRewindPoint(size_t demopos) -{ - savebuffer_t save = {0}; - rewind_t *rewind; - - if (rewindhead && rewindhead->leveltime + REWIND_POINT_INTERVAL > leveltime) - return NULL; - - rewind = (rewind_t *)malloc(sizeof (rewind_t)); - if (!rewind) - return NULL; - - P_SaveBufferFromExisting(&save, rewind->savebuffer, NETSAVEGAMESIZE); - P_SaveNetGame(&save, false); - - rewind->leveltime = leveltime; - rewind->next = rewindhead; - rewind->demopos = demopos; - rewindhead = rewind; - - return rewind; -} - -rewind_t *CL_RewindToTime(tic_t time) -{ - savebuffer_t save = {0}; - rewind_t *rewind; - - while (rewindhead && rewindhead->leveltime > time) - { - rewind = rewindhead->next; - free(rewindhead); - rewindhead = rewind; - } - - if (!rewindhead) - return NULL; - - P_SaveBufferFromExisting(&save, rewindhead->savebuffer, NETSAVEGAMESIZE); - P_LoadNetGame(&save, false); - - wipegamestate = gamestate; // No fading back in! - timeinmap = leveltime; - - return rewindhead; -} - void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest) { #ifdef NOMD5 diff --git a/src/d_clisrv.h b/src/d_clisrv.h index de606acb6..eb3861c55 100644 --- a/src/d_clisrv.h +++ b/src/d_clisrv.h @@ -712,21 +712,6 @@ extern boolean hu_stopped; // SRB2Kart // -struct rewind_t { - UINT8 savebuffer[NETSAVEGAMESIZE]; - tic_t leveltime; - size_t demopos; - - ticcmd_t oldcmd[MAXPLAYERS]; - mobj_t oldghost[MAXPLAYERS]; - - rewind_t *next; -}; - -void CL_ClearRewinds(void); -rewind_t *CL_SaveRewindPoint(size_t demopos); -rewind_t *CL_RewindToTime(tic_t time); - void HandleSigfail(const char *string); void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message); diff --git a/src/d_main.cpp b/src/d_main.cpp index 7afb32df7..98590c30a 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -742,9 +742,6 @@ static bool D_Display(bool world) if (forcerefresh && G_GamestateUsesLevel() == false) V_SetPalette(0); - if (demo.rewinding) - V_DrawFadeScreen(TC_RAINBOW, (leveltime & 0x20) ? SKINCOLOR_PASTEL : SKINCOLOR_MOONSET); - // vid size change is now finished if it was on... vid.recalc = 0; @@ -936,7 +933,7 @@ void D_SRB2Loop(void) realtics = entertic - oldentertics; oldentertics = entertic; - if (demo.playback && gamestate == GS_LEVEL) + if (demo.playback && gamestate == GS_LEVEL && demo.simplerewind == DEMO_REWIND_OFF) { // Nicer place to put this. realtics = realtics * cv_playbackspeed.value; @@ -1216,6 +1213,7 @@ void D_ClearState(void) // Reset GP and roundqueue memset(&grandprixinfo, 0, sizeof(struct grandprixinfo)); memset(&roundqueue, 0, sizeof(struct roundqueue)); + memset(&menuqueue, 0, sizeof(struct menuqueue)); // empty some other semi-important state maptol = 0; @@ -1248,7 +1246,8 @@ void D_ClearState(void) if (gamedata && gamedata->deferredsave) G_SaveGameData(); - K_UnsetDialogue(); + P_FreeLevelState(); + P_InvalidateThinkersWithoutInit(); G_SetGamestate(GS_NULL); wipegamestate = GS_NULL; diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 054504912..2be94c176 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2291,11 +2291,11 @@ void D_SetupVote(INT16 newgametype) void D_ModifyClientVote(UINT8 player, SINT8 voted) { - char buf[2]; + char buf[3]; char *p = buf; - UINT8 sendPlayer = consoleplayer; + UINT8 sendPlayer = 0; - if (player == UINT8_MAX) + if (player >= MAXPLAYERS) { // Special game vote (map anger, duel) if (!server) @@ -2304,16 +2304,16 @@ void D_ModifyClientVote(UINT8 player, SINT8 voted) } } - if (player == UINT8_MAX) - { - // special vote - WRITEUINT8(p, UINT8_MAX); - } - else - { - INT32 i = 0; - WRITEUINT8(p, player); + // Context value -- if context has changed, then discard vote update. + // This is to prevent votes being registered from different vote types. + // Currently used for Duel vs Normal votes. + WRITEUINT8(p, Y_VoteContext()); + WRITEUINT8(p, player); + + if (player <= MAXPLAYERS) + { + INT32 i; for (i = 0; i <= splitscreen; i++) { if (g_localplayers[i] == player) @@ -2529,7 +2529,7 @@ static void Command_Map_f(void) newforcespecialstage = COM_CheckParm("-forcespecialstage"); usingcheats = CV_CheatsEnabled(); - ischeating = (!(netgame || multiplayer)) || (!newresetplayers); + ischeating = (!(netgame || multiplayer)) || (!newresetplayers) || (!K_CanChangeRules(false)); if (!( first_option = COM_FirstOption() )) first_option = COM_Argc(); @@ -2984,7 +2984,7 @@ static void Command_RestartLevel(void) D_MapChange(gamemap, g_lastgametype, newencore, false, 0, false, false); } -static void Handle_MapQueueSend(UINT16 newmapnum, UINT16 newgametype, boolean newencoremode) +void Handle_MapQueueSend(UINT16 newmapnum, UINT16 newgametype, boolean newencoremode) { UINT8 flags = 0; @@ -5636,30 +5636,35 @@ static void Got_SetupVotecmd(const UINT8 **cp, INT32 playernum) static void Got_ModifyVotecmd(const UINT8 **cp, INT32 playernum) { + UINT8 context = READUINT8(*cp); UINT8 targetID = READUINT8(*cp); SINT8 vote = READSINT8(*cp); - if (targetID == UINT8_MAX) + if (context != Y_VoteContext()) { - if (playernum != serverplayer) // server-only special vote + // Silently discard. Server changed the + // vote type as we were sending our vote. + return; + } + + if (targetID >= MAXPLAYERS) + { + // only the server is allowed to send these + if (playernum != serverplayer) { goto fail; } - - targetID = VOTE_SPECIAL; } else if (playeringame[targetID] == true && players[targetID].bot == true) { - if (targetID >= MAXPLAYERS - || playernum != serverplayer) + if (playernum != serverplayer) { goto fail; } } else { - if (targetID >= MAXPLAYERS - || playernode[targetID] != playernode[playernum]) + if (playernode[targetID] != playernode[playernum]) { goto fail; } diff --git a/src/d_netcmd.h b/src/d_netcmd.h index bff6fe264..7391d0921 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -52,6 +52,7 @@ extern consvar_t cv_joyscale[MAXSPLITSCREENPLAYERS]; extern consvar_t cv_pointlimit; extern consvar_t cv_timelimit; +extern consvar_t cv_dueltimelimit, cv_duelscorelimit; extern consvar_t cv_numlaps; extern UINT32 timelimitintics, extratimeintics, secretextratime; extern UINT32 g_pointlimit; @@ -205,6 +206,7 @@ size_t WeaponPref_Parse(const UINT8 *p, INT32 playernum); void D_SendPlayerConfig(UINT8 n); void Command_ExitGame_f(void); void Command_Retry_f(void); +void Handle_MapQueueSend(UINT16 newmapnum, UINT16 newgametype, boolean newencoremode); boolean G_GamestateUsesExitLevel(void); void D_GameTypeChanged(INT32 lastgametype); // not a real _OnChange function anymore void D_MapChange(UINT16 pmapnum, INT32 pgametype, boolean pencoremode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pforcespecialstage); diff --git a/src/d_player.h b/src/d_player.h index f056ca978..96af19f80 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -830,6 +830,8 @@ struct player_t UINT8 numsneakers; // Number of stacked sneaker effects UINT16 panelsneakertimer; UINT8 numpanelsneakers; + UINT16 weaksneakertimer; + UINT8 numweaksneakers; UINT8 floorboost; // (0 to 3) - Prevents Sneaker sounds for a brief duration when triggered by a floor panel INT16 growshrinktimer; // > 0 = Big, < 0 = small @@ -959,6 +961,8 @@ struct player_t INT32 cheatchecknum; // The number of the last cheatcheck you hit INT32 checkpointId; // Players respawn here, objects/checkpoint.cpp + INT16 duelscore; + UINT8 team; // 0 == Spectator, 1 == Red, 2 == Blue UINT8 checkskip; // Skipping checkpoints? Oh no no no diff --git a/src/deh_tables.c b/src/deh_tables.c index 2d92aac06..e115287c8 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -297,6 +297,7 @@ actionpointer_t actionpointers[] = {{A_MakeSSCandle}, "A_MAKESSCANDLE"}, {{A_HologramRandomTranslucency}, "A_HOLOGRAMRANDOMTRANSLUCENCY"}, {{A_SSChainShatter}, "A_SSCHAINSHATTER"}, + {{A_GenericBumper}, "A_GENERICBUMPER"}, {{NULL}, "NONE"}, diff --git a/src/doomdef.h b/src/doomdef.h index cef04507f..b62f44472 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -745,7 +745,7 @@ extern int #define MAXAMPSCALINGDIST 18000 // Exp -#define MINEXP 50 // The min value target +#define MINEXP 25 // The min value target #define TARGETEXP 100 // The target value needed for A rank #define MAXEXP 125 // The max value displayed by the hud and in the tally screen and GP results screen diff --git a/src/doomstat.h b/src/doomstat.h index 022cd054e..c239fa5f2 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -882,16 +882,24 @@ extern SINT8 spbplace; extern boolean rainbowstartavailable; extern tic_t linecrossed; extern boolean inDuel; +extern UINT8 overtimecheckpoints; extern tic_t bombflashtimer; // Used to avoid causing seizures if multiple mines explode close to you :) extern boolean legitimateexit; extern boolean comebackshowninfo; +#define VOTE_NUM_LEVELS (4) +#define VOTE_NOT_PICKED (-1) #define VOTE_SPECIAL (MAXPLAYERS) #define VOTE_TOTAL (MAXPLAYERS+1) -extern UINT16 g_voteLevels[4][2]; + +#define VOTE_TIMEOUT_LOSER (MAXPLAYERS+1) // not a real vote ID +#define VOTE_TIMEOUT_WINNER (MAXPLAYERS+2) // ditto + +extern UINT16 g_voteLevels[VOTE_NUM_LEVELS][2]; extern SINT8 g_votes[VOTE_TOTAL]; extern SINT8 g_pickedVote; +extern boolean g_votes_striked[VOTE_NUM_LEVELS]; // =========================== // Internal parameters, fixed. diff --git a/src/g_build_ticcmd.cpp b/src/g_build_ticcmd.cpp index b8d45e3dc..f32c1f509 100644 --- a/src/g_build_ticcmd.cpp +++ b/src/g_build_ticcmd.cpp @@ -404,7 +404,10 @@ class TiccmdBuilder map(gc_item, BT_ATTACK); // fire map(gc_lookback, BT_LOOKBACK); // rear view - map(gc_respawn, BT_RESPAWN | (freecam() ? 0 : BT_EBRAKEMASK)); // respawn + if (!modeattacking) + { + map(gc_respawn, BT_RESPAWN | (freecam() ? 0 : BT_EBRAKEMASK)); // respawn + } map(gc_vote, BT_VOTE); // mp general function button // lua buttons a thru c diff --git a/src/g_demo.cpp b/src/g_demo.cpp index 88828fee6..05a881ace 100644 --- a/src/g_demo.cpp +++ b/src/g_demo.cpp @@ -270,22 +270,31 @@ static ticcmd_t oldcmd[MAXPLAYERS]; static mobj_t oldghost[MAXPLAYERS]; +boolean G_ConsiderEndingDemoWrite(void) +{ + // chill, we reserved extra memory so it's + // "safe" to have written a bit past the end + if (demobuf.p < demobuf.end) + return false; + + G_CheckDemoStatus(); + return true; +} + +boolean G_ConsiderEndingDemoRead(void) +{ + if (*demobuf.p != DEMOMARKER) + return false; + + G_CheckDemoStatus(); + return true; +} + void G_ReadDemoExtraData(void) { INT32 p, extradata, i; char name[64]; static_assert(sizeof name >= std::max({MAXPLAYERNAME+1u, SKINNAMESIZE+1u, MAXCOLORNAME+1u})); - - if (leveltime > starttime) - { - rewind_t *rewind = CL_SaveRewindPoint(demobuf.p - demobuf.buffer); - if (rewind) - { - memcpy(rewind->oldcmd, oldcmd, sizeof (oldcmd)); - memcpy(rewind->oldghost, oldghost, sizeof (oldghost)); - } - } - memset(name, '\0', sizeof name); p = READUINT8(demobuf.p); @@ -460,13 +469,6 @@ void G_ReadDemoExtraData(void) p = READUINT8(demobuf.p); } - - if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER) - { - // end of demo data stream - G_CheckDemoStatus(); - return; - } } void G_WriteDemoExtraData(void) @@ -623,13 +625,6 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum) } G_CopyTiccmd(cmd, &oldcmd[playernum], 1); - - if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER) - { - // end of demo data stream - G_CheckDemoStatus(); - return; - } } void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum) @@ -739,14 +734,6 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum) WRITEUINT16(botziptic_p, botziptic); } - - // attention here for the ticcmd size! - // latest demos with mouse aiming byte in ticcmd - if (!(demoflags & DF_GHOST) && ziptic_p > demobuf.end - 9) - { - G_CheckDemoStatus(); // no more space - return; - } } void G_GhostAddFlip(INT32 playernum) @@ -794,8 +781,11 @@ void G_GhostAddHit(INT32 playernum, mobj_t *victim) void G_WriteAllGhostTics(void) { - boolean toobig = false; INT32 i, counter = leveltime; + + if (!demobuf.p || !(demoflags & DF_GHOST)) + return; // No ghost data to write. + for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) @@ -811,22 +801,9 @@ void G_WriteAllGhostTics(void) WRITEUINT8(demobuf.p, i); G_WriteGhostTic(players[i].mo, i); - - // attention here for the ticcmd size! - // latest demos with mouse aiming byte in ticcmd - if (demobuf.p >= demobuf.end - (13 + 9 + 9)) - { - toobig = true; - break; - } } + WRITEUINT8(demobuf.p, 0xFF); - - if (toobig) - { - G_CheckDemoStatus(); // no more space - return; - } } void G_WriteGhostTic(mobj_t *ghost, INT32 playernum) @@ -835,11 +812,6 @@ void G_WriteGhostTic(mobj_t *ghost, INT32 playernum) UINT8 *ziptic_p; UINT32 i; - if (!demobuf.p) - return; - if (!(demoflags & DF_GHOST)) - return; // No ghost data to write. - ziptic_p = demobuf.p++; // the ziptic, written at the end of this function #define MAXMOM (0xFFFF<<8) @@ -1061,7 +1033,7 @@ void G_ConsAllGhostTics(void) { UINT8 p; - if (!demobuf.p || !demo.deferstart) + if (!demobuf.p || !(demoflags & DF_GHOST) || !demo.deferstart) return; p = READUINT8(demobuf.p); @@ -1071,13 +1043,6 @@ void G_ConsAllGhostTics(void) G_ConsGhostTic(p); p = READUINT8(demobuf.p); } - - if (*demobuf.p == DEMOMARKER) - { - // end of demo data stream - G_CheckDemoStatus(); - return; - } } // Uses ghost data to do consistency checks on your position. @@ -1089,9 +1054,6 @@ void G_ConsGhostTic(INT32 playernum) mobj_t *testmo; UINT32 syncleeway; - if (!(demoflags & DF_GHOST)) - return; // No ghost data to use. - testmo = players[playernum].mo; // Grab ghost data. @@ -1282,13 +1244,6 @@ void G_ConsGhostTic(INT32 playernum) } } } - - if (*demobuf.p == DEMOMARKER) - { - // end of demo data stream - G_CheckDemoStatus(); - return; - } } void G_GhostTicker(void) @@ -1309,11 +1264,31 @@ void G_GhostTicker(void) continue; readghosttic: +#define follow g->mo->tracer // Skip normal demo data. ziptic = READUINT8(g->p); xziptic = 0; + // Demo ends after ghost data. + if (ziptic == DEMOMARKER) + { +fadeghost: + g->mo->momx = g->mo->momy = g->mo->momz = 0; + g->mo->fuse = TICRATE; + if (follow) + { + follow->fuse = TICRATE; + } + + g->done = true; + if (p) + { + p->next = g->next; + } + continue; + } + while (ziptic != DW_END) // Get rid of extradata stuff { if (ziptic < MAXPLAYERS) @@ -1414,9 +1389,11 @@ readghosttic: // Grab ghost data. ziptic = READUINT8(g->p); + if (ziptic == DEMOMARKER) // Had to end early for some reason + goto fadeghost; if (ziptic == 0xFF) goto skippedghosttic; // Didn't write ghost info this frame - else if (ziptic != 0) + if (ziptic != 0) I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this ziptic = READUINT8(g->p); @@ -1530,7 +1507,6 @@ readghosttic: g->mo->renderflags &= ~RF_DONTDRAW; } -#define follow g->mo->tracer if (ziptic & GZT_FOLLOW) { // Even more... UINT8 followtic = READUINT8(g->p); @@ -1620,28 +1596,6 @@ skippedghosttic: if (READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here. I_Error("Ghost is not a record attack ghost GHOSTEND"); //@TODO lmao don't blow up like this - // Demo ends after ghost data. - if (*g->p == DEMOMARKER) - { - g->mo->momx = g->mo->momy = g->mo->momz = 0; -#if 0 // freeze frame (maybe more useful for time attackers) (2024-03-11: you leave it behind anyway!) - g->mo->colorized = true; - g->mo->fuse = 10*TICRATE; - if (follow) - follow->colorized = true; -#else // dissapearing act - g->mo->fuse = TICRATE; - if (follow) - follow->fuse = TICRATE; -#endif - g->done = true; - if (p) - { - p->next = g->next; - } - continue; - } - // If the timer started, skip ahead until the ghost starts too. if (starttime <= leveltime && !g->linecrossed && G_TimeAttackStart()) goto readghosttic; @@ -1651,203 +1605,6 @@ skippedghosttic: } } -// Demo rewinding functions -typedef struct rewindinfo_s { - tic_t leveltime; - - struct { - boolean ingame; - player_t player; - mobj_t mobj; - } playerinfo[MAXPLAYERS]; - - struct rewindinfo_s *prev; -} rewindinfo_t; - -static tic_t currentrewindnum; -static rewindinfo_t *rewindhead = NULL; // Reverse chronological order - -void G_InitDemoRewind(void) -{ - CL_ClearRewinds(); - - while (rewindhead) - { - rewindinfo_t *p = rewindhead->prev; - Z_Free(rewindhead); - rewindhead = p; - } - - currentrewindnum = 0; -} - -void G_StoreRewindInfo(void) -{ - static UINT8 timetolog = 8; - rewindinfo_t *info; - size_t i; - - if (timetolog-- > 0) - return; - timetolog = 8; - - info = static_cast(Z_Calloc(sizeof(rewindinfo_t), PU_STATIC, NULL)); - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - { - info->playerinfo[i].ingame = false; - continue; - } - - info->playerinfo[i].ingame = true; - memcpy(&info->playerinfo[i].player, &players[i], sizeof(player_t)); - if (players[i].mo) - memcpy(&info->playerinfo[i].mobj, players[i].mo, sizeof(mobj_t)); - } - - info->leveltime = leveltime; - info->prev = rewindhead; - rewindhead = info; -} - -void G_PreviewRewind(tic_t previewtime) -{ - SINT8 i; - //size_t j; - fixed_t tweenvalue = 0; - rewindinfo_t *info = rewindhead, *next_info = rewindhead; - - if (!info) - return; - - while (info->leveltime > previewtime && info->prev) - { - next_info = info; - info = info->prev; - } - if (info != next_info) - tweenvalue = FixedDiv(previewtime - info->leveltime, next_info->leveltime - info->leveltime); - - - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - { - if (info->playerinfo[i].player.mo) - { - //@TODO spawn temp object to act as a player display - } - - continue; - } - - if (!info->playerinfo[i].ingame || !info->playerinfo[i].player.mo) - { - if (players[i].mo) - players[i].mo->renderflags |= RF_DONTDRAW; - - continue; - } - - if (!players[i].mo) - continue; //@TODO spawn temp object to act as a player display - - players[i].mo->renderflags &= ~RF_DONTDRAW; - - P_UnsetThingPosition(players[i].mo); -#define TWEEN(pr) info->playerinfo[i].mobj.pr + FixedMul((INT32) (next_info->playerinfo[i].mobj.pr - info->playerinfo[i].mobj.pr), tweenvalue) - players[i].mo->x = TWEEN(x); - players[i].mo->y = TWEEN(y); - players[i].mo->z = TWEEN(z); - players[i].mo->angle = TWEEN(angle); -#undef TWEEN - P_SetThingPosition(players[i].mo); - - players[i].drawangle = info->playerinfo[i].player.drawangle + FixedMul((INT32) (next_info->playerinfo[i].player.drawangle - info->playerinfo[i].player.drawangle), tweenvalue); - - players[i].mo->sprite = info->playerinfo[i].mobj.sprite; - players[i].mo->sprite2 = info->playerinfo[i].mobj.sprite2; - players[i].mo->frame = info->playerinfo[i].mobj.frame; - - players[i].mo->hitlag = info->playerinfo[i].mobj.hitlag; - - players[i].realtime = info->playerinfo[i].player.realtime; - // Genuinely CANNOT be fucked. I can redo lua and I can redo netsaves but I draw the line at this abysmal hack. - /*for (j = 0; j < NUMKARTSTUFF; j++) - players[i].kartstuff[j] = info->playerinfo[i].player.kartstuff[j];*/ - } - - for (i = splitscreen; i >= 0; i--) - P_ResetCamera(&players[displayplayers[i]], &camera[i]); -} - -void G_ConfirmRewind(tic_t rewindtime) -{ - SINT8 i; - tic_t j; - boolean oldmenuactive = menuactive, oldsounddisabled = sound_disabled; - - INT32 olddp1 = displayplayers[0], olddp2 = displayplayers[1], olddp3 = displayplayers[2], olddp4 = displayplayers[3]; - UINT8 oldss = splitscreen; - - menuactive = false; // Prevent loops - - CV_StealthSetValue(&cv_renderview, 0); - - if (rewindtime <= starttime) - { - demo.rewinding = true; // this doesn't APPEAR to cause any misery, and it allows us to prevent running all the wipes again - G_DoPlayDemo(NULL); // Restart the current demo - } - else - { - rewind_t *rewind; - sound_disabled = true; // Prevent sound spam - demo.rewinding = true; - - rewind = CL_RewindToTime(rewindtime); - - if (rewind) - { - demobuf.p = demobuf.buffer + rewind->demopos; - memcpy(oldcmd, rewind->oldcmd, sizeof (oldcmd)); - memcpy(oldghost, rewind->oldghost, sizeof (oldghost)); - paused = false; - } - else - { - demo.rewinding = true; - G_DoPlayDemo(NULL); // Restart the current demo - } - } - - for (j = 0; j < rewindtime && leveltime < rewindtime; j++) - { - G_Ticker((j % NEWTICRATERATIO) == 0); - } - - demo.rewinding = false; - menuactive = oldmenuactive; // Bring the menu back up - sound_disabled = oldsounddisabled; // Re-enable SFX - - wipegamestate = gamestate; // No fading back in! - - COM_BufInsertText("renderview on\n"); - - splitscreen = oldss; - displayplayers[0] = olddp1; - displayplayers[1] = olddp2; - displayplayers[2] = olddp3; - displayplayers[3] = olddp4; - R_ExecuteSetViewSize(); - G_ResetViews(); - - for (i = splitscreen; i >= 0; i--) - P_ResetCamera(&players[displayplayers[i]], &camera[i]); -} - // // G_RecordDemo // @@ -1885,7 +1642,7 @@ void G_RecordDemo(const char *name) functions will check if they overran the buffer, but it should be safe enough because they'll think there's less memory than there actually is and stop early. */ - const size_t deadspace = 1024; + const size_t deadspace = 2048; I_Assert(demobuf.size > deadspace); demobuf.size -= deadspace; demobuf.end -= deadspace; @@ -2368,6 +2125,8 @@ void G_BeginRecording(void) WRITEUINT8(demobuf.p, player->skin); WRITEUINT8(demobuf.p, player->lastfakeskin); + WRITEUINT8(demobuf.p, player->team); + // Color demobuf.p += copy_fixed_buf(demobuf.p, skincolors[player->skincolor].name, g_buffer_sizes.color_name); @@ -2997,8 +2756,6 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum) boolean skiperrors = true; #endif - G_InitDemoRewind(); - gtname[MAXGAMETYPELENGTH-1] = '\0'; if (deflumpnum != LUMPERROR) @@ -3361,22 +3118,6 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum) // Load "mapmusrng" used for altmusic selection mapmusrng = READUINT8(demobuf.p); - // Sigh ... it's an empty demo. - if (*demobuf.p == DEMOMARKER) - { - snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - M_StartMessage("Demo Playback", msg, NULL, MM_NOTHING, NULL, "Return to Menu"); - Z_Free(demo.skinlist); - demo.skinlist = NULL; - Z_Free(pdemoname); - Z_Free(demobuf.buffer); - demo.playback = false; - return; - } - - Z_Free(pdemoname); - memset(&oldcmd,0,sizeof(oldcmd)); memset(&oldghost,0,sizeof(oldghost)); memset(&ghostext,0,sizeof(ghostext)); @@ -3500,6 +3241,8 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum) demo.currentskinid[p] = 0; lastfakeskin[p] = READUINT8(demobuf.p); + players[p].team = READUINT8(demobuf.p); + // Color demobuf.p += copy_fixed_buf(color, demobuf.p, g_buffer_sizes.color_name); for (i = 0; i < numskincolors; i++) @@ -3603,9 +3346,26 @@ void G_DoPlayDemoEx(const char *defdemoname, lumpnum_t deflumpnum) players[p].lastfakeskin = lastfakeskin[p]; } + // Sigh ... it's an empty demo. + if (*demobuf.p == DEMOMARKER) + { + snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname); + CONS_Alert(CONS_ERROR, "%s", msg); + M_StartMessage("Demo Playback", msg, NULL, MM_NOTHING, NULL, "Return to Menu"); + Z_Free(demo.skinlist); + demo.skinlist = NULL; + Z_Free(pdemoname); + Z_Free(demobuf.buffer); + demo.playback = false; + return; + } + + Z_Free(pdemoname); + demo.deferstart = true; - CV_StealthSetValue(&cv_playbackspeed, 1); + if (demo.simplerewind == DEMO_REWIND_OFF) + CV_StealthSetValue(&cv_playbackspeed, 1); } void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) @@ -3752,14 +3512,6 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) p++; // mapmusrng - if (*p == DEMOMARKER) - { - CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), defdemoname); - Z_Free(skinlist); - P_SaveBufferFree(buffer); - return; - } - p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once // any invalidating flags? @@ -3783,6 +3535,8 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) ghskin = &skins[skinlist[i].mapping]; p++; // lastfakeskin + p++; // team + // Color p += copy_fixed_buf(color, p, ghostsizes.color_name); @@ -3805,6 +3559,14 @@ void G_AddGhost(savebuffer_t *buffer, const char *defdemoname) return; } + if (*p == DEMOMARKER) + { + CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), defdemoname); + Z_Free(skinlist); + P_SaveBufferFree(buffer); + return; + } + gh = static_cast(Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL)); gh->sizes = ghostsizes; @@ -4167,7 +3929,7 @@ boolean G_CheckDemoStatus(void) // Keep the demo open and don't boot to intermission // YET, pause demo playback. if (!demo.waitingfortally && modeattacking && exitcountdown) - demo.waitingfortally = true; + ; else if (!demo.attract) G_FinishExitLevel(); else @@ -4185,6 +3947,8 @@ boolean G_CheckDemoStatus(void) D_SetDeferredStartTitle(true); } + demo.waitingfortally = true; // if we've returned early for some reason... + return true; } @@ -4229,6 +3993,13 @@ void G_SaveDemo(void) if (currentMenu == &TitleEntryDef) M_ClearMenus(true); + if (!leveltime) + { + // Why would you save if nothing has been recorded + G_ResetDemoRecording(); + return; + } + // Ensure extrainfo pointer is always available, even if no info is present. if (demoinfo_p && *(UINT32 *)demoinfo_p == 0) { diff --git a/src/g_demo.h b/src/g_demo.h index e3c4c74ff..4ce05c2a3 100644 --- a/src/g_demo.h +++ b/src/g_demo.h @@ -84,7 +84,7 @@ struct demovars_s { boolean recording, playback, timing; UINT16 version; // Current file format of the demo being played UINT8 attract; // Attract demo can be cancelled by any key - boolean rewinding; // Rewind in progress + UINT8 simplerewind; boolean loadfiles, ignorefiles; // Demo file loading options boolean quitafterplaying; // quit after playing a demo from cmdline @@ -172,6 +172,8 @@ extern UINT8 demo_writerng; boolean G_CompatLevel(UINT16 level); // Record/playback tics +boolean G_ConsiderEndingDemoRead(void); +boolean G_ConsiderEndingDemoWrite(void); void G_ReadDemoExtraData(void); void G_WriteDemoExtraData(void); void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum); @@ -186,11 +188,6 @@ void G_ConsAllGhostTics(void); void G_ConsGhostTic(INT32 playernum); void G_GhostTicker(void); -void G_InitDemoRewind(void); -void G_StoreRewindInfo(void); -void G_PreviewRewind(tic_t previewtime); -void G_ConfirmRewind(tic_t rewindtime); - struct DemoBufferSizes { size_t player_name; @@ -250,6 +247,13 @@ typedef enum DEMO_ATTRACT_CREDITS } demoAttractMode_t; +typedef enum +{ + DEMO_REWIND_OFF = 0, + DEMO_REWIND_RESUME, + DEMO_REWIND_PAUSE +} demoRewindMode_t; + void G_SyncDemoParty(INT32 rem, INT32 newsplitscreen); #ifdef __cplusplus diff --git a/src/g_game.c b/src/g_game.c index 03af98262..a081e437b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -297,9 +297,10 @@ boolean franticitems; // Frantic items currently enabled? boolean g_teamplay; // Voting system -UINT16 g_voteLevels[4][2]; // Levels that were rolled by the host +UINT16 g_voteLevels[VOTE_NUM_LEVELS][2]; // Levels that were rolled by the host SINT8 g_votes[VOTE_TOTAL]; // Each player's vote SINT8 g_pickedVote; // What vote the host rolls +boolean g_votes_striked[VOTE_NUM_LEVELS]; // Which levels were striked from votes? // Server-sided, synched variables tic_t wantedcalcdelay; // Time before it recalculates WANTED @@ -311,6 +312,7 @@ SINT8 spbplace; // SPB exists, give the person behind better items boolean rainbowstartavailable; // Boolean, keeps track of if the rainbow start was gotten tic_t linecrossed; // For Time Attack boolean inDuel; // Boolean, keeps track of if it is a 1v1 +UINT8 overtimecheckpoints; // Duel overtime speedups! // Client-sided, unsynched variables (NEVER use in anything that needs to be synced with other players) tic_t bombflashtimer = 0; // Cooldown before another FlashPal can be intialized by a bomb exploding near a displayplayer. Avoids seizures. @@ -1243,26 +1245,36 @@ void G_StartTitleCard(void) // prepare status bar ST_startTitleCard(); // <-- always must be called to init some variables - // The title card has been disabled for this map. - // Oh well. - if (demo.rewinding || !G_IsTitleCardAvailable()) - { - WipeStageTitle = false; + if (demo.simplerewind) return; + + sfxenum_t kstart = 0; + + if (K_CheckBossIntro() == true) + { + kstart = sfx_ssa021; } + else if (encoremode) + { + kstart = sfx_ruby2; + } + + if (kstart) + { + // Play the guaranteed alt sounds + S_StartSound(NULL, kstart); + } + + if (!G_IsTitleCardAvailable()) + return; // start the title card WipeStageTitle = (gamestate == GS_LEVEL); - // play the sound - if (WipeStageTitle) + if (WipeStageTitle && !kstart) { - sfxenum_t kstart = sfx_kstart; - if (K_CheckBossIntro() == true) - kstart = sfx_ssa021; - else if (encoremode == true) - kstart = sfx_ruby2; - S_StartSound(NULL, kstart); + // Play the standard titlecard sound + S_StartSound(NULL, sfx_kstart); } } @@ -1437,13 +1449,7 @@ boolean G_Responder(event_t *ev) { paused = !paused; - if (demo.rewinding) - { - G_ConfirmRewind(leveltime); - paused = true; - S_PauseAudio(); - } - else if (paused) + if (paused) S_PauseAudio(); else S_ResumeAudio(); @@ -1554,7 +1560,7 @@ boolean G_CouldView(INT32 playernum) return false; // SRB2Kart: we have no team-based modes, YET... - if (G_GametypeHasTeams()) + if (G_GametypeHasTeams() && !demo.playback) { if (players[consoleplayer].spectator == false && player->team != players[consoleplayer].team) return false; @@ -2148,6 +2154,14 @@ void G_Ticker(boolean run) if (g_fast_forward == 0) { + // Not "rewinding" anymore. + if (demo.simplerewind == DEMO_REWIND_PAUSE) + { + paused = true; + S_PauseAudio(); + } + demo.simplerewind = DEMO_REWIND_OFF; + // Next fast-forward is unlimited. g_fast_forward_clock_stop = INFTICS; } @@ -2262,6 +2276,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) INT32 kickstartaccel; INT32 checkpointId; boolean enteredGame; + tic_t spectatewait; UINT8 lastsafelap; UINT8 lastsafecheatcheck; UINT16 bigwaypointgap; @@ -2349,8 +2364,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) bot = players[player].bot; botdifficulty = players[player].botvars.difficulty; - cangrabitems = players[player].cangrabitems; - botdiffincrease = players[player].botvars.diffincrease; botrival = players[player].botvars.rival; @@ -2436,13 +2449,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) tallyactive = false; cangrabitems = 0; - if (gametyperules & GTR_SPHERES - || gametyperules & GTR_CATCHER - || G_TimeAttackStart() - || gametype == GT_TUTORIAL - || !M_NotFreePlay() - || K_GetNumWaypoints() == 0) - cangrabitems = EARLY_ITEM_FLICKER; } else { @@ -2498,6 +2504,8 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) { tally = players[player].tally; } + + cangrabitems = players[player].cangrabitems; } spectatorReentry = (betweenmaps ? 0 : players[player].spectatorReentry); @@ -2551,6 +2559,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) checkpointId = players[player].checkpointId; enteredGame = players[player].enteredGame; + spectatewait = players[player].spectatewait; p = &players[player]; memset(p, 0, sizeof (*p)); @@ -2624,6 +2633,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->karthud[khud_fault] = khudfault; p->kickstartaccel = kickstartaccel; p->checkpointId = checkpointId; + p->spectatewait = spectatewait; p->ringvolume = 255; p->ringtransparency = 255; @@ -3868,6 +3878,12 @@ tryAgain: continue; } + if (numPlayers == 2 && gametype == GT_RACE && ((mapheaderinfo[i]->levelflags & LF_SECTIONRACE) == LF_SECTIONRACE)) + { + // Duel doesn't support sprints. + continue; + } + // Only care about restrictions if the host is a listen server. if (!dedicated) { @@ -4090,6 +4106,7 @@ doremove: // Next map apparatus struct roundqueue roundqueue; +struct menuqueue menuqueue; void G_MapSlipIntoRoundQueue(UINT8 position, UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted) { @@ -5373,7 +5390,7 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr S_ResumeAudio(); } - prevencoremode = ((!Playing()) ? false : encoremode); + prevencoremode = encoremode; encoremode = pencoremode; legitimateexit = false; // SRB2Kart @@ -5415,6 +5432,8 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr players[i].xtralife = 0; players[i].totalring = 0; players[i].score = 0; + if (roundqueue.position == 0) // Don't unassign teams in tournament play + players[i].team = TEAM_UNASSIGNED; } if (resetplayer || !(gametyperules & GTR_CHECKPOINTS && map == gamemap)) @@ -5823,7 +5842,7 @@ boolean G_GetExitGameFlag(void) // Same deal with retrying. void G_SetRetryFlag(void) { - if (retrying == false) + if (retrying == false && grandprixinfo.gp) { grandprixinfo.rank.continuesUsed++; } diff --git a/src/g_game.h b/src/g_game.h index 14ee6494a..7996a8e61 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -75,6 +75,15 @@ extern struct roundqueue roundentry_t entries[ROUNDQUEUE_MAX]; // Entries in the round queue } roundqueue; +extern struct menuqueue +{ + // Degenerate version of roundqueue exclusively for menu use. + UINT8 size; + UINT8 sending; + UINT8 anchor; + roundentry_t entries[ROUNDQUEUE_MAX]; +} menuqueue; + void G_MapSlipIntoRoundQueue(UINT8 position, UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted); void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted); void G_GPCupIntoRoundQueue(cupheader_t *cup, UINT8 setgametype, boolean setencore); diff --git a/src/info.c b/src/info.c index c33e6b943..d017dda40 100644 --- a/src/info.c +++ b/src/info.c @@ -3640,7 +3640,7 @@ state_t states[NUMSTATES] = {SPR_S_SP, FF_ANIMATE|FF_SEMIBRIGHT, -1, {NULL}, 3, 2, S_NULL}, // S_SLSTMACE // MT_SEALEDSTAR_BUMPER - {SPR_SBMP, 0|FF_FULLBRIGHT, -1, {NULL}, 2, 8, S_SEALEDSTAR_BUMPER}, // S_SEALEDSTAR_BUMPER + {SPR_SBMP, 0|FF_FULLBRIGHT, -1, {A_GenericBumper}, 0, 56, S_SEALEDSTAR_BUMPER}, // S_SEALEDSTAR_BUMPER {SPR_SBMP, 1|FF_ANIMATE|FF_FULLBRIGHT, 8, {NULL}, 1, 2, S_SEALEDSTAR_BUMPER}, // S_SEALEDSTAR_BUMPERHIT // MT_SSCHAIN_SPAWNER @@ -13585,8 +13585,8 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL, // xdeathstate sfx_None, // deathsound 0, // speed - 95*FRACUNIT, // radius - 95*FRACUNIT, // height + 108*FRACUNIT, // radius + 50*FRACUNIT, // height 0, // display offset 100, // mass 0, // damage @@ -22233,7 +22233,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = 0, // mass 0, // damage sfx_None, // activesound - MF_SPECIAL|MF_NOGRAVITY, // flags + MF_NOGRAVITY|MF_SOLID, // flags S_NULL // raisestate }, { // MT_SSCHAIN_SPAWNER diff --git a/src/info.h b/src/info.h index d7495e852..2be47f164 100644 --- a/src/info.h +++ b/src/info.h @@ -289,6 +289,7 @@ enum actionnum A_MAKESSCANDLE, A_HOLOGRAMRANDOMTRANSLUCENCY, A_SSCHAINSHATTER, + A_GENERICBUMPER, NUMACTIONS }; @@ -557,6 +558,7 @@ void A_BlendEyePuyoHack(); void A_MakeSSCandle(); void A_HologramRandomTranslucency(); void A_SSChainShatter(); +void A_GenericBumper(); extern boolean actionsoverridden[NUMACTIONS]; diff --git a/src/k_bot.cpp b/src/k_bot.cpp index 8e4f2f597..20107a159 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -2077,6 +2077,11 @@ void K_UpdateBotGameplayVars(player_t *player) if (cv_levelskull.value) player->botvars.difficulty = MAXBOTDIFFICULTY; + if (K_InRaceDuel()) + player->botvars.rival = true; + else if (grandprixinfo.gp != true) + player->botvars.rival = false; + player->botvars.rubberband = K_UpdateRubberband(player); player->botvars.turnconfirm += player->cmd.bot.turnconfirm; diff --git a/src/k_collide.cpp b/src/k_collide.cpp index 4e1a3451b..d42be35d0 100644 --- a/src/k_collide.cpp +++ b/src/k_collide.cpp @@ -75,7 +75,7 @@ boolean K_BananaBallhogCollide(mobj_t *t1, mobj_t *t2) if (t1->type == MT_BALLHOGBOOM && t2->type == MT_BALLHOGBOOM) return true; // Ballhogs don't collide with eachother - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (t2->player) @@ -178,7 +178,7 @@ boolean K_EggItemCollide(mobj_t *t1, mobj_t *t2) if (t1->health <= 0 || t2->health <= 0) return true; - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (!P_CanPickupItem(t2->player, PICKUP_EGGBOX)) @@ -434,7 +434,7 @@ boolean K_LandMineCollide(mobj_t *t1, mobj_t *t2) if (t1->health <= 0 || t2->health <= 0) return true; - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (t2->player) @@ -544,7 +544,7 @@ boolean K_DropTargetCollide(mobj_t *t1, mobj_t *t2) if (t2->player && (t2->player->hyudorotimer || t2->player->justbumped)) return true; - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (draggeddroptarget && P_MobjWasRemoved(draggeddroptarget)) @@ -777,6 +777,12 @@ static inline BlockItReturn_t PIT_LightningShieldAttack(mobj_t *thing) return BMIT_CONTINUE; } + // see if it went over / under + if (lightningSource->z - lightningDist > thing->z + thing->height) + return BMIT_CONTINUE; // overhead + if (lightningSource->z + lightningSource->height + lightningDist < thing->z) + return BMIT_CONTINUE; // underneath + #if 0 if (P_CheckSight(lightningSource, thing) == false) { @@ -967,6 +973,7 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim) attackerPlayer->spindashboost = 0; attackerPlayer->sneakertimer = 0; attackerPlayer->panelsneakertimer = 0; + attackerPlayer->weaksneakertimer = 0; attackerPlayer->instaWhipCharge = 0; attackerPlayer->flashing = 0; @@ -1036,7 +1043,14 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim) } else if (victim->type == MT_DROPTARGET || victim->type == MT_DROPTARGET_SHIELD) { - K_DropTargetCollide(victim, shield); + if (K_TryPickMeUp(attacker, victim, true)) + { + shield->hitlag = attacker->hitlag; // players hitlag is handled in K_TryPickMeUp, and we need to set for the shield too + } + else + { + K_DropTargetCollide(victim, shield); + } return true; } else @@ -1053,8 +1067,13 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim) shield->extravalue1 = 1; } - if (P_DamageMobj(victim, shield, attacker, 1, DMG_NORMAL)) + if (K_TryPickMeUp(attacker, victim, true)) { + shield->hitlag = attacker->hitlag; // players hitlag is handled in K_TryPickMeUp, and we need to set for the shield too + } + else + { + P_DamageMobj(victim, shield, attacker, 1, DMG_NORMAL); K_AddHitLag(attacker, attackerHitlag, false); shield->hitlag = attacker->hitlag; } @@ -1068,7 +1087,7 @@ boolean K_KitchenSinkCollide(mobj_t *t1, mobj_t *t2) if (((t1->target == t2) || (!(t2->flags & (MF_ENEMY|MF_BOSS)) && (t1->target == t2->target))) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0))) return true; - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (t2->player) @@ -1193,7 +1212,7 @@ boolean K_PvPTouchDamage(mobj_t *t1, mobj_t *t2) { auto shouldSteal = [](mobj_t *t1, mobj_t *t2) { - return ((t1->player->sneakertimer > 0 || t1->player->panelsneakertimer > 0) + return ((t1->player->sneakertimer > 0 || t1->player->panelsneakertimer > 0 || t1->player->weaksneakertimer > 0) && !P_PlayerInPain(t1->player) && (t1->player->flashing == 0)); }; diff --git a/src/k_grandprix.cpp b/src/k_grandprix.cpp index d4b96a257..24a197f39 100644 --- a/src/k_grandprix.cpp +++ b/src/k_grandprix.cpp @@ -67,31 +67,6 @@ INT16 K_CalculateGPRankPoints(UINT16 exp, UINT8 position, UINT8 numplayers) points = exp; - // Give bonus to high-ranking players, depending on player count - // This rounds out the point gain when you get 1st every race, - // and gives bots able to catch up in points if a player gets an early lead. - // The maximum points you can get in a cup is: ((number of players - 1) + (max extra points)) * (number of races) - // 8P: (7 + 5) * 5 = 60 maximum points - // 12P: (11 + 5) * 5 = 80 maximum points - // 16P: (15 + 5) * 5 = 100 maximum points - switch (numplayers) - { - case 0: case 1: case 2: // 1v1 - break; // No bonus needed. - case 3: case 4: // 3-4P - if (position == 1) { points += 5; } // 1st gets +1 extra point - break; - case 5: case 6: // 5-6P - if (position == 1) { points += 10; } // 1st gets +3 extra points - // else if (position == 2) { points += 4; } // 2nd gets +1 extra point - break; - default: // Normal matches - if (position == 1) { points += 10; } // 1st gets +5 extra points - // else if (position == 2) { points += 5; } // 2nd gets +3 extra points - // else if (position == 3) { points += 2; } // 3rd gets +1 extra point - break; - } - // somehow underflowed? if (points < 0) { diff --git a/src/k_hud.cpp b/src/k_hud.cpp index 1aae11443..4e683c677 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -235,6 +235,13 @@ static patch_t *kp_team_underlay[2][2]; static patch_t *kp_team_minihead; static patch_t *kp_team_you; +static patch_t *kp_duel_foe; +static patch_t *kp_duel_you; +static patch_t *kp_duel_sticker; +static patch_t *kp_duel_under; +static patch_t *kp_duel_over; +static patch_t *kp_duel_margin[24]; + patch_t *kp_autoroulette; patch_t *kp_autoring; @@ -1061,6 +1068,20 @@ void K_LoadKartHUDGraphics(void) HU_UpdatePatch(&kp_team_underlay[1][1], "TEAM4UR"); HU_UpdatePatch(&kp_team_minihead, "TEAM4H"); HU_UpdatePatch(&kp_team_you, "TEAM_YOU"); + + HU_UpdatePatch(&kp_duel_foe, "DUEL_FOE"); + HU_UpdatePatch(&kp_duel_sticker, "DUEL_S"); + HU_UpdatePatch(&kp_duel_under, "DUEL_B"); + HU_UpdatePatch(&kp_duel_over, "DUEL_B2"); + HU_UpdatePatch(&kp_duel_you, "DUEL_YOU"); + + sprintf(buffer, "DUELMBxx"); + for (i = 0; i < MARGINLEVELS; i++) + { + buffer[6] = '0'+(i/10); + buffer[7] = '0'+(i%10); + HU_UpdatePatch(&kp_duel_margin[i], "%s", buffer); + } } // For the item toggle menu @@ -1515,6 +1536,46 @@ void K_DrawLikeMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, p ); } +void K_DrawMapAsFace(INT32 x, INT32 y, UINT32 flags, UINT16 map, const UINT8 *colormap) +{ + const fixed_t iconHeight = (14 << FRACBITS); + const fixed_t iconWidth = (iconHeight * 320) / 200; + INT32 unit = 1; + fixed_t mul = FRACUNIT; + if (flags & V_NOSCALESTART) + { + unit = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy); + mul = 1; + } + + V_DrawFill( + x, + y, + 16 * unit, + 16 * unit, + (flags & ~V_FLIP) + ); + + V_SetClipRect( + (x + unit) * mul, + (y + unit) * mul, + (14 * unit) * mul, + (14 * unit) * mul, + (flags & ~V_FLIP) + ); + + K_DrawMapThumbnail( + ((x + unit) * FRACUNIT) - (iconWidth - iconHeight)/2, + ((y + unit) * FRACUNIT), + iconWidth, + flags, + map, + colormap + ); + + V_ClearClipRect(); +} + // see also K_DrawNameTagItemSpy static void K_drawKartItem(void) { @@ -2970,6 +3031,9 @@ static boolean K_drawKartPositionFaces(void) if (!LUA_HudEnabled(hud_minirankings)) return false; // Don't proceed but still return true for free play above if HUD is disabled. + if (K_InRaceDuel()) + return false; + switch (r_splitscreen) { case 0: @@ -3229,11 +3293,292 @@ INT32 K_GetTransFlagFromFixed(fixed_t value) } } +static tic_t duel_lastleveltime = 0; +static INT32 duel_marginanim = 0; +static INT32 duel_lastmargin = 0; +static INT32 youheight = 0; + +static void K_drawKartDuelScores(void) +{ + if (!K_InRaceDuel()) + return; + + using srb2::Draw; + + player_t *foe = K_DuelOpponent(stplyr); + + INT32 basex = 0; + INT32 basey = 40; + INT32 flags = V_SNAPTOLEFT|V_HUDTRANS|V_SLIDEIN; + + // score bars, here barheight is the size of bars at tied score + INT32 barx = 8; + INT32 bary = 61; + INT32 barheight = 48; + INT32 barwidth = 6; + + // portraits + INT32 foex = 16; + INT32 foey = 21; + INT32 youx = 16; + INT32 youy = 85; + + // scores + INT32 foescorex = 16; + INT32 foescorey = 38; + INT32 youscorex = 16; + INT32 youscorey = 69; + + Draw::Font scorefont = Draw::Font::kThinTimer; + + UINT8 ri = 6; + INT32 youfill = skincolors[stplyr->skincolor].ramp[ri]; + INT32 foefill = skincolors[foe->skincolor].ramp[ri]; + + V_DrawScaledPatch(basex, basey, flags, kp_duel_sticker); + + INT32 scoredelta = stplyr->duelscore - foe->duelscore; + INT32 clutchscore = DUELWINNINGSCORE - 1; // we want the bar to be full when NEXT checkpoint wins... + INT32 savemargin = 3; // ...minus a little bit. + + if (leveltime/(TICRATE/2) % 2) + savemargin += ((leveltime/2)%2); + + if (clutchscore == 0) + clutchscore = 1; // Fuck it, just don't crash + + INT32 targetyouheight = barheight*abs(clutchscore+scoredelta)/clutchscore; + + if (targetyouheight == 0) + { + targetyouheight = savemargin; + } + else if (targetyouheight >= 2*barheight) + { + targetyouheight = 2*barheight - savemargin; + } + + if (leveltime != duel_lastleveltime) + { + INT32 slide = std::max(1, abs(targetyouheight - youheight)/3); + if (targetyouheight > youheight) + youheight += slide; + else if (targetyouheight < youheight) + youheight -= slide; + } + + INT32 foeheight = 2*barheight-youheight; // barheight is a single tied bar, so total height of the full gauge is 2x barheight + + V_DrawFill(basex+barx, basey+bary-barheight, barwidth, foeheight, foefill|flags); + V_DrawFill(basex+barx, basey+bary-barheight+foeheight, barwidth, youheight, youfill|flags); + + V_DrawScaledPatch(basex, basey, flags, kp_duel_under); + V_DrawScaledPatch(basex, basey-barheight+foeheight, flags, kp_duel_over); + V_DrawScaledPatch(basex, basey, flags, kp_duel_foe); + V_DrawScaledPatch(basex, basey, flags, kp_duel_you); + + Draw foenum = Draw(basex+foescorex, basey+foescorey).flags(flags).font(scorefont).align(Draw::Align::kLeft); + Draw younum = Draw(basex+youscorex, basey+youscorey).flags(flags).font(scorefont).align(Draw::Align::kLeft); + + if (abs(scoredelta) == clutchscore && ((leveltime % 2) || cv_reducevfx.value)) + { + if (foe->duelscore > stplyr->duelscore) + foenum = foenum.colorize(SKINCOLOR_GOLD); + else + younum = younum.colorize(SKINCOLOR_GOLD); + } + + foenum.text("{}", foe->duelscore); + younum.text("{}", stplyr->duelscore); + + // minirankings shamelessly copypasted because i know that shit works already + // and SURELY we will never need to use this somewhere else, right? + + UINT8 workingskin; + UINT8 *colormap; + INT32 xoff, yoff, flipflag, skinflags; + + for (UINT8 draw = 0; draw < 2; draw++) + { + UINT8 drawme = draw ? (stplyr - players) : (foe - players); + UINT8 drawx = basex + (draw ? youx : foex); + UINT8 drawy = basey + (draw ? youy : foey); + + if (!playeringame[drawme] || players[drawme].spectator) + continue; + + if (!players[drawme].mo || P_MobjWasRemoved(players[drawme].mo)) + continue; + + skinflags = (demo.playback) + ? demo.skinlist[demo.currentskinid[drawme]].flags + : skins[players[drawme].skin].flags; + + // Flip SF_IRONMAN portraits, but only if they're transformed + if (skinflags & SF_IRONMAN + && !(players[drawme].charflags & SF_IRONMAN) ) + { + flipflag = V_FLIP|V_VFLIP; // blonic flip + xoff = yoff = 16; + } else + { + flipflag = 0; + xoff = yoff = 0; + } + + if ((skin_t*)players[drawme].mo->skin) + workingskin = (skin_t*)players[drawme].mo->skin - skins; + else + workingskin = players[drawme].skin; + + colormap = R_GetTranslationColormap(workingskin, static_cast(players[drawme].mo->color), GTC_CACHE); + if (players[drawme].mo->colorized) + colormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(players[drawme].mo->color), GTC_CACHE); + else + colormap = R_GetTranslationColormap(workingskin, static_cast(players[drawme].mo->color), GTC_CACHE); + + V_DrawMappedPatch(drawx+xoff, drawy+yoff, flags|flipflag, faceprefix[workingskin][FACE_RANK], colormap); + } + + // Dogshit. Should have just figured out how to do log base 5 in C++. + // However, I think this works anyway. + // I did my best to comment this but the algorithm is honestly just bad and hard to + // reason about. Please don't try to maintain this, just yell at me if it needs any + // adjustments. -Tyron 2025-05-29 + + // DESIGN INTENT: Create realistic-looking Puyo garbage stacks, while using the + // leading garbage symbol as an indicator of the current Margin Boost value. + + INT32 rawmargin = overtimecheckpoints; // The actual Margin Boost value. + INT32 boostspersymbol = 3; // How many boosts should it take to see a new symbol? + // rawmargin = (leveltime/10)%(3*boostspersymbol); + + if (duel_lastleveltime != leveltime) // Trigger the "slide" animation when rawmargin changes. + { + duel_marginanim = std::min(duel_marginanim + 1, 100); // not magic just arbitrary + if (duel_lastmargin != rawmargin) + { + duel_marginanim = 0; + duel_lastmargin = rawmargin; + } + } + + duel_lastleveltime = leveltime; + + // CONS_Printf("=== RAWMARGIN %d\n", rawmargin); + + if (rawmargin == 0) + return; + + rawmargin--; // Start at 0, idiot + + // We're invoking the RNG to get a slightly chaotic symbol distribution, + // but we're a HUD hook, so we need to keep the results of the call consistent. + P_SetRandSeed(PR_NUISANCE, 69 + rawmargin); + + INT32 highsymbol = rawmargin/boostspersymbol + 1; // Highest symbol that should appear. + INT32 symbolsperupgrade = 5; // What is each symbol worth relative to each other? Like, 5 Stars = 1 Moon, etc. + + // Okay, so we would LOVE to do this in a way that isn't a big clusterfuck, like just + // doing rawmargin^3 and then subtracting powers of 5 out of that. Unfortunately, UINT64 + // is too small for the values that feel intuitively right here, so we have to do some of + // the math on a limited set of symbols, then shift up. This is the concept of "symbol + // headroom" that's in use here. + // + // (Note that Puyo~n uses a super inconsistent symbol table, probably to avoid this problem, + // but we're assholes and want things to feel logically consistent I guess? + // I dunno. I sort of feel like I should have just directly used the Puyo~n garbage table and + // avoided most of this, LOL) + + INT32 symbolheadroom = 5; // Maximum # symbols we can "step down". + INT32 frac = rawmargin % boostspersymbol; // Used in intermediate calculations. + INT32 minsymbol = std::max(1, highsymbol - symbolheadroom); // The lowest symbol that should appear. + INT32 symbolheadroominuse = highsymbol - minsymbol; // The # of symbols we are stepping down. + INT32 minscore = std::pow(symbolsperupgrade, symbolheadroominuse+1); + INT32 maxscore = std::pow(symbolsperupgrade, symbolheadroominuse+2) - 1; + + // CONS_Printf("min %d max %d\n", minscore, maxscore); + + // We show the player successive combos with the same leading symbol, but we + // waht them to feel intuitively like they're increasing each time. + // Maxscore and minscore have been mapped to the correct power-of-N, so any + // point we pick between them will lead with the correct symbol once we adjust + // for symbol headroom. Pick a point that's appropriate for how "far" into the + // current symbol we are. + fixed_t lobound = FRACUNIT * frac / boostspersymbol; + fixed_t hibound = FRACUNIT * (frac+1) / boostspersymbol; + fixed_t roll = P_RandomRange(PR_NUISANCE, lobound, hibound); + + INT32 margin = Easing_Linear(roll, minscore, maxscore); // The score we're trying to draw a garbage stack for. + + INT32 margindigits[5]; + memset(margindigits, -1, sizeof(margindigits)); + + INT32 nummargindigits = 0; + + // CONS_Printf("margin %d min %d max %d roll %d shiu %d ms %d\n", margin, minscore, maxscore, roll, symbolheadroominuse, minsymbol); + + if (rawmargin/boostspersymbol >= (MARGINLEVELS-1)) + { + // Capped out. Show 5 Chaos. + nummargindigits = 5; + for(UINT8 i = 0; i < nummargindigits; i++) + { + margindigits[i] = MARGINLEVELS-1; + } + } + else + { + // Subtract powers of N from our chosen score to create a decent-enough-looking + // garbage stack, then queue up the right patches to be drawn, shifting all the math + // up by "minsymbol"—remember, once maxsymbol goes above symbolheadroom, we are doing + // a low-precision version of the math that ignores low enough symbols. + while (margin > 0) + { + INT32 significant_margin = 0; + for (UINT8 i = symbolheadroominuse+1; i >= 0; i--) + { + INT32 test = std::pow(symbolsperupgrade, i); + // CONS_Printf("testing %d (%d)\n", i, test); + if (margin >= test) + { + significant_margin = i; + break; + } + } + + INT32 index = significant_margin; + + margindigits[nummargindigits] = index + minsymbol - 1; + // CONS_Printf("digit %d %d\n", nummargindigits, margindigits[nummargindigits]); + + nummargindigits++; + + // CONS_Printf("margin was %d ", margin); + margin -= std::pow(symbolsperupgrade, index); + // CONS_Printf("is %d\n", margin); + + if (nummargindigits >= 3 + frac) + break; + } + } + + INT32 marginspacing = std::min(6, duel_marginanim); + INT32 marginx = ((nummargindigits-1) * marginspacing)/2; + + for (INT32 i = nummargindigits - 1; i >= 0; i--) + { + // CONS_Printf("draw %d - %d\n", i, margindigits[i]); + V_DrawScaledPatch(basex + marginx, basey, flags, kp_duel_margin[margindigits[i]]); + marginx -= marginspacing; + } +} + static INT32 easedallyscore = 0; static tic_t scorechangecooldown = 0; // Mildly ugly. Don't want to export this to khud when it's so nicely handled here, // but HUD hooks run at variable timing based on your actual framerate. -static tic_t lastleveltime = 0; +static tic_t teams_lastleveltime = 0; static void K_drawKartTeamScores(void) { @@ -3309,6 +3654,9 @@ static void K_drawKartTeamScores(void) UINT16 enemyscore = g_teamscores[enemies]; UINT16 totalscore = allyscore + enemyscore; + if (totalscore == 0) + return; + using srb2::Draw; srb2::Draw::Font scorefont = Draw::Font::kTimer; @@ -3360,11 +3708,11 @@ static void K_drawKartTeamScores(void) } else { - if (lastleveltime != leveltime) // Timing consistency + if (teams_lastleveltime != leveltime) // Timing consistency { INT32 delta = abs(easedallyscore - allyscore); // how wrong is display score? - if (scorechangecooldown == 0) + if (scorechangecooldown == 0 && delta) { if (allyscore > easedallyscore) { @@ -3387,7 +3735,7 @@ static void K_drawKartTeamScores(void) enemyscore = totalscore - allyscore; } - lastleveltime = leveltime; + teams_lastleveltime = leveltime; fixed_t enemypercent = FixedDiv(enemyscore*FRACUNIT, totalscore*FRACUNIT); // fixed_t allypercent = FixedDiv(allyscore*FRACUNIT, totalscore*FRACUNIT); @@ -3531,6 +3879,11 @@ static void K_drawKartTeamScores(void) */ } +static boolean K_DrawingLaps() +{ + return (numlaps != 1 && !K_InRaceDuel() && (UINT16)stplyr->exp != UINT16_MAX); +} + static boolean K_drawKartLaps(void) { INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN; @@ -3543,7 +3896,9 @@ static boolean K_drawKartLaps(void) // I do not understand the way this system of offsets is laid out at all, // so it's probably going to be pretty bad to maintain. Sorry. - if (numlaps != 1 && displayEXP != UINT16_MAX) + boolean drawinglaps = (numlaps != 1 && !K_InRaceDuel() && displayEXP != UINT16_MAX); + + if (drawinglaps) { if (r_splitscreen > 1) bump = 27; @@ -3551,7 +3906,7 @@ static boolean K_drawKartLaps(void) bump = 40; } - if (numlaps != 1) + if (drawinglaps) { if (r_splitscreen > 1) { @@ -3968,7 +4323,16 @@ static void K_drawKartAccessibilityIcons(boolean gametypeinfoshown, INT32 fx) } else { - fx = LAPS_X+44; + fx = LAPS_X + 33; + + if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS) + fx -= 14; + + if (K_DrawingLaps()) + { + fx += 30; + } + fy = LAPS_Y; if (R_GetViewNumber() & 1) // If we are not P1 or P3... { @@ -4862,7 +5226,7 @@ playertagtype_t K_WhichPlayerTag(player_t *p) } else if (p->bot) { - if (p->botvars.rival == true || cv_levelskull.value) + if ((p->botvars.rival == true || cv_levelskull.value) && (!K_InRaceDuel())) { return PLAYERTAG_RIVAL; } @@ -6717,10 +7081,8 @@ static void K_DrawGPRankDebugger(void) V_DrawThinString(0, 0, V_SNAPTOTOP|V_SNAPTOLEFT, va("POS: %d / %d", grandprixinfo.rank.position, RANK_NEUTRAL_POSITION)); - V_DrawThinString(0, 10, V_SNAPTOTOP|V_SNAPTOLEFT, - va("PTS: %d / %d", grandprixinfo.rank.winPoints, grandprixinfo.rank.totalPoints)); V_DrawThinString(0, 20, V_SNAPTOTOP|V_SNAPTOLEFT, - va("LAPS: %d / %d", grandprixinfo.rank.exp, grandprixinfo.rank.totalExp)); + va("EXP: %d / %d", grandprixinfo.rank.exp, grandprixinfo.rank.totalExp)); V_DrawThinString(0, 30, V_SNAPTOTOP|V_SNAPTOLEFT, va("CONTINUES: %d", grandprixinfo.rank.continuesUsed)); V_DrawThinString(0, 40, V_SNAPTOTOP|V_SNAPTOLEFT, @@ -6944,35 +7306,39 @@ static void K_DrawMessageFeed(void) text.font(Draw::Font::kMenu); - UINT8 x = BASEVIDWIDTH/2; - UINT8 y = 10; + UINT32 vw = vid.width / vid.dupx; + UINT32 vh = vid.height / vid.dupy; + + UINT32 x = vw / 2; + UINT32 y = 10; + SINT8 shift = 0; if (r_splitscreen >= 2) { text.font(Draw::Font::kThin); shift = -2; - x = BASEVIDWIDTH/4; + x = vw/4; y = 5; if (i % 2) - x += BASEVIDWIDTH/2; + x += vw / 2; if (i >= 2) - y += BASEVIDHEIGHT / 2; + y += vh / 2; } else if (r_splitscreen >= 1) { y = 5; if (i >= 1) - y += BASEVIDHEIGHT / 2; + y += vh / 2; } UINT16 sw = text.width(); - K_DrawSticker(x - sw/2, y, sw, 0, true); - Draw(x, y+shift).align(Draw::Align::kCenter).text(text); + K_DrawSticker(x - sw/2, y, sw, V_SNAPTOTOP|V_SNAPTOLEFT, true); + Draw(x, y+shift).align(Draw::Align::kCenter).flags(V_SNAPTOTOP|V_SNAPTOLEFT).text(text); } } @@ -7253,9 +7619,14 @@ void K_drawKartHUD(void) K_drawKartTeamScores(); } + if (K_InRaceDuel()) + { + K_drawKartDuelScores(); + } + if (LUA_HudEnabled(hud_gametypeinfo)) { - if (gametyperules & GTR_CIRCUIT) + if (gametyperules & GTR_CIRCUIT && !K_InRaceDuel()) { K_drawKartLaps(); gametypeinfoshown = true; diff --git a/src/k_hud.h b/src/k_hud.h index cd93a192b..6a991bddf 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -25,6 +25,8 @@ extern "C" { #define POS_DELAY_TIME 10 +#define MARGINLEVELS 24 + extern INT32 MINI_X, MINI_Y; struct trackingResult_t @@ -58,6 +60,7 @@ void K_drawKart4PTimestamp(void); void K_drawEmeraldWin(boolean overlay); void K_DrawMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, UINT16 map, const UINT8 *colormap); void K_DrawLikeMapThumbnail(fixed_t x, fixed_t y, fixed_t width, UINT32 flags, patch_t *patch, const UINT8 *colormap); +void K_DrawMapAsFace(INT32 x, INT32 y, UINT32 flags, UINT16 map, const UINT8 *colormap); void K_drawTargetHUD(const vector3_t *origin, player_t *player); void K_drawButton(fixed_t x, fixed_t y, INT32 flags, patch_t *button[2], boolean pressed); void K_drawButtonAnim(INT32 x, INT32 y, INT32 flags, patch_t *button[2], tic_t animtic); diff --git a/src/k_hud_track.cpp b/src/k_hud_track.cpp index 49abd8a5b..0ac07e641 100644 --- a/src/k_hud_track.cpp +++ b/src/k_hud_track.cpp @@ -143,6 +143,8 @@ struct TargetTracking return false; default: + if (K_IsPickMeUpItem(mobj->type)) + return false; return true; } } @@ -277,28 +279,17 @@ private: {{6, 2, {kp_spraycantarget_far[1]}, V_ADD}}, // 4P }}, }; - - case MT_JAWZ: - case MT_JAWZ_SHIELD: - case MT_ORBINAUT: - case MT_ORBINAUT_SHIELD: - case MT_DROPTARGET: - case MT_DROPTARGET_SHIELD: - case MT_LANDMINE: - case MT_BANANA: - case MT_BANANA_SHIELD: - case MT_GACHABOM: - case MT_EGGMANITEM: - case MT_EGGMANITEM_SHIELD: - case MT_BUBBLESHIELDTRAP: - return { - { // Near - {2, TICRATE/2, {kp_pickmeup}, 0}, // 1P - {{2, TICRATE/2, {kp_pickmeup}, 0}}, // 4P - }, - }; - default: + if (K_IsPickMeUpItem(mobj->type)) + { + return { + { // Near + {2, TICRATE/2, {kp_pickmeup}, 0}, // 1P + {{2, TICRATE/2, {kp_pickmeup}, 0}}, // 4P + }, + }; + } + return { { // Near {8, 2, {kp_capsuletarget_near[0]}}, // 1P @@ -902,32 +893,17 @@ void K_drawTargetHUD(const vector3_t* origin, player_t* player) if (tracking) { fixed_t itemOffset = 36*mobj->scale; - switch (mobj->type) + + if (K_IsPickMeUpItem(mobj->type)) { - case MT_JAWZ: - case MT_JAWZ_SHIELD: - case MT_ORBINAUT: - case MT_ORBINAUT_SHIELD: - case MT_DROPTARGET: - case MT_DROPTARGET_SHIELD: - case MT_LANDMINE: - case MT_BANANA: - case MT_BANANA_SHIELD: - case MT_GACHABOM: - case MT_BUBBLESHIELDTRAP: - case MT_EGGMANITEM: - case MT_EGGMANITEM_SHIELD: - if (stplyr->mo->eflags & MFE_VERTICALFLIP) - { - pos.z -= itemOffset; - } - else - { - pos.z += itemOffset; - } - break; - default: - break; + if (stplyr->mo->eflags & MFE_VERTICALFLIP) + { + pos.z -= itemOffset; + } + else + { + pos.z += itemOffset; + } } K_ObjectTracking(&tr.result, &pos, false); diff --git a/src/k_kart.c b/src/k_kart.c index 7322e941b..d8148f510 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -119,6 +119,27 @@ boolean K_DuelItemAlwaysSpawns(mapthing_t *mt) return !!(mt->thing_args[0]); } +boolean K_InRaceDuel(void) +{ + return (inDuel && (gametyperules & GTR_CIRCUIT) && !(mapheaderinfo[gamemap-1]->levelflags & LF_SECTIONRACE)) && !specialstageinfo.valid; +} + +player_t *K_DuelOpponent(player_t *player) +{ + if (!K_InRaceDuel()) + return player; // ???? + else + { + for (UINT8 i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] && !players[i].spectator && player - players != i) + return &players[i]; + } + } + + return player; // ???????????? +} + static void K_SpawnDuelOnlyItems(void) { mapthing_t *mt = NULL; @@ -145,6 +166,7 @@ void K_TimerReset(void) numbulbs = 1; inDuel = rainbowstartavailable = false; linecrossed = 0; + overtimecheckpoints = 0; timelimitintics = extratimeintics = secretextratime = 0; g_pointlimit = 0; } @@ -273,6 +295,9 @@ void K_TimerInit(void) introtime = (108) + 5; // 108 for rotation, + 5 for white fade numbulbs += (numPlayers-2); // Extra POSITION!! time } + + if (K_InRaceDuel()) + numlaps = 99; } } @@ -446,6 +471,9 @@ boolean K_IsPlayerScamming(player_t *player) if (!M_NotFreePlay()) return false; + if (!(gametyperules & GTR_CIRCUIT)) + return false; + // "Why 8?" Consistency // "Why 2000?" Vibes return (K_GetItemRouletteDistance(player, 8) < SCAMDIST); @@ -468,7 +496,10 @@ fixed_t K_GetKartGameSpeedScalar(SINT8 value) if (cv_4thgear.value && !netgame && (!demo.playback || !demo.netgame) && !modeattacking) value = 3; - return ((13 + (3*value)) << FRACBITS) / 16; + fixed_t base = ((13 + (3*value)) << FRACBITS) / 16; + fixed_t duel = overtimecheckpoints*(1<invincibilitytimer || player->hyudorotimer || player->sneakertimer || player->panelsneakertimer) + if (player->invincibilitytimer || player->hyudorotimer || player->sneakertimer || player->panelsneakertimer|| player->weaksneakertimer) return false; if (K_IsRidingFloatingTop(player)) return false; @@ -2990,7 +3021,7 @@ boolean K_ApplyOffroad(const player_t *player) boolean K_SlopeResistance(const player_t *player) { - if (player->invincibilitytimer || player->sneakertimer || player->panelsneakertimer || player->tiregrease || player->flamedash) + if (player->invincibilitytimer || player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || player->tiregrease || player->flamedash) return true; if (player->curshield == KSHIELD_TOP) return true; @@ -3131,6 +3162,7 @@ boolean K_WaterRun(mobj_t *mobj) if (mobj->player->invincibilitytimer || mobj->player->sneakertimer || mobj->player->panelsneakertimer + || mobj->player->weaksneakertimer || mobj->player->tiregrease || mobj->player->flamedash || mobj->player->speed > minspeed) @@ -3495,19 +3527,29 @@ static void K_GetKartBoostPower(player_t *player) UINT8 i; for (i = 0; i < player->numsneakers; i++) { - ADDBOOST(FRACUNIT*85/100, 8*FRACUNIT, HANDLESCALING+HANDLESCALING/3); // + 50% top speed, + 800% acceleration, +50% handling + ADDBOOST(FRACUNIT, 8*FRACUNIT, HANDLESCALING+HANDLESCALING/3); // + 100% top speed, + 800% acceleration, +50%(???) handling } } - if (player->panelsneakertimer) // Sneaker + if (player->panelsneakertimer) // Sneaker panel { UINT8 i; for (i = 0; i < player->numpanelsneakers; i++) { - ADDBOOST(FRACUNIT/2, 8*FRACUNIT, HANDLESCALING); // + 50% top speed, + 800% acceleration, +50% handling + ADDBOOST(FRACUNIT/2, 8*FRACUNIT, HANDLESCALING); // + 50% top speed, + 800% acceleration, +50%(???) handling } } + if (player->weaksneakertimer) // Rocket sneaker boost + { + UINT8 i; + for (i = 0; i < player->numweaksneakers; i++) + { + ADDBOOST((FRACUNIT*85)/100, 8*FRACUNIT, HANDLESCALING+HANDLESCALING/3); // + 85% top speed, + 800% acceleration, +50%(???) handling + } + } + //NOTE: The various sneaker booth strengths are also defined in K_DoSneaker()! + if (player->invincibilitytimer) // Invincibility { // S-Monitor: no extra % @@ -3564,7 +3606,7 @@ static void K_GetKartBoostPower(player_t *player) Easing_InCubic( player->overdrivepower, 0, - 5*FRACUNIT/10 + 75*FRACUNIT/100 ), Easing_InSine( player->overdrivepower, @@ -3640,7 +3682,7 @@ static void K_GetKartBoostPower(player_t *player) { fixed_t ringboost_base = FRACUNIT/4; if (player->overdrive) - ringboost_base += FRACUNIT/2; + ringboost_base += FRACUNIT/4; // This one's a little special: we add extra top speed per tic of ringboost stored up, to allow for Ring Box to really rocket away. // (We compensate when decrementing ringboost to avoid runaway exponential scaling hell.) fixed_t rb = FixedDiv(player->ringboost * FRACUNIT, max(FRACUNIT, K_RingDurationBoost(player))); @@ -3971,7 +4013,7 @@ SINT8 K_GetForwardMove(const player_t *player) return 0; } - if (player->sneakertimer || player->panelsneakertimer || player->spindashboost + if (player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || player->spindashboost || (((gametyperules & (GTR_ROLLINGSTART|GTR_CIRCUIT)) == (GTR_ROLLINGSTART|GTR_CIRCUIT)) && (leveltime < TICRATE/2))) { return MAXPLMOVE; @@ -4134,8 +4176,8 @@ void K_SpawnAmps(player_t *player, UINT8 amps, mobj_t *impact) if (amps == 0) return; - UINT32 itemdistance = max(0, min( FRACUNIT, K_GetItemRouletteDistance(player, D_NumPlayersInRace()))); // cap this to FRACUNIT, so it doesn't wrap when turning it into fixed_t - fixed_t itemdistmult = FRACUNIT + max( 0, min(FixedMul(FixedDiv(itemdistance<kartspeed) - (9-player->kartweight)) / 10); // Debug print for scaledamps calculation // CONS_Printf("K_SpawnAmps: player=%s, amps=%d, kartspeed=%d, kartweight=%d, itemdistance=%d, itemdistmult=%0.2f, statscaledamps=%d, distscaledamps=%d\n", @@ -4239,7 +4281,7 @@ void K_CheckpointCrossAward(player_t *player) if (gametype != GT_RACE) return; - player->gradingfactor += K_GetGradingMultAdjustment(player); + player->gradingfactor += K_GetGradingFactorAdjustment(player); player->gradingpointnum++; player->exp = K_GetEXP(player); //CONS_Printf("player: %s factor: %.2f exp: %d\n", player_names[player-players], FIXED_TO_FLOAT(player->gradingfactor), player->exp); @@ -4247,6 +4289,92 @@ void K_CheckpointCrossAward(player_t *player) player->cangrabitems = 1; K_AwardPlayerRings(player, (player->bot ? 20 : 10), true); + + // Update Duel scoring. + if (K_InRaceDuel() && player->position == 1) + { + player->duelscore += 1; + + if (leveltime > (tic_t)(TICRATE*DUELOVERTIME)) + { + overtimecheckpoints++; + if (overtimecheckpoints > 1) + { + K_AddMessage(va("Margin Boost x%d!", overtimecheckpoints), true, false); + } + else + { + K_AddMessage("Margin Boost!", true, false); + g_darkness.start = leveltime; + g_darkness.end = INT32_MAX; + for (UINT8 i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + g_darkness.value[i] = FRACUNIT; + } + } + + S_StartSound(NULL, sfx_gsha6); + } + + player_t *opp = K_DuelOpponent(player); + boolean clutch = (player->duelscore - opp->duelscore == (DUELWINNINGSCORE-1)); + boolean win = (player->duelscore - opp->duelscore == DUELWINNINGSCORE); + + if (!win) + { + for (UINT8 i = 0; i < MAXSPLITSCREENPLAYERS; i++) + { + player_t *check = &players[displayplayers[i]]; + if (check == player) + { + S_StartSound(NULL, sfx_mbs45); + if (clutch) + S_StartSoundAtVolume(NULL, sfx_s3k9c, 170); + } + + else if (check == opp) + { + S_StartSound(NULL, sfx_mbs60); + if (clutch) + S_StartSoundAtVolume(NULL, sfx_kc4b, 150); + } + + } + } + + if (player->duelscore - opp->duelscore == DUELWINNINGSCORE) + { + opp->position = 2; + player->position = 1; + + if (opp->distancetofinish - player->distancetofinish < 200) // Setting player.exiting changes distance reporting, check these first! + { + K_StartRoundWinCamera( + player->mo, + player->angleturn + ANGLE_180, + 400*mapobjectscale, + 6*TICRATE, + FRACUNIT/16 + ); + } + + S_StartSound(NULL, sfx_s3k6a); + P_DoPlayerExit(player, 0); + P_DoAllPlayersExit(PF_NOCONTEST, 0); + } + else + { + // Doing this here because duel exit is a weird path, and we don't want to transform for endcam. + UINT32 skinflags = (demo.playback) + ? demo.skinlist[demo.currentskinid[(player-players)]].flags + : skins[player->skin].flags; + if (skinflags & SF_IRONMAN) + { + SetRandomFakePlayerSkin(player, true, false); + } + } + } + } boolean K_Overdrive(player_t *player) @@ -4948,7 +5076,7 @@ static boolean K_IsLosingWavedash(player_t *player) if (!K_Sliptiding(player) && player->wavedash < MIN_WAVEDASH_CHARGE) return true; if (!K_Sliptiding(player) && player->drift == 0 - && P_IsObjectOnGround(player->mo) && player->sneakertimer == 0 && player->panelsneakertimer == 0 + && P_IsObjectOnGround(player->mo) && player->sneakertimer == 0 && player->panelsneakertimer == 0 && player->weaksneakertimer == 0 && player->driftboost == 0) return true; return false; @@ -7087,8 +7215,14 @@ void K_DoSneaker(player_t *player, INT32 type) fixed_t intendedboost = FRACUNIT/2; - // If you've already got an item sneaker type boost, panel sneakers will instead turn into item sneaker boosts - if (player->numsneakers && type == 0) + // If you've already got an rocket sneaker type boost, panel sneakers will instead turn into rocket sneaker boosts + if (player->numweaksneakers && type == 0) + { + type = 2; + } + + // If you've already got an item sneaker type boost, other sneakers will instead turn into item sneaker boosts + if (player->numsneakers && type != 1) { type = 1; } @@ -7099,10 +7233,13 @@ void K_DoSneaker(player_t *player, INT32 type) intendedboost = FRACUNIT/2; break; case 1: // Single item sneaker - case 2: // Rocket sneaker + intendedboost = 100*FRACUNIT/100; + break; + case 2: // Rocket sneaker (aka. weaksneaker) intendedboost = 85*FRACUNIT/100; break; } + //NOTE: The various sneaker booth strengths are also defined in K_GetKartBoostPower()! if (player->roundconditions.touched_sneakerpanel == false && !(player->exiting || (player->pflags & PF_NOCONTEST)) @@ -7118,7 +7255,7 @@ void K_DoSneaker(player_t *player, INT32 type) const sfxenum_t smallsfx = sfx_cdfm40; sfxenum_t sfx = normalsfx; - if (player->numsneakers || player->numpanelsneakers) + if (player->numsneakers || player->numpanelsneakers || player->numweaksneakers) { // Use a less annoying sound when stacking sneakers. sfx = smallsfx; @@ -7134,17 +7271,19 @@ void K_DoSneaker(player_t *player, INT32 type) switch (type) { - case 0: + case 0: // Panel sneaker player->numpanelsneakers++; break; - case 1: - case 2: + case 1: // Single item sneaker player->numsneakers++; break; + case 2: // Rocket sneaker (aka. weaksneaker) + player->numweaksneakers++; + break; } } - if (player->sneakertimer == 0 && player->panelsneakertimer == 0) + if (player->sneakertimer == 0 && player->panelsneakertimer == 0 && player->weaksneakertimer == 0) { if (type == 2) { @@ -7176,17 +7315,19 @@ void K_DoSneaker(player_t *player, INT32 type) switch (type) { - case 0: + case 0: // Panel sneaker player->panelsneakertimer = sneakertime; - player->overshield += 1; + if (player->overshield > 0) { + player->overshield = min( player->overshield + TICRATE/3, max( TICRATE, player->overshield )); + } break; - case 1: + case 1: // Single item sneaker player->sneakertimer = sneakertime; - player->overshield += TICRATE/2; + player->overshield = max( player->overshield, 25 ); break; - case 2: - player->sneakertimer = 3*sneakertime/4; - player->overshield += TICRATE/2; + case 2: // Rocket sneaker (aka. weaksneaker) + player->weaksneakertimer = 3*sneakertime/4; + player->overshield = max( player->overshield, TICRATE/2 ); break; } @@ -7321,7 +7462,7 @@ void K_DoPogoSpring(mobj_t *mo, fixed_t vertispeed, UINT8 sound) } } - if (mo->player->sneakertimer || mo->player->panelsneakertimer || mo->player->invincibilitytimer) + if (mo->player->sneakertimer || mo->player->panelsneakertimer || mo->player->weaksneakertimer || mo->player->invincibilitytimer) { thrust = FixedMul(thrust, (3*FRACUNIT)/2); } @@ -8925,6 +9066,7 @@ static void K_UpdateTripwire(player_t *player) boolean goodSpeed = (player->speed >= speedThreshold); boolean boostExists = (player->tripwireLeniency > 0); // can't be checked later because of subtractions... tripwirepass_t triplevel = K_TripwirePassConditions(player); + boolean mightplaysound = false; if (triplevel != TRIPWIRE_NONE) { @@ -8933,12 +9075,7 @@ static void K_UpdateTripwire(player_t *player) mobj_t *front = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_TRIPWIREBOOST); mobj_t *back = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_TRIPWIREBOOST); - if (P_IsDisplayPlayer(player)) - { - S_StartSound(player->mo, sfx_s3k40); - S_StopSoundByID(player->mo, sfx_gshaf); - } - + mightplaysound = true; P_SetTarget(&front->target, player->mo); P_SetTarget(&back->target, player->mo); @@ -8957,6 +9094,12 @@ static void K_UpdateTripwire(player_t *player) if (triplevel != TRIPWIRE_CONSUME) player->tripwireLeniency = max(player->tripwireLeniency, TRIPWIRETIME); + + if (P_IsDisplayPlayer(player) && player->tripwireLeniency && mightplaysound) + { + S_StartSound(player->mo, TRIPWIRE_OK_SOUND); + S_StopSoundByID(player->mo, TRIPWIRE_NG_SOUND); + } } // TRIPWIRE_CONSUME is only applied in very specific cases (currently, riding Garden Top) @@ -9081,7 +9224,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->speed > 0) { // Speed lines - if (player->sneakertimer || player->panelsneakertimer || player->ringboost + if (player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || player->ringboost || player->driftboost || player->startboost || player->eggmanexplode || player->trickboost || player->gateBoost || player->wavedashboost) @@ -9322,7 +9465,8 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (((P_IsObjectOnGround(player->mo) || ( player->spinouttype & KSPIN_AIRTIMER )) && (!player->sneakertimer) - && (!player->panelsneakertimer)) + && (!player->panelsneakertimer) + && (!player->weaksneakertimer)) || (player->respawn.state != RESPAWNST_NONE && player->spinouttimer > 1 && (leveltime & 1))) @@ -9518,6 +9662,16 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) } } + if (player->weaksneakertimer) + { + player->weaksneakertimer--; + + if (player->weaksneakertimer <= 0) + { + player->numweaksneakers = 0; + } + } + if (player->trickboost) player->trickboost--; @@ -9537,7 +9691,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->counterdash) player->counterdash--; - if ((player->sneakertimer || player->panelsneakertimer) && player->wipeoutslow > 0 && player->wipeoutslow < wipeoutslowtime+1) + if ((player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer) && player->wipeoutslow > 0 && player->wipeoutslow < wipeoutslowtime+1) player->wipeoutslow = wipeoutslowtime+1; if (player->floorboost > 0) @@ -9662,6 +9816,26 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) player->invincibilitytimer--; if (player->invincibilitytimer && K_IsPlayerScamming(player)) player->invincibilitytimer--; + + // Extra tripwire leniency for the end of invincibility + if (player->invincibilitytimer <= 0) { + player->tripwireLeniency = max( player->tripwireLeniency, TICRATE ); + } + } + + // The precise ordering of start-of-level made me want to cut my head off, + // so let's try this instead. Whatever! + if (leveltime <= starttime || player->gradingpointnum == 0) + { + if ((gametyperules & GTR_SPHERES) + || (gametyperules & GTR_CATCHER) + || G_TimeAttackStart() + || (gametype == GT_TUTORIAL) + || !M_NotFreePlay() + || (K_GetNumWaypoints() == 0)) + player->cangrabitems = EARLY_ITEM_FLICKER; + else + player->cangrabitems = 0; } if (player->cangrabitems && player->cangrabitems <= EARLY_ITEM_FLICKER) @@ -9826,6 +10000,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) player->tiregrease = 0; player->sneakertimer = 0; player->panelsneakertimer = 0; + player->weaksneakertimer = 0; player->spindashboost = 0; player->flashing = TICRATE/2; player->ringboost = 0; @@ -10993,7 +11168,7 @@ static void K_UpdateDistanceFromFinishLine(player_t *const player) const mapheader_t *mapheader = mapheaderinfo[gamemap - 1]; if ((mapheader->levelflags & LF_SECTIONRACE) == 0U) { - const UINT8 numfulllapsleft = ((UINT8)numlaps - player->laps) / mapheader->lapspersection; + UINT8 numfulllapsleft = ((UINT8)numlaps - player->laps) / mapheader->lapspersection; player->distancetofinish += numfulllapsleft * K_GetCircuitLength(); } } @@ -11041,6 +11216,7 @@ static void K_UpdatePlayerWaypoints(player_t *const player) player->respawn.state == RESPAWNST_NONE && // Respawning should be a full reset. old_currentwaypoint != NULL && // So should touching the first waypoint ever. player->laps != 0 && // POSITION rooms may have unorthodox waypoints to guide bots. + player->exiting == 0 && // What the fuck? Why do duels antiskip the bot? !(player->pflags & PF_TRUSTWAYPOINTS)) // Special exception. { extern consvar_t cv_debuglapcheat; @@ -11337,6 +11513,14 @@ INT16 K_GetKartTurnValue(const player_t *player, INT16 turnvalue) turnfixed = FixedMul(turnfixed, 2*FRACUNIT); // Base increase to turning } + /* + if (overtimecheckpoints) + { + fixed_t assistpercent = FRACUNIT * overtimecheckpoints / 32; + turnfixed += FixedMul(Easing_Linear(assistpercent, 0, FRACUNIT/2), turnfixed); + } + */ + if (player->drift != 0 && P_IsObjectOnGround(player->mo)) { if (G_CompatLevel(0x000A)) @@ -11393,7 +11577,7 @@ INT16 K_GetKartTurnValue(const player_t *player, INT16 turnvalue) // If you're sliptiding, don't interact with handling boosts. // You need turning power proportional to your speed, no matter what! fixed_t topspeed = K_GetKartSpeed(player, false, false); - if (K_Sliptiding(player)) + if (K_Sliptiding(player) || player->flamedash) { fixed_t sliptide_handle; @@ -12040,6 +12224,11 @@ void K_KartUpdatePosition(player_t *player) realplayers++; } } + else if (K_InRaceDuel() && player->exiting) + { + // Positions directly set in K_CheckpointCrossAward, don't touch. + return; + } else { for (i = 0; i < MAXPLAYERS; i++) @@ -12146,7 +12335,9 @@ void K_KartUpdatePosition(player_t *player) /* except in FREE PLAY */ if (player->curshield == KSHIELD_TOP && (gametyperules & GTR_CIRCUIT) && - realplayers > 1) + realplayers > 1 && + !specialstageinfo.valid + && !K_Cooperative()) { /* grace period so you don't fall off INSTANTLY */ if (K_GetItemRouletteDistance(player, 8) < 2000 && player->topinfirst < 2*TICRATE) // "Why 8?" Literally no reason, but since we intend for constant-ish distance we choose a fake fixed playercount. @@ -13708,7 +13899,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) { UINT32 behind = K_GetItemRouletteDistance(player, player->itemRoulette.playing); UINT32 behindScaled = behind * TICRATE / 4500; - behindScaled = min(behindScaled, 10*TICRATE); + behindScaled = min(behindScaled, 15*TICRATE); K_DoInvincibility(player, max(7u * TICRATE + behindScaled, player->invincibilitytimer + 5u*TICRATE)); @@ -14261,7 +14452,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground) { P_Thrust( player->mo, player->mo->angle, - FixedMul((50*player->mo->scale), K_GetKartGameSpeedScalar(gamespeed)) + FixedMul((80*player->mo->scale), K_GetKartGameSpeedScalar(gamespeed)) ); player->wavedashboost += TICRATE; @@ -15277,8 +15468,17 @@ void K_EggmanTransfer(player_t *source, player_t *victim) if (victim->eggmanexplode) return; + boolean prank = false; + + if (victim->itemRoulette.eggman) + { + K_StopRoulette(&source->itemRoulette); + prank = true; // Give the transferring player the victim's eggbox roulette?! + } + K_AddHitLag(victim->mo, 5, false); K_DropItems(victim); + victim->eggmanexplode = 6*TICRATE; victim->eggmanblame = (source - players); K_StopRoulette(&victim->itemRoulette); @@ -15287,9 +15487,20 @@ void K_EggmanTransfer(player_t *source, player_t *victim) S_StartSound(NULL, sfx_itrole); K_AddHitLag(source->mo, 5, false); - source->eggmanexplode = 0; - source->eggmanblame = -1; - K_StopRoulette(&source->itemRoulette); + + if (prank) + { + source->eggmanexplode = 0; + source->eggmanblame = (victim - players); + K_StartEggmanRoulette(source); + S_StartSound(source->mo, sfx_s223); + } + else + { + source->eggmanexplode = 0; + source->eggmanblame = -1; + K_StopRoulette(&source->itemRoulette); + } source->eggmanTransferDelay = 25; victim->eggmanTransferDelay = 15; @@ -15517,7 +15728,7 @@ boolean K_PlayerCanUseItem(player_t *player) return (player->mo->health > 0 && !player->spectator && !P_PlayerInPain(player) && !mapreset && leveltime > introtime); } -fixed_t K_GetGradingMultAdjustment(player_t *player) +fixed_t K_GetGradingFactorAdjustment(player_t *player) { fixed_t power = 3*FRACUNIT/100; // adjust to change overall xp volatility fixed_t stablerate = 3*FRACUNIT/10; // how low is your placement before losing XP? 4*FRACUNIT/10 = top 40% of race will gain @@ -15571,13 +15782,57 @@ fixed_t K_GetGradingMultAdjustment(player_t *player) return result; } +fixed_t K_GetGradingFactorMinMax(UINT32 gradingpointnum, boolean max) +{ + // Create a dummy player structure for the theoretical last-place player + player_t dummy_player; + memset(&dummy_player, 0, sizeof(player_t)); + dummy_player.gradingfactor = FRACUNIT; // Start at 1.0 + + if (G_GametypeHasTeams()) + { + const UINT8 orange_count = G_CountTeam(TEAM_ORANGE); + const UINT8 blue_count = G_CountTeam(TEAM_BLUE); + if (orange_count <= blue_count) + { + dummy_player.team = TEAM_ORANGE; + } + else + { + dummy_player.team = TEAM_BLUE; + } + dummy_player.position = max ? 0 : D_NumPlayersInRace() + 1; // Ensures that all enemy players are counted, and our dummy won't overlap + } + else + { + dummy_player.position = max ? 1 : D_NumPlayersInRace(); + } + + // Apply the adjustment for each grading point + for (UINT32 i = 0; i < gradingpointnum; i++) + { + dummy_player.gradingfactor += K_GetGradingFactorAdjustment(&dummy_player); + } + return dummy_player.gradingfactor; +} + UINT16 K_GetEXP(player_t *player) { UINT32 numgradingpoints = K_GetNumGradingPoints(); - // target is where you should be if you're doing good and at a 1.0 mult - fixed_t clampedmult = max(FRACUNIT/2, min(FRACUNIT*5/4, player->gradingfactor)); // clamp between 0.5 and 1.25 - fixed_t targetexp = (TARGETEXP*player->gradingpointnum/max(1,numgradingpoints))<>FRACBITS; + UINT16 targetminexp = (MINEXP*player->gradingpointnum/max(1,numgradingpoints)); // about what a last place player should be at this stage of the race + UINT16 targetexp = (MAXEXP*player->gradingpointnum/max(1,numgradingpoints)); // about what a 1.0 factor should be at this stage of the race + fixed_t factormin = K_GetGradingFactorMinMax(player->gradingpointnum, false); + fixed_t factormax = K_GetGradingFactorMinMax(player->gradingpointnum, true); + fixed_t clampedfactor = max(factormin, min(factormax, player->gradingfactor)); + fixed_t range = factormax - factormin; + fixed_t normalizedfactor = FixedDiv(clampedfactor - factormin, range); + fixed_t easedexp = Easing_Linear(normalizedfactor, targetminexp, targetexp); + // fixed_t easedexp = Easing_Linear(normalizedfactor, MINEXP*FRACUNIT, MAXEXP*FRACUNIT); + UINT16 exp = easedexp; + // CONS_Printf("Player %s numgradingpoints=%d targetminexp=%d targetexp=%d factormin=%.2f factormax=%.2f clampedfactor=%.2f normalizedfactor=%.2f easedexp=%d\n", + // player_names[player - players], numgradingpoints, targetminexp, targetexp, FIXED_TO_FLOAT(factormin), FIXED_TO_FLOAT(factormax), + // FIXED_TO_FLOAT(clampedfactor), FIXED_TO_FLOAT(normalizedfactor), easedexp); + // UINT16 exp = (player->gradingfactor*100)>>FRACBITS; return exp; } @@ -15598,6 +15853,29 @@ void K_BotHitPenalty(player_t *player) } } +boolean K_IsPickMeUpItem(mobjtype_t type) +{ + switch (type) + { + case MT_JAWZ: + case MT_JAWZ_SHIELD: + case MT_ORBINAUT: + case MT_ORBINAUT_SHIELD: + case MT_DROPTARGET: + case MT_DROPTARGET_SHIELD: + case MT_LANDMINE: + case MT_BANANA: + case MT_BANANA_SHIELD: + case MT_GACHABOM: + case MT_EGGMANITEM: + case MT_EGGMANITEM_SHIELD: + case MT_BUBBLESHIELDTRAP: + return true; + default: + return false; + } +} + static boolean K_PickUp(player_t *player, mobj_t *picked) { SINT8 type = -1; @@ -15688,7 +15966,7 @@ static boolean K_PickUp(player_t *player, mobj_t *picked) } // ACHTUNG this destroys items when returning true, make sure to bail out -boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2) +boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2, boolean allowHostile) { if (!m1 || P_MobjWasRemoved(m1)) return false; @@ -15723,7 +16001,7 @@ boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2) if (inflictor->target->player && G_SameTeam(inflictor->target->player, victim->player)) allied = true; - if (!allied) + if (!allied && !allowHostile) return false; // CONS_Printf("target check passed\n"); diff --git a/src/k_kart.h b/src/k_kart.h index cd5dee152..738513358 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -64,6 +64,9 @@ Make sure this matches the actual number of states #define EARLY_ITEM_FLICKER (NUMTRANSMAPS) +#define TRIPWIRE_OK_SOUND (sfx_s3k40) +#define TRIPWIRE_NG_SOUND (sfx_gshaf) + // 2023-08-26 +ang20 to Sal's OG values to make them friendlier - Tyron #define STUMBLE_STEEP_VAL (ANG60 + ANG20) #define STUMBLE_STEEP_VAL_AIR (ANG30 + ANG10 + ANG20) @@ -80,6 +83,9 @@ Make sure this matches the actual number of states #define RINGVOLUMEREGEN 1 #define RINGTRANSPARENCYREGEN 3 +#define DUELOVERTIME (cv_dueltimelimit.value) +#define DUELWINNINGSCORE (cv_duelscorelimit.value) + #define MIN_WAVEDASH_CHARGE ((11*TICRATE/16)*9) #define MAXTOPACCEL (12*FRACUNIT) @@ -105,6 +111,8 @@ angle_t K_ReflectAngle(angle_t angle, angle_t against, fixed_t maxspeed, fixed_t boolean K_IsDuelItem(mobjtype_t type); boolean K_DuelItemAlwaysSpawns(mapthing_t *mt); +boolean K_InRaceDuel(void); +player_t *K_DuelOpponent(player_t *player); void K_TimerReset(void); void K_TimerInit(void); @@ -310,8 +318,8 @@ boolean K_ThunderDome(void); boolean K_PlayerCanUseItem(player_t *player); -fixed_t K_GetGradingMultAdjustment(player_t *player); - +fixed_t K_GetGradingFactorAdjustment(player_t *player); +fixed_t K_GetGradingFactorMinMax(UINT32 gradingpointnum, boolean max); UINT16 K_GetEXP(player_t *player); UINT32 K_GetNumGradingPoints(void); @@ -320,7 +328,9 @@ boolean K_LegacyRingboost(player_t *player); void K_BotHitPenalty(player_t *player); -boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2); +boolean K_IsPickMeUpItem(mobjtype_t type); + +boolean K_TryPickMeUp(mobj_t *m1, mobj_t *m2, boolean allowHostile); fixed_t K_TeamComebackMultiplier(player_t *player); diff --git a/src/k_menu.h b/src/k_menu.h index c9564c484..28db4810e 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -593,10 +593,10 @@ extern menu_t PAUSE_PlaybackMenuDef; typedef enum { playback_hide, + playback_restart, playback_rewind, playback_pause, playback_fastforward, - playback_backframe, playback_resume, playback_advanceframe, playback_viewcount, @@ -923,6 +923,7 @@ typedef struct levellist_s { UINT8 guessgt; levelsearch_t levelsearch; boolean netgame; // Start the game in an actual server + boolean canqueue; menu_t *backMenu; } levellist_t; @@ -944,6 +945,8 @@ void M_CupSelectTick(void); void M_LevelSelectHandler(INT32 choice); void M_LevelSelectTick(void); +INT16 M_LevelFromScrolledList(INT16 add); +void M_MenuToLevelPreamble(UINT8 ssplayers, boolean nowipe); void M_LevelSelected(INT16 add, boolean menuupdate); boolean M_LevelSelectCupSwitch(boolean next, boolean skipones); diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 61853ece5..0c02b15ba 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -755,33 +755,40 @@ static void M_DrawMenuTyping(void) } -// Largely replaced by boxed drawing mode in K_DrawGameControl and rich text -/* -static void M_DrawMediocreKeyboardKey(const char *text, INT32 *workx, INT32 worky, boolean push, boolean rightaligned) +static void M_DrawPauseRoundQueue(INT16 offset, boolean canqueue) { - INT32 buttonwidth = V_StringWidth(text, 0) + 2; + y_data_t standings; + memset(&standings, 0, sizeof (standings)); - if (rightaligned) + if (gamestate == GS_MENU) { - (*workx) -= buttonwidth; - } - - if (push) - { - worky += 2; + standings.mainplayer = MAXPLAYERS; } else { - V_DrawFill((*workx)-1, worky+10, buttonwidth, 2, 24); + standings.mainplayer = (demo.playback ? displayplayers[0] : consoleplayer); } - V_DrawFill((*workx)-1, worky, buttonwidth, 10, 16); - V_DrawString( - (*workx), worky + 1, - 0, text - ); + // See also G_GetNextMap, Y_CalculateMatchData + if ( + canqueue == false + && grandprixinfo.gp == true + && netgame == false // TODO netgame Special Mode support + && grandprixinfo.gamespeed >= KARTSPEED_NORMAL + && roundqueue.size > 1 + && roundqueue.entries[roundqueue.size - 1].rankrestricted == true + && ( + gamedata->everseenspecial == true + || roundqueue.position == roundqueue.size + ) + ) + { + // Additional cases in which it should always be shown. + standings.showrank = true; + } + + Y_RoundQueueDrawer(&standings, offset, false, false, canqueue); } -*/ // Draw the message popup submenu void M_DrawMenuMessage(void) @@ -991,6 +998,16 @@ void M_Drawer(void) // Draw message overlay when needed M_DrawMenuMessage(); + + if ( + ( + currentMenu == &PLAY_LevelSelectDef + || currentMenu == &PLAY_CupSelectDef + ) && levellist.canqueue + ) + { + M_DrawPauseRoundQueue(0, true); + } } if (menuwipe) @@ -6288,30 +6305,11 @@ void M_DrawPause(void) V_DrawCenteredLSTitleLowString(220 + offset*2, 103, mainflags, word2); } + const boolean rulescheck = (K_CanChangeRules(false) && (server || IsPlayerAdmin(consoleplayer))); + boolean drawqueue = (rulescheck && (menuqueue.size > 0)); + if (gamestate != GS_INTERMISSION && roundqueue.size > 0) { - y_data_t standings; - memset(&standings, 0, sizeof (standings)); - - standings.mainplayer = (demo.playback ? displayplayers[0] : consoleplayer); - - // See also G_GetNextMap, Y_CalculateMatchData - if ( - grandprixinfo.gp == true - && netgame == false // TODO netgame Special Mode support - && grandprixinfo.gamespeed >= KARTSPEED_NORMAL - && roundqueue.size > 1 - && roundqueue.entries[roundqueue.size - 1].rankrestricted == true - && ( - gamedata->everseenspecial == true - || roundqueue.position == roundqueue.size - ) - ) - { - // Additional cases in which it should always be shown. - standings.showrank = true; - } - if (roundqueue.position > 0 && roundqueue.position <= roundqueue.size) { patch_t *smallroundpatch = ST_getRoundPicture(true); @@ -6328,7 +6326,7 @@ void M_DrawPause(void) V_DrawCenteredMenuString(24, 167 + offset/2, V_YELLOWMAP, M_GetGameplayMode()); - Y_RoundQueueDrawer(&standings, offset/2, false, false); + drawqueue = true; } else if (gametype == GT_TUTORIAL) { @@ -6347,6 +6345,11 @@ void M_DrawPause(void) { V_DrawMenuString(4, 188 + offset/2, V_YELLOWMAP, M_GetGameplayMode()); } + + if (drawqueue) + { + M_DrawPauseRoundQueue(offset/2, rulescheck); + } } void M_DrawKickHandler(void) @@ -6485,7 +6488,7 @@ void M_DrawPlaybackMenu(void) else if (currentMenu->menuitems[i].patch && W_CheckNumForName(currentMenu->menuitems[i].patch) != LUMPERROR) icon = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE); - if ((i == playback_fastforward && cv_playbackspeed.value > 1) || (i == playback_rewind && demo.rewinding)) + if ((i == playback_fastforward && cv_playbackspeed.value > 1)) V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].mvar1, currentMenu->y, V_SNAPTOTOP, icon, R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_JAWZ, GTC_MENUCACHE)); else V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].mvar1, currentMenu->y, V_SNAPTOTOP, icon, (i == itemOn) ? activemap : inactivemap); diff --git a/src/k_menufunc.c b/src/k_menufunc.c index 0e3b1385d..3631441b0 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -466,7 +466,7 @@ boolean M_Responder(event_t *ev) if (Playing() && !demo.playback) { // Quick Retry (Y in modeattacking) - if (modeattacking && G_PlayerInputDown(0, gc_y, splitscreen + 1) == true) + if (modeattacking && G_PlayerInputDown(0, gc_respawn, splitscreen + 1) == true) { M_TryAgain(0); return true; diff --git a/src/k_objects.h b/src/k_objects.h index cbdb189a7..c6cc64db9 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -411,7 +411,6 @@ void Obj_SSGobletMobjThink(mobj_t* mo); void Obj_SSLampMapThingSpawn(mobj_t* mo, mapthing_t* mt); void Obj_SSWindowMapThingSpawn(mobj_t* mo, mapthing_t* mt); void Obj_SLSTMaceMobjThink(mobj_t* mo); -void Obj_SSBumperTouchSpecial(mobj_t* special, mobj_t* toucher); void Obj_SSBumperMobjSpawn(mobj_t* mo); void Obj_SSChainMobjThink(mobj_t* mo); void Obj_SSGachaTargetMobjSpawn(mobj_t* mo); diff --git a/src/k_pwrlv.c b/src/k_pwrlv.c index 6291d3a21..62c257952 100644 --- a/src/k_pwrlv.c +++ b/src/k_pwrlv.c @@ -19,6 +19,7 @@ #include "k_grandprix.h" #include "k_profiles.h" #include "k_serverstats.h" +#include "k_kart.h" // K_InRaceDuel // Client-sided calculations done for Power Levels. // This is done so that clients will never be able to hack someone else's score over the server. @@ -213,6 +214,10 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) CONS_Debug(DBG_PWRLV, "========\n"); yourPower = clientpowerlevels[playerNum][powerType]; + + if (K_InRaceDuel()) + yourPower += clientPowerAdd[playerNum]; + if (yourPower == 0) { // Guests don't record power level changes. @@ -225,6 +230,8 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) CONS_Debug(DBG_PWRLV, "%s's gametype score: %d\n", player_names[playerNum], yourScore); CONS_Debug(DBG_PWRLV, "========\n"); + + boolean dueling = K_InRaceDuel(); for (i = 0; i < MAXPLAYERS; i++) { UINT16 theirScore = 0; @@ -254,6 +261,9 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) CONS_Debug(DBG_PWRLV, "%s VS %s:\n", player_names[playerNum], player_names[i]); theirPower = clientpowerlevels[i][powerType]; + if (K_InRaceDuel()) + theirPower += clientPowerAdd[i]; + if (theirPower == 0) { // No power level (splitscreen guests, bots) @@ -295,11 +305,13 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) } } - if (exitBonus == false) + if (dueling) { INT16 prevInc = inc; - inc /= max(numlaps-1, 1); + // INT32 winnerscore = (yourScore > theirScore) ? player->duelscore : players[i].duelscore; + INT32 multiplier = 2; + inc *= multiplier; if (inc == 0) { @@ -313,7 +325,32 @@ void K_UpdatePowerLevels(player_t *player, UINT8 lap, boolean forfeit) } } - CONS_Debug(DBG_PWRLV, "Reduced (%d / %d = %d) because it's not the end of the race\n", prevInc, numlaps, inc); + // CONS_Printf("%s PWR UPDATE: %d\n", player_names[player - players], inc); + + CONS_Debug(DBG_PWRLV, "DUELING: Boosted (%d * %d = %d)\n", prevInc, multiplier, inc); + } + else + { + if (exitBonus == false) + { + INT16 prevInc = inc; + + inc /= max(numlaps-1, 1); + + if (inc == 0) + { + if (prevInc > 0) + { + inc = 1; + } + else if (prevInc < 0) + { + inc = -1; + } + } + + CONS_Debug(DBG_PWRLV, "Reduced (%d / %d = %d) because it's not the end of the race\n", prevInc, numlaps, inc); + } } CONS_Debug(DBG_PWRLV, "========\n"); @@ -346,6 +383,10 @@ void K_UpdatePowerLevelsFinalize(player_t *player, boolean onForfeit) INT16 lapsLeft = 0; UINT8 i; + // No remaining laps in Duel. + if (K_InRaceDuel()) + return; + lapsLeft = (numlaps - player->latestlap) + 1; if (lapsLeft <= 0) @@ -394,7 +435,7 @@ INT16 K_FinalPowerIncrement(player_t *player, INT16 yourPower, INT16 baseInc) if (inc <= 0) { - if (player->position == 1 && numPlayers > 1) + if (player->position == 1 && numPlayers > 1 && !(K_InRaceDuel())) { // Won the whole match? // Get at least one point. diff --git a/src/k_tally.cpp b/src/k_tally.cpp index 877e39dd1..11c07f8ff 100644 --- a/src/k_tally.cpp +++ b/src/k_tally.cpp @@ -46,6 +46,9 @@ boolean level_tally_t::UseBonuses(void) return false; } + if (K_InRaceDuel()) + return false; + // No bonuses / ranking in FREE PLAY or Time Attack return (grandprixinfo.gp == true || K_TimeAttackRules() == false); } @@ -218,7 +221,7 @@ INT32 level_tally_t::CalculateGrade(void) } } - const INT32 positionWeight = (position > 0 && numPlayers > 2) ? 50 : 0; + const INT32 positionWeight = (position > 0 && numPlayers > 2) ? 20 : 0; const INT32 total = positionWeight + bonusWeights[0] + bonusWeights[1]; INT32 ours = 0; @@ -242,10 +245,7 @@ INT32 level_tally_t::CalculateGrade(void) } case TALLY_BONUS_EXP: { - // Use a special curve for this. - // Low Exp amounts are guaranteed, higher than half is where skill expression starts - // Magic numbers here are to reduce the range from 50-125 to 0-75 and compare with a max of 58, 85% of which is 49.3, which should put an even 100 or higher exp at A rank - const fixed_t frac = std::min(FRACUNIT, ((exp-50) * FRACUNIT) / std::max(1, static_cast(totalExp-42))); + const fixed_t frac = std::min(FRACUNIT, ((exp-15) * FRACUNIT) / std::max(1, static_cast(totalExp))); ours += Easing_Linear(frac, 0, bonusWeights[i]); break; } diff --git a/src/k_vote.c b/src/k_vote.c index 48c83db16..e5eeb5c63 100644 --- a/src/k_vote.c +++ b/src/k_vote.c @@ -109,6 +109,7 @@ // Give time for the animations to finish before finalizing the vote stages. #define SELECT_DELAY_TIME (TICRATE*4) #define PICK_DELAY_TIME (TICRATE/2) +#define STRIKE_DELAY_TIME (TICRATE/3) #define MAP_ANGER_MAX (2) @@ -179,12 +180,21 @@ typedef struct { INT32 timer; INT32 tic, endtic; - INT32 selectFinalize, pickFinalize; + INT32 selectFinalize, pickFinalize, strikeFinalize; boolean notYetPicked; boolean loaded; SINT8 deferredLevel; y_vote_player players[MAXSPLITSCREENPLAYERS]; y_vote_roulette roulette; + + // If both of these players are valid, + // and they're the only players in the server, + // then we want stage striking! + player_t *strike_loser; + player_t *strike_winner; + boolean strike_turn; + boolean strike_time_out; + boolean stage_striking; } y_vote_data; // Voting level drawing @@ -209,6 +219,8 @@ typedef struct patch_t *ruby_icon; fixed_t ruby_height; + patch_t *strike_icon; + patch_t *bg_planet[PLANET_FRAMES]; patch_t *bg_checker; patch_t *bg_levelText; @@ -230,13 +242,54 @@ typedef struct static y_vote_data vote = {0}; static y_vote_draw vote_draw = {0}; +static void Y_SetVoteTimer(void) +{ + vote.timer = cv_votetime.value * TICRATE; + + if (vote.stage_striking == true) + { + vote.timer /= 2; + } +} + +static UINT8 Y_CountStriked(void) +{ + INT32 i; + + UINT8 num_striked = 0; + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + if (g_votes_striked[i] == true) + { + num_striked++; + } + } + + return num_striked; +} + +static boolean Y_VoteIDIsSpecial(const UINT8 playerId) +{ + switch (playerId) + { + case VOTE_SPECIAL: + case VOTE_TIMEOUT_LOSER: + case VOTE_TIMEOUT_WINNER: + { + // Special vote spot, always allow + return true; + } + default: + { + return false; + } + } +} + boolean Y_PlayerIDCanVote(const UINT8 playerId) { - player_t *player = NULL; - - if (playerId == VOTE_SPECIAL) + if (Y_VoteIDIsSpecial(playerId) == true) { - // Special vote spot, always allow return true; } @@ -245,7 +298,7 @@ boolean Y_PlayerIDCanVote(const UINT8 playerId) return false; } - player = &players[playerId]; + const player_t *player = &players[playerId]; if (player->spectator == true) { return false; @@ -260,8 +313,48 @@ boolean Y_PlayerIDCanVote(const UINT8 playerId) return true; } +static boolean Y_IsPlayersTurn(const UINT8 playerId) +{ + if (Y_VoteIDIsSpecial(playerId) == true) + { + return true; + } + + if (vote.stage_striking == false) + { + // Not stage striking -- we can always vote. + return true; + } + + // Is it our turn to strike a stage? + const player_t *player = &players[playerId]; + if (vote.strike_turn == true) + { + return (player == vote.strike_winner); + } + else + { + return (player == vote.strike_loser); + } +} + +static boolean Y_PlayerIDCanVoteRightNow(const UINT8 playerId) +{ + if (Y_IsPlayersTurn(playerId) == false) + { + return false; + } + + return Y_PlayerIDCanVote(playerId); +} + static boolean Y_PlayerCanSelect(const UINT8 localId) { + if (localId > splitscreen) + { + return false; + } + const UINT8 p = g_localplayers[localId]; if (g_pickedVote != VOTE_NOT_PICKED) @@ -280,7 +373,7 @@ static boolean Y_PlayerCanSelect(const UINT8 localId) return false; } - return Y_PlayerIDCanVote(p); + return Y_PlayerIDCanVoteRightNow(p); } static void Y_SortPile(void) @@ -381,21 +474,73 @@ static void Y_SortPile(void) } } -void Y_SetPlayersVote(const UINT8 playerId, SINT8 newVote) +void Y_SetPlayersVote(const UINT8 inputPlayerId, SINT8 newVote) { - y_vote_pile *const pile = &vote.roulette.pile[playerId]; - y_vote_catcher *const catcher = &pile->catcher; + INT32 i; if (gamestate != GS_VOTING) { return; } + UINT8 playerId = inputPlayerId; + + // Manually overwrite these players for timed out votes. + // Loser/winner is encoded in the vote ID to prevent race + // race conditions with real votes causing problems. + if (inputPlayerId == VOTE_TIMEOUT_LOSER) + { + playerId = (vote.strike_loser - players); + } + else if (inputPlayerId == VOTE_TIMEOUT_WINNER) + { + playerId = (vote.strike_winner - players); + } + if (newVote < 0 || newVote >= VOTE_NUM_LEVELS) { newVote = VOTE_NOT_PICKED; } + if (playerId < MAXPLAYERS) + { + if (Y_PlayerIDCanVoteRightNow(playerId) == false) + { + // Not your turn, dude! + return; + } + + if (vote.stage_striking == true) + { + if (newVote != VOTE_NOT_PICKED + && g_votes_striked[newVote] == false + && Y_CountStriked() < VOTE_NUM_LEVELS-1) + { + // Strike a stage, instead of voting. + g_votes_striked[newVote] = true; + + // Change turn. + vote.strike_turn = !vote.strike_turn; + + // Reset variables. + Y_SetVoteTimer(); + for (i = 0; i <= splitscreen; i++) + { + vote.players[i].sentTimeOutVote = false; + vote.players[i].delay = NEWTICRATE/7; + } + vote.strike_time_out = false; + + // TODO: striking animation + } + + return; + } + } + + y_vote_pile *const pile = &vote.roulette.pile[playerId]; + y_vote_catcher *const catcher = &pile->catcher; + g_votes[playerId] = newVote; Y_SortPile(); @@ -416,12 +561,12 @@ void Y_SetPlayersVote(const UINT8 playerId, SINT8 newVote) if (vote.timer == -1) { // Someone has voted, so start the timer now. - vote.timer = cv_votetime.value * TICRATE; + Y_SetVoteTimer(); } #endif } -static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t width, INT32 flags, SINT8 v, boolean dim, SINT8 playerID) +static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t width, INT32 flags, SINT8 v, boolean dim, SINT8 playerID, boolean from_selection) { const boolean encore = vote_draw.levels[v].encore; const fixed_t height = (width * BASEVIDHEIGHT) / BASEVIDWIDTH; @@ -468,29 +613,64 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt V_AdjustXYWithSnap(&fx, &fy, flags, dupx, dupy); + boolean striked = false; + if (from_selection == true) + { + striked = g_votes_striked[v]; + } + V_DrawFill( fx - dupx, fy - dupy, fw + (dupx << 1), fh + (dupy << 1), 0|flags|V_NOSCALESTART ); - K_DrawMapThumbnail( - x, y, - width, flags | ((encore == true) ? V_FLIP : 0), - g_voteLevels[v][0], - (dim == true ? R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE) : NULL) - ); - - if (encore == true) + if (striked == true) { - const fixed_t rubyScale = width / 72; + const fixed_t strikeScale = width / 32; V_DrawFixedPatch( - center_x, center_y - FixedMul(vote_draw.ruby_height << 1, rubyScale), - rubyScale, flags, - vote_draw.ruby_icon, + center_x - (strikeScale * 25 / 2), center_y - (strikeScale * 22 / 2), + strikeScale, flags, + vote_draw.strike_icon, NULL ); } + else + { + K_DrawMapThumbnail( + x, y, + width, flags | ((encore == true) ? V_FLIP : 0), + g_voteLevels[v][0], + (dim == true ? R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE) : NULL) + ); + + if (encore == true) + { + const fixed_t rubyScale = width / 72; + V_DrawFixedPatch( + center_x, center_y - FixedMul(vote_draw.ruby_height << 1, rubyScale), + rubyScale, flags, + vote_draw.ruby_icon, + NULL + ); + } + + if (vote.stage_striking == true + && from_selection == true + && dim == false) + { + if (Y_CountStriked() < VOTE_NUM_LEVELS-1) + { + const fixed_t strikeScale = width / 32; + V_DrawFixedPatch( + center_x - (strikeScale * 25 / 2), center_y - (strikeScale * 22 / 2), + strikeScale, flags /*| V_TRANSLUCENT*/, + vote_draw.strike_icon, + NULL + ); + } + } + } if (dim == true) { @@ -506,7 +686,7 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt { const INT32 whiteSq = 16 * dupx; - if (playerID < MAXPLAYERS) + if (playerID < MAXPLAYERS) // Player vote { UINT8 *playerMap = R_GetTranslationColormap(players[playerID].skin, players[playerID].skincolor, GTC_CACHE); patch_t *playerPatch = faceprefix[players[playerID].skin][FACE_RANK]; @@ -518,37 +698,15 @@ static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t widt playerPatch, playerMap ); } - else + else if (vote.stage_striking == false) // Angry map { - const fixed_t iconHeight = (14 << FRACBITS); - const fixed_t iconWidth = (iconHeight * 320) / 200; - - V_DrawFill( + K_DrawMapAsFace( fx + fw - whiteSq + dupx, fy + fh - whiteSq + dupy, - whiteSq, - whiteSq, - 0|flags|V_NOSCALESTART - ); - - V_SetClipRect( - fx + fw - whiteSq + (2 * dupx), - fy + fh - whiteSq + (2 * dupy), - whiteSq - (2 * dupx), - whiteSq - (2 * dupy), - flags|V_NOSCALESTART - ); - - K_DrawMapThumbnail( - ((fx + fw - whiteSq + (2 * dupx)) * FRACUNIT) - (iconWidth - iconHeight), - (fy + fh - whiteSq + (2 * dupy)) * FRACUNIT, - iconWidth, flags | V_NOSCALESTART | ((encore == true) ? V_FLIP : 0), g_voteLevels[v][0], NULL ); - - V_ClearClipRect(); } } } @@ -618,7 +776,7 @@ static void Y_DrawCatcher(y_vote_catcher *catcher) baseX, catcher->y, ((catcher->small == true) ? PILE_WIDTH : SELECTION_WIDTH), 0, catcher->level, false, - catcher->player + catcher->player, false ); } @@ -813,7 +971,7 @@ static void Y_DrawVoteSelection(fixed_t offset) continue; } - if (g_votes[p] != VOTE_NOT_PICKED || Y_PlayerIDCanVote(p) == false) + if (g_votes[p] != VOTE_NOT_PICKED || Y_PlayerIDCanVoteRightNow(p) == false) { continue; } @@ -881,12 +1039,41 @@ static void Y_DrawVoteSelection(fixed_t offset) x, y - vote_draw.levels[i].hop, SELECTION_WIDTH, 0, i, (selected == false), - -1 + -1, true ); x += SELECTION_SPACING_W; } + if (vote.stage_striking == true && Y_CountStriked() < VOTE_NUM_LEVELS-1) + { + UINT8 current_strike_player = ( + (vote.strike_turn == true) + ? (vote.strike_winner - players) + : (vote.strike_loser - players) + ); + + for (i = 0; i <= splitscreen; i++) + { + if (g_localplayers[i] == current_strike_player) + { + break; + } + } + + if (i > splitscreen) + { + const char *wait_str = va("Waiting for %s...", player_names[current_strike_player]); + + V_DrawThinString( + BASEVIDWIDTH / 2 - (V_ThinStringWidth(wait_str, 0) / 2), + 180, + 0, + wait_str + ); + } + } + // // Draw our catchers // @@ -944,7 +1131,7 @@ static void Y_DrawVotePile(void) PILE_WIDTH, 0, g_votes[i], (i != vote.roulette.anim || g_pickedVote == VOTE_NOT_PICKED), - i + i, false ); } @@ -1060,8 +1247,30 @@ static void Y_VoteStops(SINT8 pick, SINT8 level) } } +static void Y_PlayerSendStrike(const UINT8 localPlayer) +{ + y_vote_player *const player = &vote.players[localPlayer]; + y_vote_catcher *const catcher = &player->catcher; + + if (g_votes_striked[player->selection] == true) + { + // TODO: "Can't select" animation + return; + } + + D_ModifyClientVote(g_localplayers[localPlayer], player->selection); + catcher->action = CATCHER_NA; + catcher->delay = 5; +} + static void Y_PlayerSendVote(const UINT8 localPlayer) { + if (vote.stage_striking == true) + { + Y_PlayerSendStrike(localPlayer); + return; + } + y_vote_player *const player = &vote.players[localPlayer]; y_vote_catcher *const catcher = &player->catcher; @@ -1188,7 +1397,11 @@ static void Y_TickPlayerCatcher(const UINT8 localPlayer) { if (catcher->x == catcher->destX && catcher->y == catcher->destY) { - D_ModifyClientVote(g_localplayers[localPlayer], vote.players[localPlayer].selection); + if (vote.stage_striking == false) + { + D_ModifyClientVote(g_localplayers[localPlayer], vote.players[localPlayer].selection); + } + catcher->action = CATCHER_NA; catcher->delay = 5; S_StopSoundByNum(sfx_kc37); @@ -1434,6 +1647,201 @@ static SINT8 Y_TryMapAngerVote(void) return angryMaps[pick]; } +static void Y_ExitStageStrike(void) +{ + INT32 i; + + vote.stage_striking = false; + + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + g_votes_striked[i] = false; + } + + vote.strike_loser = NULL; + vote.strike_winner = NULL; + vote.strike_turn = false; + vote.strike_time_out = false; + + Y_SetVoteTimer(); +} + +static boolean Y_CheckStageStrikeStatus(void) +{ + INT32 i; + UINT8 num_voters = 0; + for (i = 0; i < MAXPLAYERS; i++) + { + if (Y_PlayerIDCanVote(i) == false) + { + continue; + } + + num_voters++; + if (num_voters > 2) + { + break; + } + } + + if (num_voters != 2) + { + // Someone joined or left. Stage striking is broken! + return false; + } + + if (vote.strike_loser == NULL || Y_PlayerIDCanVote(vote.strike_loser - players) == false) + { + // Loser is invalidated! + return false; + } + + if (vote.strike_winner == NULL || Y_PlayerIDCanVote(vote.strike_winner - players) == false) + { + // Winner is invalidated! + return false; + } + + // Looks good, we can tick stage striking. + return true; +} + +static void Y_TickVoteStageStrike(void) +{ + INT32 i; + + if (Y_CheckStageStrikeStatus() == false) + { + Y_ExitStageStrike(); + return; + } + + SINT8 the_only_level = VOTE_NOT_PICKED; + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + if (g_votes_striked[i] == true) + { + continue; + } + + if (the_only_level != VOTE_NOT_PICKED) + { + // More than 1 valid level. + // Unset and stop iterating. + the_only_level = VOTE_NOT_PICKED; + break; + } + + the_only_level = i; + } + + if (the_only_level != VOTE_NOT_PICKED) + { + vote.timer = 0; + vote.strikeFinalize = STRIKE_DELAY_TIME; + + if (vote.selectFinalize < SELECT_DELAY_TIME) + { + if (vote.selectFinalize == 0) + { + for (i = 0; i <= splitscreen; i++) + { + UINT8 p = g_localplayers[i]; + + if (p != (vote.strike_loser - players) + && p != (vote.strike_winner - players)) + { + continue; + } + + y_vote_player *const player = &vote.players[i]; + y_vote_catcher *const catcher = &player->catcher; + + player->selection = the_only_level; + catcher->action = CATCHER_FG_LOWER; + + catcher->x = catcher->destX = SELECTION_X + (SELECTION_SPACING_W * player->selection); + catcher->y = CATCHER_OFFSCREEN; + catcher->destY = SELECTION_Y - SELECTION_HOP; + catcher->spr = 0; + catcher->level = VOTE_NOT_PICKED; + + S_StartSound(NULL, sfx_kc37); + } + } + + vote.selectFinalize++; + } + + if (vote.selectFinalize >= SELECT_DELAY_TIME) + { + if (vote.pickFinalize < PICK_DELAY_TIME) + { + vote.pickFinalize++; + } + else if (vote.endtic == -1) + { + vote.notYetPicked = false; /* don't pick vote twice */ + + if (server) + { + D_PickVote( the_only_level ); + } + } + } + } + else + { + if (vote.timer == 0) + { + if (vote.strikeFinalize < STRIKE_DELAY_TIME) + { + vote.strikeFinalize++; + } + } + else + { + vote.strikeFinalize = 0; + } + + if (vote.strikeFinalize >= STRIKE_DELAY_TIME) + { + // We didn't get their timeout strike net command. + // Maybe they hacked their exe, or connection was + // interrupted, or some other issue. + + // Let's just strike a random stage for them. + if (server && vote.strike_time_out == false) + { + INT32 rng = M_RandomKey(VOTE_NUM_LEVELS); + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + if (g_votes_striked[i] == false) + { + break; + } + + rng++; + if (rng >= VOTE_NUM_LEVELS) + { + rng = 0; + } + } + + D_ModifyClientVote((vote.strike_turn == true) ? VOTE_TIMEOUT_WINNER : VOTE_TIMEOUT_LOSER, rng); + } + + vote.strike_time_out = true; + } + else if (vote.timer > 0) + { + vote.timer--; + vote.selectFinalize = 0; + vote.pickFinalize = 0; + } + } +} + static void Y_TickVoteSelection(void) { boolean everyone_voted = true;/* the default condition */ @@ -1463,6 +1871,22 @@ static void Y_TickVoteSelection(void) // Time's up, send our vote ASAP. if (vote.players[i].sentTimeOutVote == false) { + // Move off of striked stages for the timeout vote. + INT32 j; + for (j = 0; j < VOTE_NUM_LEVELS; j++) + { + if (g_votes_striked[vote.players[i].selection] == false) + { + break; + } + + vote.players[i].selection++; + if (vote.players[i].selection >= VOTE_NUM_LEVELS) + { + vote.players[i].selection = 0; + } + } + Y_PlayerSendVote(i); vote.players[i].sentTimeOutVote = true; vote.players[i].delay = NEWTICRATE/7; @@ -1514,12 +1938,27 @@ static void Y_TickVoteSelection(void) continue; } - if (players[i].bot == true && g_votes[i] == VOTE_NOT_PICKED) + if (server && players[i].bot == true && Y_PlayerIDCanVoteRightNow(i) == true && g_votes[i] == VOTE_NOT_PICKED) { if (( M_RandomFixed() % 100 ) == 0) { // bots vote randomly - D_ModifyClientVote(i, M_RandomKey(VOTE_NUM_LEVELS)); + INT32 rng = M_RandomKey(VOTE_NUM_LEVELS); + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + if (g_votes_striked[i] == false) + { + break; + } + + rng++; + if (rng >= VOTE_NUM_LEVELS) + { + rng = 0; + } + } + + D_ModifyClientVote(i, rng); } } @@ -1529,6 +1968,13 @@ static void Y_TickVoteSelection(void) } } + if (vote.stage_striking == true) + { + // Use the same selection logic, otherwise use separate ending logic. + Y_TickVoteStageStrike(); + return; + } + if (everyone_voted == true) { vote.timer = 0; @@ -1649,6 +2095,7 @@ static void Y_InitVoteDrawing(void) INT32 i = 0, j = 0; vote_draw.ruby_icon = W_CachePatchName("RUBYICON", PU_STATIC); + vote_draw.strike_icon = W_CachePatchName("K_NOBLNS", PU_STATIC); for (i = 0; i < PLANET_FRAMES; i++) { @@ -1736,6 +2183,103 @@ static void Y_InitVoteDrawing(void) vote_draw.selectTransition = FRACUNIT; } +static boolean Y_DetermineStageStrike(void) +{ + player_t *a = NULL; + player_t *b = NULL; + + UINT8 num_voters = 0; + + INT32 i; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (Y_PlayerIDCanVote(i) == false) + { + continue; + } + + num_voters++; + + // Just set the pointers for now, sort them later. + if (a == NULL) + { + a = &players[i]; + } + else if (b == NULL) + { + b = &players[i]; + } + else + { + // Too many players + return false; + } + } + + if (num_voters != 2 || a == NULL || b == NULL) + { + // Requires exactly 2 of them. + return false; + } + + UINT32 score_a = 0; + UINT32 score_b = 0; + + intertype_t scoring_type = Y_GetIntermissionType(); + switch (scoring_type) + { + case int_time: + { + score_a = UINT32_MAX - a->realtime; + score_b = UINT32_MAX - b->realtime; + break; + } + case int_score: + { + score_a = a->score; + score_b = b->realtime; + break; + } + default: + { + // Invalid, exit now. + return false; + } + } + + if (a->pflags & PF_NOCONTEST) + { + score_a = 0; + } + + if (b->pflags & PF_NOCONTEST) + { + score_b = 0; + } + + if (score_a == score_b) + { + // TODO: should be a coin flip, but how + // should the RNG for this be handled? + score_a++; + } + + if (score_a > score_b) + { + vote.strike_loser = b; + vote.strike_winner = a; + } + else + { + vote.strike_loser = a; + vote.strike_winner = b; + } + + vote.stage_striking = true; + return true; +} + void Y_StartVote(void) { INT32 i = 0; @@ -1749,12 +2293,6 @@ void Y_StartVote(void) vote.tic = vote.endtic = -1; -#ifdef VOTE_TIME_WAIT_FOR_VOTE - vote.timer = -1; // Timer is not set until the first vote is added -#else - vote.timer = cv_votetime.value * TICRATE; -#endif - g_pickedVote = VOTE_NOT_PICKED; vote.notYetPicked = true; @@ -1782,6 +2320,19 @@ void Y_StartVote(void) catcher->player = i; } + for (i = 0; i < VOTE_NUM_LEVELS; i++) + { + g_votes_striked[i] = false; + } + + Y_DetermineStageStrike(); + +#ifdef VOTE_TIME_WAIT_FOR_VOTE + vote.timer = -1; // Timer is not set until the first vote is added +#else + Y_SetVoteTimer(); +#endif + Y_InitVoteDrawing(); vote.loaded = true; @@ -1803,6 +2354,7 @@ static void Y_UnloadVoteData(void) } UNLOAD(vote_draw.ruby_icon); + UNLOAD(vote_draw.strike_icon); for (i = 0; i < PLANET_FRAMES; i++) { @@ -1941,4 +2493,25 @@ void Y_SetupVoteFinish(SINT8 pick, SINT8 level, SINT8 anger) vote.timer = -1; vote.selectFinalize = SELECT_DELAY_TIME; vote.pickFinalize = PICK_DELAY_TIME; + vote.strikeFinalize = STRIKE_DELAY_TIME; +} + +// +// Y_VoteContext +// + +enum +{ + VOTE_CTX_NORMAL = 0, + VOTE_CTX_DUEL, +}; + +UINT8 Y_VoteContext(void) +{ + if (vote.stage_striking == true) + { + return VOTE_CTX_DUEL; + } + + return VOTE_CTX_NORMAL; } diff --git a/src/k_vote.h b/src/k_vote.h index e5e2c6350..2edbf2b7a 100644 --- a/src/k_vote.h +++ b/src/k_vote.h @@ -19,9 +19,6 @@ extern "C" { #endif -#define VOTE_NUM_LEVELS (4) -#define VOTE_NOT_PICKED (-1) - #define VOTE_MOD_ENCORE (0x01) boolean Y_PlayerIDCanVote(const UINT8 playerId); @@ -32,6 +29,7 @@ void Y_VoteTicker(void); void Y_StartVote(void); void Y_EndVote(void); void Y_SetupVoteFinish(SINT8 pick, SINT8 level, SINT8 anger); +UINT8 Y_VoteContext(void); #ifdef __cplusplus } // extern "C" diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c index 158909815..dd80daf66 100644 --- a/src/lua_mobjlib.c +++ b/src/lua_mobjlib.c @@ -111,6 +111,7 @@ enum mobj_e { mobj_reappear, mobj_punt_ref, mobj_owner, + mobj_relinkplayer, }; static const char *const mobj_opt[] = { @@ -201,6 +202,7 @@ static const char *const mobj_opt[] = { "reappear", "punt_ref", "owner", + "relinkplayer", NULL}; #define UNIMPLEMENTED luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", mobj_opt[field]) @@ -519,6 +521,9 @@ static int mobj_get(lua_State *L) } LUA_PushUserdata(L, mo->owner, META_MOBJ); break; + case mobj_relinkplayer: + lua_pushinteger(L, mo->relinkplayer); + break; default: // extra custom variables in Lua memory lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS); I_Assert(lua_istable(L, -1)); @@ -934,6 +939,9 @@ static int mobj_set(lua_State *L) P_SetTarget(&mo->owner, owner); } break; + case mobj_relinkplayer: + mo->relinkplayer = luaL_checkinteger(L, 3); + break; default: lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS); I_Assert(lua_istable(L, -1)); diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index a2e5e20d0..a71b3214d 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -498,6 +498,14 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->sneakertimer); else if (fastcmp(field,"numsneakers")) lua_pushinteger(L, plr->numsneakers); + else if (fastcmp(field,"panelsneakertimer")) + lua_pushinteger(L, plr->panelsneakertimer); + else if (fastcmp(field,"numpanelsneakers")) + lua_pushinteger(L, plr->numpanelsneakers); + else if (fastcmp(field,"weaksneakertimer")) + lua_pushinteger(L, plr->weaksneakertimer); + else if (fastcmp(field,"numweaksneakers")) + lua_pushinteger(L, plr->numweaksneakers); else if (fastcmp(field,"floorboost")) lua_pushinteger(L, plr->floorboost); else if (fastcmp(field,"growshrinktimer")) @@ -709,6 +717,8 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->checkskip); else if (fastcmp(field,"cheatchecknum")) lua_pushinteger(L, plr->cheatchecknum); + else if (fastcmp(field,"duelscore")) + lua_pushinteger(L, plr->duelscore); else if (fastcmp(field,"lastsidehit")) lua_pushinteger(L, plr->lastsidehit); else if (fastcmp(field,"lastlinehit")) @@ -1312,6 +1322,8 @@ static int player_set(lua_State *L) plr->checkskip = (INT32)luaL_checkinteger(L, 3); else if (fastcmp(field,"cheatchecknum")) plr->cheatchecknum = (INT32)luaL_checkinteger(L, 3); + else if (fastcmp(field,"duelscore")) + plr->duelscore = (INT16)luaL_checkinteger(L, 3); else if (fastcmp(field,"lastsidehit")) plr->lastsidehit = (INT16)luaL_checkinteger(L, 3); else if (fastcmp(field,"lastlinehit")) diff --git a/src/m_cond.c b/src/m_cond.c index 5659205ee..026dace10 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1858,7 +1858,10 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && (gamespeed != KARTSPEED_EASY) && (player->tally.active == true) && (player->tally.totalExp > 0) // Only true if not Time Attack - && (player->tally.exp >= player->tally.totalExp)); + && ( + (player->tally.exp >= player->tally.totalExp) + || (K_InRaceDuel() && player->duelscore == DUELWINNINGSCORE) + )); case UCRP_FINISHALLPRISONS: return (battleprisons && !(player->pflags & PF_NOCONTEST) diff --git a/src/m_random.h b/src/m_random.h index edc59ee66..dcfeeca3e 100644 --- a/src/m_random.h +++ b/src/m_random.h @@ -92,6 +92,8 @@ typedef enum PR_ITEM_SPAWNER = PROLDDEMO, // Battle mode item spawners PR_TEAMS, // Teamplay shuffling + PR_NUISANCE, // Margin Boost HUD + PRNUMSYNCED, PR_INTERPHUDRANDOM = PRNUMSYNCED, // Interpolation-accomodating HUD randomisation diff --git a/src/menus/main-goner.cpp b/src/menus/main-goner.cpp index 5539ed5a2..75b41d172 100644 --- a/src/menus/main-goner.cpp +++ b/src/menus/main-goner.cpp @@ -1272,6 +1272,7 @@ void M_GonerTutorial(INT32 choice) // Please also see M_LevelSelectInit as called in extras-1.c levellist.netgame = false; + levellist.canqueue = false; levellist.levelsearch.checklocked = true; levellist.levelsearch.grandprix = false; levellist.levelsearch.timeattack = false; diff --git a/src/menus/options-gameplay-1.c b/src/menus/options-gameplay-1.c index 11398a6f4..7cf67ee3f 100644 --- a/src/menus/options-gameplay-1.c +++ b/src/menus/options-gameplay-1.c @@ -52,6 +52,17 @@ menuitem_t OPTIONS_Gameplay[] = NULL, {.cvar = &cv_kartbumpers}, 0, 0}, + + {IT_HEADER, "Duel...", NULL, + NULL, {NULL}, 0, 0}, + + {IT_STRING | IT_CVAR, "Duel Time Limit", "How long it takes for Margin Boost to kick in (seconds).", + NULL, {.cvar = &cv_dueltimelimit}, 0, 0}, + + {IT_STRING | IT_CVAR, "Duel Score Limit", "How many points a player must be ahead to win a Duel.", + NULL, {.cvar = &cv_duelscorelimit}, 0, 0}, + + {IT_SPACE | IT_DYBIGSPACE, NULL, NULL, NULL, {NULL}, 0, 0}, diff --git a/src/menus/options-server-1.c b/src/menus/options-server-1.c index 877d0e223..a6640a6e6 100644 --- a/src/menus/options-server-1.c +++ b/src/menus/options-server-1.c @@ -39,7 +39,7 @@ menuitem_t OPTIONS_Server[] = {IT_STRING | IT_CVAR, "CPU Level", "Bots can fill unused slots. How strong should they be?", NULL, {.cvar = &cv_kartbot}, 0, 0}, - {IT_STRING | IT_CVAR, "Use PWR.LV", "Should players should be rated on their performance?", + {IT_STRING | IT_CVAR, "Use Mobiums", "Should players should be rated on their performance?", NULL, {.cvar = &cv_kartusepwrlv}, 0, 0}, {IT_STRING | IT_CVAR, "Antigrief Timer (seconds)", "How long can players stop progressing before they're removed?", diff --git a/src/menus/options-voice.cpp b/src/menus/options-voice.cpp index 8f02fad5f..7f3a8ba00 100644 --- a/src/menus/options-voice.cpp +++ b/src/menus/options-voice.cpp @@ -72,6 +72,24 @@ static boolean input_routine(INT32) return false; } +static void init_routine(void) +{ + if (!netgame) + { + S_SoundInputSetEnabled(true); + } +} + +static boolean quit_routine(void) +{ + if (!netgame) + { + S_SoundInputSetEnabled(false); + } + + return true; +} + menu_t OPTIONS_VoiceDef = { sizeof (OPTIONS_Voice) / sizeof (menuitem_t), &OPTIONS_MainDef, @@ -85,7 +103,7 @@ menu_t OPTIONS_VoiceDef = { draw_routine, M_DrawOptionsCogs, tick_routine, - NULL, - NULL, + init_routine, + quit_routine, input_routine, }; diff --git a/src/menus/play-local-race-time-attack.c b/src/menus/play-local-race-time-attack.c index b682cfe07..7ac7343bf 100644 --- a/src/menus/play-local-race-time-attack.c +++ b/src/menus/play-local-race-time-attack.c @@ -614,34 +614,19 @@ void M_StartTimeAttack(INT32 choice) { modeattacking |= ATTACKING_SPB; } + + if (gamestate == GS_MENU) + { + encoremode = true; // guarantees short wipe + } + modeprefix = "spb-"; } // DON'T SOFTLOCK CON_ToggleOff(); - // Still need to reset devmode - cht_debug = 0; - - if (demo.playback) - G_StopDemo(); - - splitscreen = 0; - SplitScreen_OnChange(); - - S_StartSound(NULL, sfx_s3k63); - - paused = false; - - S_StopMusicCredit(); - - // 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); - - SV_StartSinglePlayerServer(levellist.newgametype, false); + M_MenuToLevelPreamble(0, (gamestate != GS_MENU || cv_dummyspbattack.value == 1)); gpath = va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder); @@ -659,8 +644,17 @@ void M_StartTimeAttack(INT32 choice) restoreMenu = &PLAY_TimeAttackDef; + D_MapChange( + levellist.choosemap+1, + levellist.newgametype, + (cv_dummyspbattack.value == 1), + true, + 1, + false, + false + ); + M_ClearMenus(true); - D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_dummyspbattack.value == 1), 1, 1, false, false); G_UpdateTimeStickerMedals(levellist.choosemap, true); } diff --git a/src/menus/play-online-host.c b/src/menus/play-online-host.c index a52f04822..ccc3214c2 100644 --- a/src/menus/play-online-host.c +++ b/src/menus/play-online-host.c @@ -172,6 +172,7 @@ void M_MPSetupNetgameMapSelect(INT32 choice) // Yep, we'll be starting a netgame. levellist.netgame = true; + levellist.canqueue = true; // Make sure we reset those levellist.levelsearch.timeattack = false; levellist.levelsearch.checklocked = true; diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index 8fa830094..f8c5266f3 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -57,26 +57,7 @@ static void M_StartCup(UINT8 entry) entry = UINT8_MAX; } - S_StartSound(NULL, sfx_s3k63); - - paused = false; - - S_StopMusicCredit(); - - // 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); - - if (cv_maxconnections.value < ssplayers+1) - CV_SetValue(&cv_maxconnections, ssplayers+1); - - if (splitscreen != ssplayers) - { - splitscreen = ssplayers; - SplitScreen_OnChange(); - } + M_MenuToLevelPreamble(ssplayers, false); if (entry == UINT8_MAX) { @@ -135,10 +116,6 @@ static void M_StartCup(UINT8 entry) } } - paused = false; - - SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame); - M_ClearMenus(true); restoreMenu = &PLAY_CupSelectDef; diff --git a/src/menus/transient/level-select.c b/src/menus/transient/level-select.c index a0c57ae8c..30c39ef75 100644 --- a/src/menus/transient/level-select.c +++ b/src/menus/transient/level-select.c @@ -21,6 +21,7 @@ #include "../../v_video.h" #include "../../g_game.h" // G_GetBackupCupData #include "../../p_saveg.h" // cupsavedata +#include "../../d_netcmd.h" // Handle_MapQueueSend cupheader_t dummy_lostandfound; @@ -657,14 +658,22 @@ void M_LevelSelectInit(INT32 choice) case 0: levellist.levelsearch.grandprix = false; levellist.levelsearch.timeattack = false; + levellist.canqueue = true; + + CV_StealthSet(&cv_kartbot, cv_dummymatchbots.string); + CV_StealthSet(&cv_kartencore, (cv_dummygpencore.value == 1) ? "On" : "Auto"); + CV_StealthSet(&cv_kartspeed, (cv_dummykartspeed.value == KARTSPEED_NORMAL) ? "Auto Gear" : cv_dummykartspeed.string); + break; case 1: levellist.levelsearch.grandprix = false; levellist.levelsearch.timeattack = true; + levellist.canqueue = false; break; case 2: levellist.levelsearch.grandprix = true; levellist.levelsearch.timeattack = false; + levellist.canqueue = false; break; default: CONS_Alert(CONS_WARNING, "Bad level select init\n"); @@ -676,10 +685,14 @@ void M_LevelSelectInit(INT32 choice) gt = menugametype; } - if (levellist.levelsearch.timeattack && gt == GT_RACE && skins[R_SkinAvailableEx(cv_skin[0].string, false)].flags & SF_HIVOLT) + if (levellist.levelsearch.timeattack && gt == GT_RACE) { - M_StartMessage("A long-forgotten power...", "You are using a \x82prototype engine\x80.\nRecords will not be saved.", NULL, MM_NOTHING, NULL, NULL); - S_StartSound(NULL, sfx_s3k81); + const INT32 skinid = R_SkinAvailableEx(cv_skin[0].string, false); + if (skinid >= 0 && (skins[skinid].flags & SF_HIVOLT)) + { + M_StartMessage("A long-forgotten power...", "You are using a \x82prototype engine\x80.\nRecords will not be saved.", NULL, MM_NOTHING, NULL, NULL); + S_StartSound(NULL, sfx_s3k81); + } } if (!M_LevelListFromGametype(gt)) @@ -689,7 +702,41 @@ void M_LevelSelectInit(INT32 choice) } } -void M_LevelSelected(INT16 add, boolean menuupdate) +void M_MenuToLevelPreamble(UINT8 ssplayers, boolean nowipe) +{ + cht_debug = 0; + + if (demo.playback) + G_StopDemo(); + + if (cv_maxconnections.value < ssplayers+1) + CV_SetValue(&cv_maxconnections, ssplayers+1); + + if (splitscreen != ssplayers) + { + splitscreen = ssplayers; + SplitScreen_OnChange(); + } + + paused = false; + + S_StopMusicCredit(); + + if (!nowipe) + { + S_StartSound(NULL, sfx_s3k63); + + // 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); + } + + SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame); +} + +INT16 M_LevelFromScrolledList(INT16 add) { UINT8 i = 0; INT16 map = M_GetFirstLevelInList(&i, &levellist.levelsearch); @@ -706,6 +753,13 @@ void M_LevelSelected(INT16 add, boolean menuupdate) add--; } + return map; +} + +void M_LevelSelected(INT16 add, boolean menuupdate) +{ + INT16 map = M_LevelFromScrolledList(add); + if (map >= nummapheaders) { // This shouldn't happen @@ -733,75 +787,116 @@ void M_LevelSelected(INT16 add, boolean menuupdate) { if (gamestate == GS_MENU) { - UINT8 ssplayers = levellist.levelsearch.tutorial ? 0 : cv_splitplayers.value-1; - - netgame = false; multiplayer = true; - strlcpy(connectedservername, cv_servername.string, MAXSERVERNAME); + M_MenuToLevelPreamble( + (levellist.levelsearch.tutorial + ? 0 + : cv_splitplayers.value-1 + ), + ( + (cv_kartencore.value == 1) + && (gametypes[levellist.newgametype]->rules & GTR_ENCORE) + ) + ); - // Still need to reset devmode - cht_debug = 0; - - if (demo.playback) - G_StopDemo(); - - /*if (levellist.choosemap == 0) - levellist.choosemap = G_RandMap(G_TOLFlag(levellist.newgametype), -1, 0, 0, false, NULL);*/ - - if (cv_maxconnections.value < ssplayers+1) - CV_SetValue(&cv_maxconnections, ssplayers+1); - - if (splitscreen != ssplayers) - { - splitscreen = ssplayers; - SplitScreen_OnChange(); - } - - S_StartSound(NULL, sfx_s3k63); - - paused = false; - - S_StopMusicCredit(); - - // 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); - - SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame); - - if (!levellist.netgame) - CV_StealthSet(&cv_kartbot, cv_dummymatchbots.string); - - CV_StealthSet(&cv_kartencore, (cv_dummygpencore.value == 1) ? "On" : "Auto"); - CV_StealthSet(&cv_kartspeed, (cv_dummykartspeed.value == KARTSPEED_NORMAL) ? "Auto Gear" : cv_dummykartspeed.string); - - D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false); - - if (levellist.netgame == true) - { - restoreMenu = &PLAY_MP_OptSelectDef; - } - else /*if (!M_GameTrulyStarted() || - levellist.levelsearch.tutorial)*/ - { - restoreMenu = currentMenu; - } + restoreMenu = (levellist.netgame) + ? &PLAY_MP_OptSelectDef + : currentMenu; restorelevellist = levellist; } - else - { - // directly do the map change - D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false); - } + + D_MapChange( + levellist.choosemap+1, + levellist.newgametype, + (cv_kartencore.value == 1), + true, + 1, + false, + false + ); M_ClearMenus(true); } } +static void M_MenuQueueStopSend(INT32 ch) +{ + (void)ch; + + memset(&menuqueue, 0, sizeof(struct menuqueue)); +} + +static void M_MenuQueueSelectedLocal(void) +{ + UINT8 i = 0; + + for (; i < menuqueue.size; i++) + { + G_MapIntoRoundQueue( + menuqueue.entries[i].mapnum, + menuqueue.entries[i].gametype, + menuqueue.entries[i].encore, + false + ); + } + + roundqueue.netcommunicate = true; + + if (gamestate == GS_MENU) + { + levellist.choosemap = roundqueue.entries[0].mapnum; + + multiplayer = true; + + M_MenuToLevelPreamble( + cv_splitplayers.value-1, + ( + roundqueue.entries[0].encore + && (gametypes[roundqueue.entries[0].gametype]->rules & GTR_ENCORE) + ) + ); + + restoreMenu = (levellist.netgame) + ? &PLAY_MP_OptSelectDef + : currentMenu; + + restorelevellist = levellist; + + roundqueue.position = roundqueue.roundnum = 1; + + D_MapChange( + roundqueue.entries[0].mapnum + 1, + roundqueue.entries[0].gametype, + roundqueue.entries[0].encore, + true, + 1, + false, + roundqueue.entries[0].rankrestricted + ); + + M_MenuQueueStopSend(MA_NONE); + + M_ClearMenus(true); + } + else + { + if (gamestate == GS_LEVEL && roundqueue.position == 0) + { + menuqueue.sending = UINT8_MAX; + } + else + { + S_StartSound(NULL, sfx_chchng); + + M_MenuQueueStopSend(MA_NONE); + + M_ClearMenus(true); + } + } +} + boolean M_LevelSelectCupSwitch(boolean next, boolean skipones) { levelsearch_t templevelsearch = levellist.levelsearch; @@ -883,6 +978,44 @@ boolean M_LevelSelectCupSwitch(boolean next, boolean skipones) } } +static void M_MenuQueueResponse(INT32 ch) +{ + M_ClearMenus(false); + + if (ch != MA_YES) + return; + + if (!(server || (IsPlayerAdmin(consoleplayer)))) + return; + + if (!(gamestate == GS_LEVEL && roundqueue.position == 0)) + return; + + SendNetXCmd(XD_EXITLEVEL, NULL, 0); +} + + +static void M_ClearQueueResponse(INT32 ch) +{ + if (ch != MA_YES) + return; + + if (!(server || (IsPlayerAdmin(consoleplayer)))) + return; + + S_StartSound(NULL, sfx_slip); + + if (netgame) + { + if (roundqueue.size) + { + Handle_MapQueueSend(0, ROUNDQUEUE_CMD_CLEAR, false); + } + return; + } + + memset(&roundqueue, 0, sizeof(struct roundqueue)); +} void M_LevelSelectHandler(INT32 choice) { const UINT8 pid = 0; @@ -894,6 +1027,11 @@ void M_LevelSelectHandler(INT32 choice) return; } + if (menuqueue.sending) + { + return; + } + if (menucmd[pid].dpad_ud > 0) { levellist.cursor++; @@ -926,10 +1064,96 @@ void M_LevelSelectHandler(INT32 choice) M_LevelSelectScrollDest(); - if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/) + if (M_MenuConfirmPressed(pid)) { + // Starting immediately OR importing queue + M_SetMenuDelay(pid); - M_LevelSelected(levellist.cursor, true); + + while ((menuqueue.size + roundqueue.size) > ROUNDQUEUE_MAX) + menuqueue.size--; + + if (!levellist.canqueue || !menuqueue.size) + { + M_LevelSelected(levellist.cursor, true); + } + else if (netgame) + { + menuqueue.anchor = roundqueue.size; + menuqueue.sending = 1; + + M_StartMessage("Queueing Rounds", + va(M_GetText( + "Attempting to send %d Round%s...\n" + "\n" + "If this is taking longer than you\n" + "expect, exit out of this message.\n" + ), menuqueue.size, (menuqueue.size == 1 ? "" : "s") + ), &M_MenuQueueStopSend, MM_NOTHING, + NULL, + "This is taking too long..." + ); + } + else + { + M_MenuQueueSelectedLocal(); + } + } + else if (levellist.canqueue && M_MenuButtonPressed(pid, MBT_Z)) + { + // Adding to queue + + INT16 map = NEXTMAP_INVALID; + + M_SetMenuDelay(pid); + + if ((roundqueue.size + menuqueue.size) < ROUNDQUEUE_MAX) + { + map = M_LevelFromScrolledList(levellist.cursor); + } + + if (map < nummapheaders) + { + memset(menuqueue.entries+menuqueue.size, 0, sizeof(roundentry_t)); + menuqueue.entries[menuqueue.size].mapnum = map; + menuqueue.entries[menuqueue.size].gametype = levellist.newgametype; + menuqueue.entries[menuqueue.size].encore = (cv_kartencore.value == 1); + + menuqueue.size++; + + S_StartSound(NULL, sfx_s3k4a); + } + else + { + S_StartSound(NULL, sfx_s3kb2); + } + } + else if (levellist.canqueue && M_MenuExtraPressed(pid)) + { + while ((menuqueue.size + roundqueue.size) > ROUNDQUEUE_MAX) + menuqueue.size--; + + if (menuqueue.size) + { + S_StartSound(NULL, sfx_shldls); + menuqueue.size--; + } + else if (roundqueue.size) + { + M_StartMessage("Queue Clearing", + va(M_GetText( + "There %s %d Round%s of play queued.\n" + "\n" + "Do you want to empty the queue?\n" + ), + (roundqueue.size == 1 ? "is" : "are"), + roundqueue.size, + (roundqueue.size == 1 ? "" : "s") + ), &M_ClearQueueResponse, MM_YESNO, + "Time to start fresh", + "Not right now" + ); + } } else if (M_MenuBackPressed(pid)) { @@ -944,4 +1168,55 @@ void M_LevelSelectHandler(INT32 choice) void M_LevelSelectTick(void) { + if (!menuqueue.sending) + return; + + if ((menuqueue.sending <= menuqueue.size) // Sending + && (roundqueue.size >= menuqueue.anchor)) // Didn't get it wiped + { + if (!netgame) + return; + + const UINT8 idcount = (roundqueue.size - menuqueue.anchor); + + if (idcount == menuqueue.sending-1) + { + Handle_MapQueueSend( + menuqueue.entries[idcount].mapnum, + menuqueue.entries[idcount].gametype, + menuqueue.entries[idcount].encore + ); + } + if (idcount >= menuqueue.sending-1) + { + menuqueue.sending++; + } + return; + } + + if (menuqueue.size) + { + S_StartSound(NULL, sfx_chchng); + + if (roundqueue.position || gamestate != GS_LEVEL) + { + M_StopMessage(MA_NONE); + } + else + { + M_StartMessage("Round Queue", + va(M_GetText( + "You just queued %d Round%s of play.\n" + "\n" + "Do you want to skip the current\n" + "course and start them immediately?\n" + ), menuqueue.size, (menuqueue.size == 1 ? "" : "s") + ), &M_MenuQueueResponse, MM_YESNO, + "Let's get going!", + "Not right now" + ); + } + } + + M_MenuQueueStopSend(MA_NONE); } diff --git a/src/menus/transient/pause-replay.c b/src/menus/transient/pause-replay.c index 864344d91..ef5d1eb1e 100644 --- a/src/menus/transient/pause-replay.c +++ b/src/menus/transient/pause-replay.c @@ -39,21 +39,21 @@ menuitem_t PAUSE_PlaybackMenu[] = {IT_CALL | IT_STRING, "Hide Menu", NULL, "M_PHIDE", {.routine = M_SelectableClearMenus}, 0, 0}, {IT_CALL | IT_STRING, "Restart", NULL, "M_PRSTRT", {.routine = M_PlaybackRewind}, 20, 0}, - {IT_CALL | IT_STRING, "Pause", NULL, "M_PPAUSE", {.routine = M_PlaybackPause}, 36, 0}, - {IT_CALL | IT_STRING, "Fast-Forward", NULL, "M_PFFWD", {.routine = M_PlaybackFastForward}, 52, 0}, - {IT_CALL | IT_STRING, "Restart", NULL, "M_PRSTRT", {.routine = M_PlaybackRewind}, 20, 0}, - {IT_CALL | IT_STRING, "Resume", NULL, "M_PRESUM", {.routine = M_PlaybackPause}, 36, 0}, - {IT_CALL | IT_STRING, "Advance Frame", NULL, "M_PFADV", {.routine = M_PlaybackAdvance}, 52, 0}, + {IT_CALL | IT_STRING, "Rewind 5 seconds", NULL, "M_PREW", {.routine = M_PlaybackRewind}, 36, 0}, + {IT_CALL | IT_STRING, "Pause", NULL, "M_PPAUSE", {.routine = M_PlaybackPause}, 52, 0}, + {IT_CALL | IT_STRING, "Fast-Forward", NULL, "M_PFFWD", {.routine = M_PlaybackFastForward}, 68, 0}, + {IT_CALL | IT_STRING, "Resume", NULL, "M_PRESUM", {.routine = M_PlaybackPause}, 52, 0}, + {IT_CALL | IT_STRING, "Advance Frame", NULL, "M_PFADV", {.routine = M_PlaybackAdvance}, 68, 0}, - {IT_ARROWS | IT_STRING, "View Count", NULL, "M_PVIEWS", {.routine = M_PlaybackSetViews}, 72, 0}, - {IT_ARROWS | IT_STRING, "Viewpoint", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 88, 0}, - {IT_ARROWS | IT_STRING, "Viewpoint 2", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 104, 0}, - {IT_ARROWS | IT_STRING, "Viewpoint 3", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 120, 0}, - {IT_ARROWS | IT_STRING, "Viewpoint 4", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 136, 0}, + {IT_ARROWS | IT_STRING, "View Count", NULL, "M_PVIEWS", {.routine = M_PlaybackSetViews}, 88, 0}, + {IT_ARROWS | IT_STRING, "Viewpoint", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 104, 0}, + {IT_ARROWS | IT_STRING, "Viewpoint 2", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 120, 0}, + {IT_ARROWS | IT_STRING, "Viewpoint 3", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 136, 0}, + {IT_ARROWS | IT_STRING, "Viewpoint 4", NULL, "M_PNVIEW", {.routine = M_PlaybackAdjustView}, 152, 0}, - {IT_CALL | IT_STRING, "Toggle Director", NULL, "UN_IC11A", {.routine = M_PlaybackToggleDirector}, 156, 0}, - {IT_CALL | IT_STRING, "Toggle Free Camera", NULL, "M_PVIEWS", {.routine = M_PlaybackToggleFreecam}, 172, 0}, - {IT_CALL | IT_STRING, "Stop Playback", NULL, "M_PEXIT", {.routine = M_PlaybackQuit}, 188, 0}, + {IT_CALL | IT_STRING, "Toggle Director", NULL, "UN_IC11A", {.routine = M_PlaybackToggleDirector}, 172, 0}, + {IT_CALL | IT_STRING, "Toggle Free Camera", NULL, "M_PVIEWS", {.routine = M_PlaybackToggleFreecam}, 188, 0}, + {IT_CALL | IT_STRING, "Stop Playback", NULL, "M_PEXIT", {.routine = M_PlaybackQuit}, 204, 0}, }; menu_t PAUSE_PlaybackMenuDef = { @@ -151,21 +151,21 @@ static void M_PlaybackTick(void) playback_last_menu_interaction_leveltime = leveltime - 6*TICRATE; // Toggle items - if (paused && !demo.rewinding) + if (paused) { - PAUSE_PlaybackMenu[playback_pause].status = PAUSE_PlaybackMenu[playback_fastforward].status = PAUSE_PlaybackMenu[playback_rewind].status = IT_DISABLED; - PAUSE_PlaybackMenu[playback_resume].status = PAUSE_PlaybackMenu[playback_advanceframe].status = PAUSE_PlaybackMenu[playback_backframe].status = IT_CALL|IT_STRING; + PAUSE_PlaybackMenu[playback_pause].status = PAUSE_PlaybackMenu[playback_fastforward].status = IT_DISABLED; + PAUSE_PlaybackMenu[playback_resume].status = PAUSE_PlaybackMenu[playback_advanceframe].status = IT_CALL|IT_STRING; - if (itemOn >= playback_rewind && itemOn <= playback_fastforward) - itemOn += playback_backframe - playback_rewind; + if (itemOn >= playback_pause && itemOn <= playback_fastforward) + itemOn += playback_resume - playback_pause; } else { - PAUSE_PlaybackMenu[playback_pause].status = PAUSE_PlaybackMenu[playback_fastforward].status = PAUSE_PlaybackMenu[playback_rewind].status = IT_CALL|IT_STRING; - PAUSE_PlaybackMenu[playback_resume].status = PAUSE_PlaybackMenu[playback_advanceframe].status = PAUSE_PlaybackMenu[playback_backframe].status = IT_DISABLED; + PAUSE_PlaybackMenu[playback_pause].status = PAUSE_PlaybackMenu[playback_fastforward].status = IT_CALL|IT_STRING; + PAUSE_PlaybackMenu[playback_resume].status = PAUSE_PlaybackMenu[playback_advanceframe].status = IT_DISABLED; - if (itemOn >= playback_backframe && itemOn <= playback_advanceframe) - itemOn -= playback_backframe - playback_rewind; + if (itemOn >= playback_resume && itemOn <= playback_advanceframe) + itemOn -= playback_resume - playback_pause; } if (modeattacking) @@ -173,10 +173,10 @@ static void M_PlaybackTick(void) for (i = playback_viewcount; i <= playback_director; i++) PAUSE_PlaybackMenu[i].status = IT_DISABLED; - PAUSE_PlaybackMenu[playback_freecam].mvar1 = 72; - PAUSE_PlaybackMenu[playback_quit].mvar1 = 88; + PAUSE_PlaybackMenu[playback_freecam].mvar1 = 88; + PAUSE_PlaybackMenu[playback_quit].mvar1 = 104; - currentMenu->x = BASEVIDWIDTH/2 - 52; + currentMenu->x = BASEVIDWIDTH/2 - 60; } else { @@ -189,11 +189,11 @@ static void M_PlaybackTick(void) for (i = r_splitscreen+1; i < 4; i++) PAUSE_PlaybackMenu[playback_view1+i].status = IT_DISABLED; - PAUSE_PlaybackMenu[playback_freecam].mvar1 = 172; - PAUSE_PlaybackMenu[playback_quit].mvar1 = 188; + PAUSE_PlaybackMenu[playback_freecam].mvar1 = 188; + PAUSE_PlaybackMenu[playback_quit].mvar1 = 204; - //currentMenu->x = BASEVIDWIDTH/2 - 94; - currentMenu->x = BASEVIDWIDTH/2 - 96; + //currentMenu->x = BASEVIDWIDTH/2 - 102; + currentMenu->x = BASEVIDWIDTH/2 - 104; } } @@ -204,34 +204,38 @@ void M_SetPlaybackMenuPointer(void) void M_PlaybackRewind(INT32 choice) { -#if 0 - static tic_t lastconfirmtime; + const tic_t curleveltime = leveltime; - (void)choice; - - if (!demo.rewinding) + if (choice == playback_rewind) { - if (paused) + demo.simplerewind = (paused ? DEMO_REWIND_PAUSE : DEMO_REWIND_RESUME); + menuactive = false; + } + + G_DoPlayDemo(NULL); // Restart the current demo + + if (demo.simplerewind) + { + if (curleveltime > 5*TICRATE) { - G_ConfirmRewind(leveltime-1); - paused = true; - S_PauseAudio(); + g_fast_forward = curleveltime - (5 * TICRATE); + g_fast_forward_clock_stop = INFTICS; //I_GetTime() + 2 * TICRATE; -- maybe? } else - demo.rewinding = paused = true; + { + if (demo.simplerewind == DEMO_REWIND_PAUSE) + { + paused = true; + S_PauseAudio(); + } + demo.simplerewind = DEMO_REWIND_OFF; + } + menuactive = true; } - else if (lastconfirmtime + TICRATE/2 < I_GetTime()) + else { - lastconfirmtime = I_GetTime(); - G_ConfirmRewind(leveltime); + M_ClearMenus(true); } - - CV_SetValue(&cv_playbackspeed, 1); -#else - (void)choice; - G_DoPlayDemo(NULL); // Restart the current demo - M_ClearMenus(true); -#endif } void M_PlaybackPause(INT32 choice) @@ -240,13 +244,7 @@ void M_PlaybackPause(INT32 choice) paused = !paused; - if (demo.rewinding) - { - G_ConfirmRewind(leveltime); - paused = true; - S_PauseAudio(); - } - else if (paused) + if (paused) S_PauseAudio(); else S_ResumeAudio(); @@ -258,12 +256,6 @@ void M_PlaybackFastForward(INT32 choice) { (void)choice; - if (demo.rewinding) - { - G_ConfirmRewind(leveltime); - paused = false; - S_ResumeAudio(); - } CV_SetValue(&cv_playbackspeed, cv_playbackspeed.value == 1 ? 4 : 1); } diff --git a/src/objects/checkpoint.cpp b/src/objects/checkpoint.cpp index 36e1eec84..97bba1cd5 100644 --- a/src/objects/checkpoint.cpp +++ b/src/objects/checkpoint.cpp @@ -495,7 +495,24 @@ struct CheckpointManager else // Checkpoint isn't in the list, find any associated tagged lines and make the pair { if (chk->linetag()) - lines_.try_emplace(chk->linetag(), tagged_lines(chk->linetag())); + { + auto lines = tagged_lines(chk->linetag()); + if (lines.empty() && gametype != GT_TUTORIAL) + { + CONS_Alert(CONS_WARNING, "Checkpoint thing %d, has linetag %d, but no lines found. Please ensure all checkpoints have associated lines.\n", chk->spawnpoint - mapthings, chk->linetag()); + } + else + { + lines_.try_emplace(chk->linetag(), lines); + } + } + else + { + if (gametype != GT_TUTORIAL) + { + CONS_Alert(CONS_WARNING, "Checkpoint thing %d, has no linetag. Please ensure all checkpoint things have a linetag.\n", chk->spawnpoint - mapthings); + } + } list_.push_front(chk); count_ += 1; // Mobjlist can't have a count on it, so we keep it here } @@ -567,6 +584,11 @@ void Obj_CheckpointThink(mobj_t* end) void Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) { + if (player->exiting) // can't cross checkpoints when exiting + { + return; + } + LineOnDemand ray(old_x, old_y, player->mo->x, player->mo->y, player->mo->radius); auto it = std::find_if( diff --git a/src/objects/crate.cpp b/src/objects/crate.cpp index bfea4d900..224880d14 100644 --- a/src/objects/crate.cpp +++ b/src/objects/crate.cpp @@ -110,7 +110,7 @@ struct Side : Graphic struct Toucher : Mobj { - bool boosting() const { return player && (player->sneakertimer || player->panelsneakertimer || K_PlayerCanPunt(player)); } + bool boosting() const { return player && (player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || K_PlayerCanPunt(player)); } }; struct AnyBox : Graphic diff --git a/src/objects/flame-shield.cpp b/src/objects/flame-shield.cpp index bed96c97a..df8d5b255 100644 --- a/src/objects/flame-shield.cpp +++ b/src/objects/flame-shield.cpp @@ -60,6 +60,7 @@ struct Visual : Mobj } move_origin(shield()->pos()); + scale(5 * shield()->follow()->scale() / 4); renderflags = (renderflags & ~RF_DONTDRAW) | (shield()->state()->num() == S_INVISIBLE ? 0 : RF_DONTDRAW); diff --git a/src/objects/hyudoro.c b/src/objects/hyudoro.c index f1e0f440e..c43317928 100644 --- a/src/objects/hyudoro.c +++ b/src/objects/hyudoro.c @@ -599,7 +599,8 @@ hyudoro_patrol_hit_player P_SetTarget(&hyudoro_target(hyu), master); - K_SpawnAmps(master->player, K_PvPAmpReward(20, master->player, player), toucher); + if (master && !P_MobjWasRemoved(master)) + K_SpawnAmps(master->player, K_PvPAmpReward(20, master->player, player), toucher); if (center) P_RemoveMobj(center); diff --git a/src/objects/lightning-shield.cpp b/src/objects/lightning-shield.cpp index e08e4529c..3c80e1b70 100644 --- a/src/objects/lightning-shield.cpp +++ b/src/objects/lightning-shield.cpp @@ -55,6 +55,7 @@ struct Visual : Mobj } move_origin(shield()->pos()); + scale(5 * shield()->follow()->scale() / 4); dispoffset = state()->num() == S_THNB1 ? -1 : 1; return true; diff --git a/src/objects/orbinaut.c b/src/objects/orbinaut.c index 501bc0e2d..9767f49f1 100644 --- a/src/objects/orbinaut.c +++ b/src/objects/orbinaut.c @@ -190,7 +190,7 @@ boolean Obj_OrbinautJawzCollide(mobj_t *t1, mobj_t *t2) return true; } - if (K_TryPickMeUp(t1, t2)) + if (K_TryPickMeUp(t1, t2, false)) return true; if (t1->type == MT_GARDENTOP) diff --git a/src/objects/rocks.cpp b/src/objects/rocks.cpp index 8bf400838..1ed75110d 100644 --- a/src/objects/rocks.cpp +++ b/src/objects/rocks.cpp @@ -96,7 +96,7 @@ private: { const player_t* p = toucher->player; - if (p->sneakertimer || p->panelsneakertimer || p->invincibilitytimer || p->growshrinktimer > 0 || p->hyudorotimer) + if (p->sneakertimer || p->panelsneakertimer || p->weaksneakertimer || p->invincibilitytimer || p->growshrinktimer > 0 || p->hyudorotimer) { return; } diff --git a/src/objects/sealed-star.c b/src/objects/sealed-star.c index ccf136800..2ee16a6fd 100644 --- a/src/objects/sealed-star.c +++ b/src/objects/sealed-star.c @@ -542,58 +542,6 @@ void Obj_SLSTMaceMobjThink(mobj_t* mo) } } -#define BUMPER_STRENGTH (56) - -void Obj_SSBumperTouchSpecial(mobj_t* special, mobj_t* toucher) -{ - angle_t hang; - angle_t vang; - fixed_t str; - int i; - - hang = R_PointToAngle2(special->x, special->y, toucher->x, toucher->y); - vang = 0; - - if (P_IsObjectOnGround(toucher) == false) - { - vang = R_PointToAngle2( - FixedHypot(special->x, special->y), special->z + (special->height >> 1), - FixedHypot(toucher->x, toucher->y), toucher->z + (toucher->height >> 1) - ); - } - - str = (BUMPER_STRENGTH * special->scale) >> 1; - - toucher->momx = FixedMul(FixedMul(str, FCOS(hang)), abs(FCOS(vang))); - toucher->momy = FixedMul(FixedMul(str, FSIN(hang)), abs(FCOS(vang))); - toucher->momz = FixedMul(str, FSIN(vang)); - - if (toucher->player) - { - if (toucher->player->tiregrease == 0) - { - for (i = 0; i < 2; i++) - { - mobj_t *grease = P_SpawnMobjFromMobj(toucher, 0, 0, 0, MT_TIREGREASE); - P_SetTarget(&grease->target, toucher); - grease->angle = toucher->angle; - grease->extravalue1 = i; - } - } - - if (toucher->player->tiregrease < 2*TICRATE) // greasetics - { - toucher->player->tiregrease = 2*TICRATE; - } - } - - if (special->state != &states[special->info->seestate]) - { - S_StartSound(special, special->info->deathsound); - P_SetMobjState(special, special->info->seestate); - } -} - void Obj_SSBumperMobjSpawn(mobj_t* mo) { mo->shadowscale = FRACUNIT; diff --git a/src/objects/ufo.c b/src/objects/ufo.c index f753874c3..8bdce7962 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -895,7 +895,7 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) { // Players deal damage relative to how many sneakers they used. targetdamaging = UFOD_BOOST; - ret = 15 * max(1, inflictor->player->numsneakers + inflictor->player->numpanelsneakers); + ret = 15 * max(1, inflictor->player->numsneakers + inflictor->player->numpanelsneakers + inflictor->player->numweaksneakers); break; } case MT_SPECIAL_UFO: @@ -1056,6 +1056,8 @@ void Obj_PlayerUFOCollide(mobj_t *ufo, mobj_t *other) other->player->numsneakers = 0; other->player->panelsneakertimer = 0; other->player->numpanelsneakers = 0; + other->player->weaksneakertimer = 0; + other->player->numweaksneakers = 0; // Copied from Obj_OrbinautThrown const ffloor_t *rover = P_IsObjectFlipped(other) ? other->ceilingrover : other->floorrover; diff --git a/src/p_enemy.c b/src/p_enemy.c index de957c0c4..3006b7369 100644 --- a/src/p_enemy.c +++ b/src/p_enemy.c @@ -321,6 +321,7 @@ void A_BlendEyePuyoHack(mobj_t *actor); void A_MakeSSCandle(mobj_t *actor); void A_HologramRandomTranslucency(mobj_t *actor); void A_SSChainShatter(mobj_t *actor); +void A_GenericBumper(mobj_t *actor); //for p_enemy.c @@ -12668,3 +12669,54 @@ void A_SSChainShatter(mobj_t* actor) actor->fuse = 1; } + +// var1 = If -1, triggered by collision event +// var2 = Strength value +// +// mobjinfo dependencies: +// - deathsound - bumper noise +// - seestate - bumper flashing state +// +void A_GenericBumper(mobj_t* actor) +{ + if (var1 != -1) + return; + + mobj_t *other = actor->target; + + if (!other) + return; + + // This code was ported from Lua + // Original was Balloon Park's bumpers? + INT32 hang = R_PointToAngle2( + actor->x, actor->y, + other->x, other->y + ); + + INT32 vang = 0; + + if (!P_IsObjectOnGround(other)) + { + vang = R_PointToAngle2( + FixedHypot(actor->x, actor->y), actor->z + (actor->height / 2), + FixedHypot(other->x, other->y), other->z + (other->height / 2) + ); + } + + INT32 baseStrength = abs(astate->var2); + fixed_t strength = (baseStrength * actor->scale) / 2; + + other->momx = FixedMul(FixedMul(strength, FCOS(hang)), abs(FCOS(vang))); + other->momy = FixedMul(FixedMul(strength, FSIN(hang)), abs(FCOS(vang))); + other->momz = FixedMul(strength, FSIN(vang)); + + if (other->player) + K_SetTireGrease(other->player, max(other->player->tiregrease, 2*TICRATE)); + + if (actor->state != &states[actor->info->seestate]) + { + S_StartSound(actor, actor->info->deathsound); + P_SetMobjState(actor, actor->info->seestate); + } +} diff --git a/src/p_inter.c b/src/p_inter.c index 2df953dec..0a74ab9c4 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -667,7 +667,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (!player->mo || player->spectator) return; - if (K_TryPickMeUp(special, toucher)) + if (K_TryPickMeUp(special, toucher, false)) return; // attach to player! @@ -1083,10 +1083,6 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) Obj_TrickBalloonTouchSpecial(special, toucher); return; - case MT_SEALEDSTAR_BUMPER: - Obj_SSBumperTouchSpecial(special, toucher); - return; - case MT_PULLUPHOOK: Obj_PulleyHookTouch(special, toucher); return; @@ -3292,6 +3288,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da player->sneakertimer = player->numsneakers = 0; player->panelsneakertimer = player->numpanelsneakers = 0; + player->weaksneakertimer = player->numweaksneakers = 0; player->driftboost = player->strongdriftboost = 0; player->gateBoost = 0; player->fastfall = 0; diff --git a/src/p_link.cpp b/src/p_link.cpp index bbb2773de..8393ea176 100644 --- a/src/p_link.cpp +++ b/src/p_link.cpp @@ -21,6 +21,7 @@ svg_rocks + using link = mobj_t*; using each_ref = std::initializer_list>; diff --git a/src/p_local.h b/src/p_local.h index 505ccd8e0..af4f722bf 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -612,6 +612,9 @@ void P_InitTIDHash(void); void P_AddThingTID(mobj_t *mo); void P_RemoveThingTID(mobj_t *mo); void P_SetThingTID(mobj_t *mo, mtag_t tid); + +// This function cannot be safely called after *i is removed! +// Please call at start of loops if *i is to be mutated mobj_t *P_FindMobjFromTID(mtag_t tid, mobj_t *i, mobj_t *activator); void P_DeleteMobjStringArgs(mobj_t *mobj); diff --git a/src/p_map.c b/src/p_map.c index bdec595d7..07090967f 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -1552,7 +1552,19 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) if (!K_PuntCollide(thing, g_tm.thing)) { - K_KartSolidBounce(g_tm.thing, thing); + state_t *st = &states[thing->info->spawnstate]; + + if (st->action.acp1 == A_GenericBumper) + { + P_SetTarget(&thing->target, g_tm.thing); + + var1 = -1; + var2 = 0; + astate = st; + st->action.acp1(thing); + } + else + K_KartSolidBounce(g_tm.thing, thing); } return BMIT_CONTINUE; } diff --git a/src/p_mobj.c b/src/p_mobj.c index d6782f179..b0ce8a6d6 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7864,10 +7864,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (!mobj->target || !mobj->target->health || !mobj->target->player || !mobj->target->player->tripwireLeniency) { - if (mobj->target && mobj->target->player && P_IsDisplayPlayer(mobj->target->player)) + if (mobj->target && mobj->target->player + && P_IsDisplayPlayer(mobj->target->player) + && !(mobj->target->player->curshield == KSHIELD_TOP)) { - S_StopSoundByID(mobj->target, sfx_s3k40); - S_StartSound(mobj->target, sfx_gshaf); + S_StopSoundByID(mobj->target, TRIPWIRE_OK_SOUND); + S_StartSound(mobj->target, TRIPWIRE_NG_SOUND); } P_RemoveMobj(mobj); @@ -7989,7 +7991,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (p) { - UINT16 timer = max(p->sneakertimer, p->panelsneakertimer); + UINT16 timer = max(max(p->sneakertimer, p->panelsneakertimer), p->weaksneakertimer); if (timer > mobj->movecount) P_SetMobjState(mobj, S_BOOSTFLAME); mobj->movecount = timer; @@ -9110,7 +9112,10 @@ static boolean P_MobjRegularThink(mobj_t *mobj) && (gamespeed != KARTSPEED_EASY) && (newplayer->tally.active == true) && (newplayer->tally.totalExp > 0) // Only true if not Time Attack - && (newplayer->tally.exp >= newplayer->tally.totalExp) + && ( + (newplayer->tally.exp >= newplayer->tally.totalExp) || + (K_InRaceDuel() && newplayer->duelscore == DUELWINNINGSCORE) + ) ) { UINT8 pnum = (newplayer-players); @@ -10350,9 +10355,24 @@ void P_MobjThinker(mobj_t *mobj) I_Assert(mobj != NULL); I_Assert(!P_MobjWasRemoved(mobj)); + if (P_IsKartItem(mobj->type) && mobj->target && !P_MobjWasRemoved(mobj->target)) + { + player_t *link = mobj->target->player; + if (link && playeringame[link-players] && !link->spectator) + mobj->relinkplayer = (link-players) + 1; + } + // Remove dead target/tracer. if (mobj->target && P_MobjWasRemoved(mobj->target)) + { P_SetTarget(&mobj->target, NULL); + if (P_IsKartItem(mobj->type) && mobj->relinkplayer && mobj->relinkplayer <= MAXPLAYERS) + { + player_t *relink = &players[mobj->relinkplayer-1]; + if (playeringame[relink-players] && !relink->spectator && relink->mo && !P_MobjWasRemoved(relink->mo)) + P_SetTarget(&mobj->target, relink->mo); + } + } if (mobj->tracer && P_MobjWasRemoved(mobj->tracer)) P_SetTarget(&mobj->tracer, NULL); if (mobj->hnext && P_MobjWasRemoved(mobj->hnext)) @@ -15414,6 +15434,8 @@ void P_SetThingTID(mobj_t *mo, mtag_t tid) // // P_FindMobjFromTID // Mobj tag search function. +// This function cannot be safely called after *i is removed! +// Please call at start of loops if *i is to be mutated // mobj_t *P_FindMobjFromTID(mtag_t tid, mobj_t *i, mobj_t *activator) { diff --git a/src/p_mobj.h b/src/p_mobj.h index 08b41922e..353011ddf 100644 --- a/src/p_mobj.h +++ b/src/p_mobj.h @@ -450,6 +450,8 @@ struct mobj_t INT32 po_movecount; // Polyobject carrying (NOT savegame, NOT Lua) + UINT8 relinkplayer; // reassociate kartitem target when it dies. ACHTUNG 1-INDEXED + // WARNING: New fields must be added separately to savegame and Lua. }; diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 27652f709..2c7b05fc9 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -291,6 +291,7 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT32(save->p, players[i].exp); WRITEINT32(save->p, players[i].gradingfactor); WRITEUINT16(save->p, players[i].gradingpointnum); + WRITEINT16(save->p, players[i].duelscore); WRITEINT32(save->p, players[i].cheatchecknum); WRITEINT32(save->p, players[i].checkpointId); @@ -549,6 +550,8 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].numsneakers); WRITEUINT16(save->p, players[i].panelsneakertimer); WRITEUINT8(save->p, players[i].numpanelsneakers); + WRITEUINT16(save->p, players[i].weaksneakertimer); + WRITEUINT8(save->p, players[i].numweaksneakers); WRITEUINT8(save->p, players[i].floorboost); @@ -983,6 +986,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].exp = READUINT32(save->p); players[i].gradingfactor = READINT32(save->p); players[i].gradingpointnum = READUINT16(save->p); + players[i].duelscore = READINT16(save->p); players[i].cheatchecknum = READINT32(save->p); players[i].checkpointId = READINT32(save->p); @@ -1194,6 +1198,8 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].numsneakers = READUINT8(save->p); players[i].panelsneakertimer = READUINT16(save->p); players[i].numpanelsneakers = READUINT8(save->p); + players[i].weaksneakertimer = READUINT16(save->p); + players[i].numweaksneakers = READUINT8(save->p); players[i].floorboost = READUINT8(save->p); players[i].growshrinktimer = READINT16(save->p); @@ -2997,6 +3003,7 @@ typedef enum MD3_REAPPEAR = 1<<1, MD3_PUNT_REF = 1<<2, MD3_OWNER = 1<<3, + MD3_RELINK_PLAYER = 1<<4, } mobj_diff3_t; typedef enum @@ -3320,6 +3327,8 @@ static void SaveMobjThinker(savebuffer_t *save, const thinker_t *th, const UINT8 diff3 |= MD3_PUNT_REF; if (mobj->owner) diff3 |= MD3_OWNER; + if (mobj->relinkplayer) + diff3 |= MD3_RELINK_PLAYER; if (diff3 != 0) diff2 |= MD2_MORE; @@ -3610,6 +3619,10 @@ static void SaveMobjThinker(savebuffer_t *save, const thinker_t *th, const UINT8 { WRITEUINT32(save->p, mobj->owner->mobjnum); } + if (diff3 & MD3_RELINK_PLAYER) + { + WRITEUINT8(save->p, mobj->relinkplayer); + } WRITEUINT32(save->p, mobj->mobjnum); } @@ -4923,6 +4936,10 @@ static thinker_t* LoadMobjThinker(savebuffer_t *save, actionf_p1 thinker) { mobj->owner = (mobj_t *)(size_t)READUINT32(save->p); } + if (diff3 & MD3_OWNER) + { + mobj->relinkplayer = READUINT8(save->p); + } // link tid set earlier P_AddThingTID(mobj); @@ -6757,6 +6774,7 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) { WRITEUINT16(save->p, g_voteLevels[i][0]); WRITEUINT16(save->p, g_voteLevels[i][1]); + WRITEUINT8(save->p, g_votes_striked[i]); } for (i = 0; i < VOTE_TOTAL; i++) @@ -6834,6 +6852,7 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) WRITESINT8(save->p, spbplace); WRITEUINT8(save->p, rainbowstartavailable); WRITEUINT8(save->p, inDuel); + WRITEUINT8(save->p, overtimecheckpoints); WRITEUINT32(save->p, introtime); WRITEUINT32(save->p, starttime); @@ -7143,6 +7162,7 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) { g_voteLevels[i][0] = READUINT16(save->p); g_voteLevels[i][1] = READUINT16(save->p); + g_votes_striked[i] = (boolean)READUINT8(save->p); } for (i = 0; i < VOTE_TOTAL; i++) @@ -7216,6 +7236,7 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) spbplace = READSINT8(save->p); rainbowstartavailable = (boolean)READUINT8(save->p); inDuel = (boolean)READUINT8(save->p); + overtimecheckpoints = (boolean)READUINT8(save->p); introtime = READUINT32(save->p); starttime = READUINT32(save->p); diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 3ad9d6a16..27e808c7c 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -8388,6 +8388,46 @@ void P_LoadLevelMusic(void) Music_ResetLevelVolume(); } +void P_FreeLevelState(void) +{ + if (numsectors) + { + F_EndTextPrompt(false, true); + K_UnsetDialogue(); + + ACS_InvalidateMapScope(); + LUA_InvalidateLevel(); + + Obj_ClearCheckpoints(); + + sector_t *ss; + for (ss = sectors; sectors+numsectors != ss; ss++) + { + Z_Free(ss->attached); + Z_Free(ss->attachedsolid); + } + + // This is the simplest guard against double frees. + // No valid map has zero sectors. Or, come to think + // of it, less than two in general! ~toast 310525 + numsectors = 0; + } + + // Clear pointers that would be left dangling by the purge + R_FlushTranslationColormapCache(); + +#ifdef HWRENDER + // Free GPU textures before freeing patches. + if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED)) + HWR_ClearAllTextures(); +#endif + + G_FreeGhosts(); // ghosts are allocated with PU_LEVEL + Patch_FreeTag(PU_PATCH_LOWPRIORITY); + Patch_FreeTag(PU_PATCH_ROTATED); + Z_FreeTags(PU_LEVEL, PU_PURGELEVEL - 1); +} + /** Loads a level from a lump or external wad. * * \param fromnetsave If true, skip some stuff because we're loading a netgame snapshot. @@ -8401,7 +8441,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) // 99% of the things already did, so. // Map header should always be in place at this point INT32 i, ranspecialwipe = 0; - sector_t *ss; virtlump_t *encoreLump = NULL; levelloading = true; @@ -8474,7 +8513,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) wipegamestate = gamestate; // Don't fade if reloading the gamestate // Encore mode fade to pink to white // This is handled BEFORE sounds are stopped. - else if (encoremode && !prevencoremode && modeattacking == ATTACKING_NONE && !demo.rewinding) + else if (encoremode && !prevencoremode && !demo.simplerewind) { if (rendermode != render_none) { @@ -8545,7 +8584,14 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) // Let's fade to white here // But only if we didn't do the encore startup wipe - if (!demo.rewinding && !reloadinggamestate) + if (demo.attract || demo.simplerewind) + { + // Leave the music alone! We're already playing what we want! + // Pull from RNG even though music will never change + // To silence playback has desynced warning + P_Random(PR_MUSICSELECT); + } + else if (!reloadinggamestate) { int wipetype = wipe_level_toblack; @@ -8558,15 +8604,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) FixedDiv((F_GetWipeLength(wipedefs[wipe_level_toblack])-2)*NEWTICRATERATIO, NEWTICRATE), MUSICRATE)); #endif - if (demo.attract) - { - ; // Leave the music alone! We're already playing what we want! - - // Pull from RNG even though music will never change - // To silence playback has desynced warning - P_Random(PR_MUSICSELECT); - } - else if (K_PodiumSequence()) + if (K_PodiumSequence()) { // mapmusrng is set by local player position in K_ResetCeremony P_LoadLevelMusic(); @@ -8643,44 +8681,42 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) } F_RunWipe(wipetype, wipedefs[wipetype], false, ((levelfadecol == 0) ? "FADEMAP1" : "FADEMAP0"), false, false); + + // Hold respawn to keep waiting until you're ready + if (G_IsModeAttackRetrying() && !demo.playback) + { + nowtime = lastwipetic; + while (G_PlayerInputDown(0, gc_respawn, splitscreen + 1) == true) + { + while (!((nowtime = I_GetTime()) - lastwipetic)) + { + I_Sleep(cv_sleep.value); + I_UpdateTime(); + } \ + + I_OsPolling(); + G_ResetAllDeviceResponding(); + + for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1)) + { + HandleGamepadDeviceEvents(&events[eventtail]); + G_MapEventsToControls(&events[eventtail]); + } + + lastwipetic = nowtime; + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); + else if (moviemode && rendermode == render_soft) + I_CaptureVideoFrame(); + NetKeepAlive(); + } + + //wipestyleflags |= (WSF_FADEOUT|WSF_TOWHITE); + } } } - /* - if (!titlemapinaction) - wipegamestate = GS_LEVEL; - */ - - // Close text prompt before freeing the old level - F_EndTextPrompt(false, true); - - K_UnsetDialogue(); - - ACS_InvalidateMapScope(); - - LUA_InvalidateLevel(); - - Obj_ClearCheckpoints(); - - for (ss = sectors; sectors+numsectors != ss; ss++) - { - Z_Free(ss->attached); - Z_Free(ss->attachedsolid); - } - - // Clear pointers that would be left dangling by the purge - R_FlushTranslationColormapCache(); - -#ifdef HWRENDER - // Free GPU textures before freeing patches. - if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED)) - HWR_ClearAllTextures(); -#endif - - G_FreeGhosts(); // ghosts are allocated with PU_LEVEL - Patch_FreeTag(PU_PATCH_LOWPRIORITY); - Patch_FreeTag(PU_PATCH_ROTATED); - Z_FreeTags(PU_LEVEL, PU_PURGELEVEL - 1); + P_FreeLevelState(); R_InitializeLevelInterpolators(); diff --git a/src/p_setup.h b/src/p_setup.h index b16db6bb5..0430cdfb5 100644 --- a/src/p_setup.h +++ b/src/p_setup.h @@ -107,6 +107,7 @@ extern mapthing_t *mapthings; void P_SetupLevelSky(const char *skytexname, boolean global); void P_RespawnThings(void); +void P_FreeLevelState(void); void P_ResetLevelMusic(void); boolean P_UseContinuousLevelMusic(void); void P_LoadLevelMusic(void); diff --git a/src/p_spec.c b/src/p_spec.c index 5c80fc35f..eb30cc265 100644 --- a/src/p_spec.c +++ b/src/p_spec.c @@ -53,6 +53,7 @@ #include "m_easing.h" #include "music.h" #include "k_battle.h" // battleprisons +#include "k_endcam.h" // K_EndCameraIsFreezing() // Not sure if this is necessary, but it was in w_wad.c, so I'm putting it here too -Shadow Hog #include @@ -1995,7 +1996,7 @@ static void K_HandleLapIncrement(player_t *player) } // finished race exit setup - if (player->laps > numlaps) + if (player->laps > numlaps && !K_InRaceDuel()) { pflags_t applyflags = 0; if (specialstageinfo.valid == true) @@ -2021,7 +2022,8 @@ static void K_HandleLapIncrement(player_t *player) : skins[player->skin].flags; if (skinflags & SF_IRONMAN) { - SetRandomFakePlayerSkin(player, true, false); + if ((player->laps == 1 && lapisfresh) || !K_InRaceDuel()) // We'll do this in K_CheckpointCrossAward if necessary. + SetRandomFakePlayerSkin(player, true, false); } // Always trust waypoints entering the first lap. @@ -2064,24 +2066,32 @@ static void K_HandleLapIncrement(player_t *player) if (rainbowstartavailable == true && player->mo->hitlag == 0) { - S_StartSound(player->mo, sfx_s23c); - player->startboost = 125; - - K_SpawnDriftBoostExplosion(player, 4); - K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false); - K_SpawnAmps(player, 35, player->mo); - - if (g_teamplay) + if (K_InRaceDuel()) { - for (UINT8 j = 0; i < MAXPLAYERS; i++) + K_SpawnDriftElectricSparks(player, player->skincolor, false); + K_SpawnAmps(player, 20, player->mo); + } + else + { + S_StartSound(player->mo, sfx_s23c); + player->startboost = 125; + + K_SpawnDriftBoostExplosion(player, 4); + K_SpawnDriftElectricSparks(player, SKINCOLOR_SILVER, false); + K_SpawnAmps(player, (K_InRaceDuel()) ? 20 : 35, player->mo); + + if (g_teamplay) { - if (!playeringame[j] || players[j].spectator || !players[j].mo || P_MobjWasRemoved(players[j].mo)) - continue; - if (!G_SameTeam(player, &players[j])) - continue; - if (player == &players[j]) - continue; - K_SpawnAmps(&players[j], 10, player->mo); + for (UINT8 j = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[j] || players[j].spectator || !players[j].mo || P_MobjWasRemoved(players[j].mo)) + continue; + if (!G_SameTeam(player, &players[j])) + continue; + if (player == &players[j]) + continue; + K_SpawnAmps(&players[j], 10, player->mo); + } } } @@ -2105,7 +2115,9 @@ static void K_HandleLapIncrement(player_t *player) } else if (P_IsDisplayPlayer(player)) { - if (numlaps > 1 && player->laps == numlaps) // final lap + if (K_InRaceDuel()) + S_StartSound(NULL, sfx_s221); + else if (numlaps > 1 && player->laps == numlaps) // final lap S_StartSound(NULL, sfx_s3k68); else if ((player->laps > 1) && (player->laps < numlaps)) // non-final lap S_StartSound(NULL, sfx_s221); @@ -2118,7 +2130,7 @@ static void K_HandleLapIncrement(player_t *player) } else { - if ((player->laps > numlaps) && (player->position == 1)) + if ((player->laps > numlaps) && (player->position == 1) && (!K_InRaceDuel())) { // opponent finished S_StartSound(NULL, sfx_s253); @@ -3263,8 +3275,13 @@ boolean P_ProcessSpecial(activator_t *activator, INT16 special, INT32 *args, cha return false; } - while ((targetThing = P_FindMobjFromTID(args[1], targetThing, mo)) != NULL) + mobj_t *next = P_FindMobjFromTID(args[1], targetThing, mo); + + while ((targetThing = next) != NULL) { + // First in case of deletion. (Can't check for state == S_NULL because of A_ calls, etc) + next = P_FindMobjFromTID(args[1], targetThing, mo); + if (targetThing->player != NULL) { continue; @@ -4752,7 +4769,7 @@ void P_SetupSignExit(player_t *player, boolean tie) return; // SRB2Kart: FINALLY, add in an alternative if no place is found - if (player->mo && !P_MobjWasRemoved(player->mo)) + if (player->mo && !P_MobjWasRemoved(player->mo) && !K_EndCameraIsFreezing()) { thing = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->floorz, MT_SIGN); thing->angle = bestAngle; diff --git a/src/p_tick.c b/src/p_tick.c index 29cf6d62c..c2981d0e8 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -599,9 +599,9 @@ static inline void P_DeviceRumbleTick(void) { low = high = 65536 / 2; } - else if (player->sneakertimer > (sneakertime-(TICRATE/2)) || player->panelsneakertimer > (sneakertime-(TICRATE/2))) + else if (player->sneakertimer > (sneakertime-(TICRATE/2)) || player->panelsneakertimer > (sneakertime-(TICRATE/2)) || player->weaksneakertimer > (sneakertime-(TICRATE/2))) { - low = high = 65536 / (3+player->numsneakers+player->numpanelsneakers); + low = high = 65536 / (3+player->numsneakers+player->numpanelsneakers+player->numweaksneakers); } else if (((player->boostpower < FRACUNIT) || (player->stairjank > 8)) && P_IsObjectOnGround(player->mo) && player->speed != 0) @@ -739,15 +739,7 @@ void P_Ticker(boolean run) // Check for pause or menu up in single player if (paused || P_AutoPause()) { - if (demo.rewinding && leveltime > 0) - { - leveltime = (leveltime-1) & ~3; - if (timeinmap > 0) - timeinmap = (timeinmap-1) & ~3; - G_PreviewRewind(leveltime); - } - else - P_RunChaseCameras(); // special case: allow freecam to MOVE during pause! + P_RunChaseCameras(); // special case: allow freecam to MOVE during pause! return; } @@ -762,17 +754,23 @@ void P_Ticker(boolean run) if (demo.recording) { - G_WriteDemoExtraData(); - for (i = 0; i < MAXPLAYERS; i++) - if (playeringame[i]) - G_WriteDemoTiccmd(&players[i].cmd, i); + if (!G_ConsiderEndingDemoWrite()) + { + G_WriteDemoExtraData(); + for (i = 0; i < MAXPLAYERS; i++) + if (playeringame[i]) + G_WriteDemoTiccmd(&players[i].cmd, i); + } } if (demo.playback && !demo.waitingfortally) { - G_ReadDemoExtraData(); - for (i = 0; i < MAXPLAYERS; i++) - if (playeringame[i]) - G_ReadDemoTiccmd(&players[i].cmd, i); + if (!G_ConsiderEndingDemoRead()) + { + G_ReadDemoExtraData(); + for (i = 0; i < MAXPLAYERS; i++) + if (playeringame[i]) + G_ReadDemoTiccmd(&players[i].cmd, i); + } } LUA_ResetTicTimers(); @@ -1078,7 +1076,7 @@ void P_Ticker(boolean run) } } - if (g_fast_forward == 0) + if (g_fast_forward == 0 || demo.simplerewind) { timeinmap++; } @@ -1168,14 +1166,21 @@ void P_Ticker(boolean run) if (demo.recording) { - G_WriteAllGhostTics(); - if (cv_recordmultiplayerdemos.value && demo.savebutton && demo.savebutton + 3*TICRATE < leveltime) G_CheckDemoTitleEntry(); + + if (!G_ConsiderEndingDemoWrite()) + { + G_WriteAllGhostTics(); + } } - else if (demo.playback && !demo.waitingfortally) // Use Ghost data for consistency checks. + else if (demo.playback && !demo.waitingfortally) { - G_ConsAllGhostTics(); + if (!G_ConsiderEndingDemoRead()) + { + // Use Ghost data for consistency checks. + G_ConsAllGhostTics(); + } } if (modeattacking) @@ -1238,9 +1243,6 @@ void P_Ticker(boolean run) P_MapEnd(); - if (demo.playback) - G_StoreRewindInfo(); - for (i = 0; i < MAXPLAYERS; i++) { G_CopyTiccmd(&players[i].oldcmd, &players[i].cmd, 1); diff --git a/src/p_user.c b/src/p_user.c index c487f8ea3..2bd2192ac 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -1389,7 +1389,7 @@ void P_DoPlayerExit(player_t *player, pflags_t flags) void P_DoAllPlayersExit(pflags_t flags, boolean trygivelife) { UINT8 i; - const boolean dofinishsound = (musiccountdown == 0); + const boolean dofinishsound = (musiccountdown == 0) && (!K_InRaceDuel()); if (grandprixinfo.gp == false || grandprixinfo.eventmode == GPEVENT_SPECIAL @@ -2348,7 +2348,7 @@ static void P_UpdatePlayerAngle(player_t *player) // This is the hardest case for the turn solver, because your handling properties on // client side are very different than your handling properties on server side—at least, // until your drift status makes the full round-trip and is reflected in your gamestate. - if (player->drift && abs(player->drift) < 5) + if (player->drift && abs(player->drift) < 5 && player->cmd.latency) { steeringRight = KART_FULLTURN; steeringLeft = -KART_FULLTURN; @@ -2621,7 +2621,7 @@ void P_MovePlayer(player_t *player) //////////////////////////// // SRB2kart - Drifting smoke and fire - if ((player->sneakertimer || player->panelsneakertimer || player->flamedash) + if ((player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || player->flamedash) && onground && (leveltime & 1)) K_SpawnBoostTrail(player); @@ -3766,7 +3766,6 @@ boolean P_SpectatorJoinGame(player_t *player) } player->spectator = false; player->pflags &= ~PF_WANTSTOJOIN; - player->spectatewait = 0; player->team = TEAM_UNASSIGNED; // We will auto-assign later. player->playerstate = PST_REBORN; player->enteredGame = true; @@ -3781,6 +3780,10 @@ boolean P_SpectatorJoinGame(player_t *player) // a surprise tool that will help us later... text = va("\x82*%s entered the game.", player_names[player-players]); + if (P_IsMachineLocalPlayer(player) && player->spectatewait > TICRATE) + S_StartSound(NULL, sfx_s3ka9); + player->spectatewait = 0; + HU_AddChatText(text, false); return true; // no more player->mo, cannot continue. } @@ -4251,6 +4254,12 @@ void P_PlayerThink(player_t *player) player->airtime++; } + if ((player->pflags & PF_FAULT) || (player->pflags & PF_VOID)) + { + player->lastairtime = 0; + player->airtime = 0; + } + cmd = &player->cmd; // SRB2kart diff --git a/src/r_things.cpp b/src/r_things.cpp index 6fca80e5f..a1e59cf88 100644 --- a/src/r_things.cpp +++ b/src/r_things.cpp @@ -3797,6 +3797,12 @@ boolean R_ThingVisible (mobj_t *thing) case MT_BATTLECAPSULE_PIECE: case MT_SPRAYCAN: case MT_PLAYER: + case MT_LANDMINE: + case MT_SSMINE: + case MT_SSMINE_SHIELD: + case MT_CHECKPOINT_END: + case MT_SIGNSPARKLE: + case MT_THOK: // checkpoint parts return false; default: diff --git a/src/sdl/new_sound.cpp b/src/sdl/new_sound.cpp index 3ded71879..1fa9deea2 100644 --- a/src/sdl/new_sound.cpp +++ b/src/sdl/new_sound.cpp @@ -198,7 +198,6 @@ static void (*music_fade_callback)(); static SDL_AudioDeviceID g_device_id; static SDL_AudioDeviceID g_input_device_id; -static boolean g_input_device_paused; void* I_GetSfx(sfxinfo_t* sfx) { @@ -999,14 +998,14 @@ void I_UpdateAudioRecorder(void) boolean I_SoundInputIsEnabled(void) { - return g_input_device_id != 0 && !g_input_device_paused; + return g_input_device_id != 0; } boolean I_SoundInputSetEnabled(boolean enabled) { if (g_input_device_id == 0 && enabled) { - if (SDL_GetNumAudioDevices(true) == 0) + if (!sound_started || SDL_GetNumAudioDevices(true) == 0) { return false; } @@ -1023,21 +1022,17 @@ boolean I_SoundInputSetEnabled(boolean enabled) CONS_Alert(CONS_WARNING, "Failed to open input audio device: %s\n", SDL_GetError()); return false; } - g_input_device_paused = true; - } - - if (enabled && g_input_device_paused) - { SDL_PauseAudioDevice(g_input_device_id, SDL_FALSE); - g_input_device_paused = false; } - else if (!enabled && !g_input_device_paused) + else if (g_input_device_id != 0 && !enabled) { SDL_PauseAudioDevice(g_input_device_id, SDL_TRUE); SDL_ClearQueuedAudio(g_input_device_id); - g_input_device_paused = true; + SDL_CloseAudioDevice(g_input_device_id); + g_input_device_id = 0; } - return !g_input_device_paused; + + return enabled; } UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len) diff --git a/src/st_stuff.c b/src/st_stuff.c index fef6481dc..44703e839 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -737,6 +737,8 @@ void ST_startTitleCard(void) lt_ticker = lt_exitticker = lt_lasttic = 0; lt_endtime = 4*TICRATE; // + (10*NEWTICRATERATIO); lt_fade = 0; + + WipeStageTitle = false; } // @@ -788,7 +790,7 @@ patch_t *ST_getRoundPicture(boolean small) // void ST_runTitleCard(void) { - boolean run = !(paused || P_AutoPause() || g_fast_forward > 0); + boolean run = !(paused || P_AutoPause() || (g_fast_forward > 0 && demo.simplerewind == DEMO_REWIND_OFF)); INT32 auxticker; boolean doroundicon = (ST_getRoundPicture(false) != NULL); diff --git a/src/typedef.h b/src/typedef.h index 03ecea75d..3dfa9b8c0 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -76,7 +76,6 @@ TYPEDEF (plrconfig); TYPEDEF (filesneededconfig_pak); TYPEDEF (doomdata_t); TYPEDEF (serverelem_t); -TYPEDEF (rewind_t); TYPEDEF (clientkey_pak); TYPEDEF (serverchallenge_pak); TYPEDEF (challengeall_pak); diff --git a/src/y_inter.cpp b/src/y_inter.cpp index b0c0f64c1..d2c509424 100644 --- a/src/y_inter.cpp +++ b/src/y_inter.cpp @@ -951,6 +951,18 @@ void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset) i--; } while (true); + + if (standings->rankingsmode) + { + if (standings->isduel) + { + Y_DrawRankMode(BASEVIDWIDTH / 2 + xoffset, BASEVIDHEIGHT - 19, true); + } + else + { + Y_DrawRankMode(x + 122, returny - yspacing + 7, false); + } + } } // @@ -959,11 +971,15 @@ void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset) // Handles drawing the bottom-of-screen progression. // Currently requires intermission y_data for animation only. // -void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, boolean widescreen) +void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, boolean widescreen, boolean adminmode) { if (roundqueue.size == 0) { - return; + if (!adminmode + || menuqueue.size == 0) + { + return; + } } // The following is functionally a hack. @@ -1029,6 +1045,10 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, prize_dot[BPP_AHEAD] = static_cast(W_CachePatchName("R_RRMRK4", PU_PATCH)); prize_dot[BPP_DONE] = static_cast(W_CachePatchName("R_RRMRK6", PU_PATCH)); + patch_t *rpmark[2]; + rpmark[0] = static_cast(W_CachePatchName("R_RPMARK", PU_PATCH)); + rpmark[1] = static_cast(W_CachePatchName("R_R2MARK", PU_PATCH)); + UINT8 *colormap = NULL, *oppositemap = NULL; fixed_t playerx = 0, playery = 0; UINT8 pskin = MAXSKINS; @@ -1067,10 +1087,37 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, upwa = true; } - workingqueuesize--; + if (!adminmode) + { + workingqueuesize--; + } } - INT32 widthofroundqueue = 24*(workingqueuesize - 1); + INT32 widthofroundqueue, totalsteps; + + INT32 menusendoffset = 0; + if (menuqueue.sending) + { + if (menuqueue.sending > menuqueue.size) + { + menusendoffset = menuqueue.size; + } + else + { + menusendoffset = menuqueue.sending-1; + } + } + + if (adminmode) + { + totalsteps = std::min(workingqueuesize + (menuqueue.size - menusendoffset), ROUNDQUEUE_MAX); + } + else + { + totalsteps = workingqueuesize; + } + + widthofroundqueue = (totalsteps - 1) * 24; INT32 x = (BASEVIDWIDTH - widthofroundqueue) / 2; INT32 y, basey = 167 + offset; @@ -1079,7 +1126,7 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, // The following block handles horizontal easing of the // progression bar on the last non-rankrestricted round. - if (standings->showrank == true) + if (!adminmode && standings->showrank == true) { fixed_t percentslide = 0; SINT8 deferxoffs = 0; @@ -1139,12 +1186,22 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, V_DrawMappedPatch(xiter, basey, baseflags, queuebg_upwa, greymap); } + // Draw to left side of screen while (xiter > -bufferspace) { xiter -= 24; V_DrawMappedPatch(xiter, basey, baseflags, queuebg_flat, greymap); } + // Draw to right side of screen + xiter = x + widthofroundqueue; + while (xiter < BASEVIDWIDTH + bufferspace) + { + xiter += 24; + V_DrawMappedPatch(xiter, basey, baseflags, queuebg_flat, greymap); + } + + // Actually queued maps for (i = 0; i < workingqueuesize; i++) { // Draw the background, and grab the appropriate line, to the right of the dot @@ -1174,7 +1231,13 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, } else { - V_DrawMappedPatch(x, basey, baseflags, queuebg_flat, greymap); + V_DrawMappedPatch( + x, + basey, + baseflags, + ((workingqueuesize == totalsteps) ? queuebg_flat : queuebg_upwa), + greymap + ); } } @@ -1309,17 +1372,9 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, } else { - // No more line! Fill in background to right edge of screen - xiter = x; - while (xiter < BASEVIDWIDTH + bufferspace) - { - xiter += 24; - V_DrawMappedPatch(xiter, basey, baseflags, queuebg_flat, greymap); - } - // Handle special entry on the end // (has to be drawn before the semifinal dot due to overlap) - if (standings->showrank == true) + if (!adminmode && standings->showrank == true) { const fixed_t x2 = x + spacetospecial; @@ -1330,7 +1385,7 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, } else if ( doanimations == true - && roundqueue.position == roundqueue.size-1 + && roundqueue.position == workingqueuesize && timer - interpoffs <= 2*TICRATE ) { @@ -1510,13 +1565,62 @@ void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, x += 24; } + totalsteps -= i; + + // Maps in the progress of being queued on the menu + if (adminmode && totalsteps) + { + for (i = menusendoffset; i < (totalsteps + menusendoffset); i++) + { + upwa ^= true; + if (upwa == false) + { + y = basey + 4; + + V_DrawMappedPatch(x, basey, baseflags, queuebg_down, greymap); + } + else + { + y = basey + 12; + + if (i+1 != menuqueue.size) // no more line? + { + V_DrawMappedPatch(x, basey, baseflags, queuebg_upwa, greymap); + } + else + { + V_DrawMappedPatch(x, basey, baseflags, queuebg_flat, greymap); + } + } + + V_DrawMappedPatch( + x - 8, y, + baseflags, + level_dot[BPP_AHEAD], + NULL + ); + + V_DrawMappedPatch( + x - 10, y - 14, + baseflags, + rpmark[0], + NULL + ); + + K_DrawMapAsFace( + x - 9, y - 13, + (baseflags|((menuqueue.entries[i].encore) ? V_FLIP : 0)), + menuqueue.entries[i].mapnum, + NULL + ); + + x += 24; + } + } + // Draw the player position through the round queue! if (playery != 0) { - patch_t *rpmark[2]; - rpmark[0] = static_cast(W_CachePatchName("R_RPMARK", PU_PATCH)); - rpmark[1] = static_cast(W_CachePatchName("R_R2MARK", PU_PATCH)); - // Change alignment playerx -= (10 * FRACUNIT); playery -= (14 * FRACUNIT); @@ -1627,6 +1731,88 @@ void Y_DrawIntermissionButton(INT32 startslide, INT32 through, boolean widescree } } +// +// Y_DrawRankMode +// +// Draws EXP or MOBIUMS label depending on context. +// x and y designate the coordinates of the most bottom-right pixel to draw from (because it is the left extent and patch heights that vary), +// or the bottom-center if center is true. +// +void Y_DrawRankMode(INT32 x, INT32 y, boolean center) +{ + boolean useMobiums = (powertype != PWRLV_DISABLED); + INT32 textWidth, middleLeftEdge, middleRightEdge, middleWidth; + + char text[8]; + char iconPatchName[8]; + UINT8 iconWidth; // the graphic paddings are inconsistent... + UINT8 *iconColormap; + UINT8 *stickerColormap; + + patch_t *iconPatch; + patch_t *stickerTail = static_cast(W_CachePatchName("INT_STK1", PU_CACHE)); + patch_t *stickerMiddle = static_cast(W_CachePatchName("INT_STK2", PU_CACHE)); + patch_t *stickerHead = center ? stickerTail : static_cast(W_CachePatchName("INT_STK3", PU_CACHE)); + UINT32 stickerHeadFlags = 0; + UINT8 stickerHeadOffset = 0; + + if (useMobiums) + { + snprintf(text, sizeof text, "MOBIUMS"); + snprintf(iconPatchName, sizeof iconPatchName, "K_STMOB"); + iconWidth = 22; + iconColormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_NONE), GTC_CACHE); + stickerColormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_TEA), GTC_CACHE); + } + else + { + snprintf(text, sizeof text, "EXP"); + snprintf(iconPatchName, sizeof iconPatchName, "K_STEXP"); + iconWidth = 16; + iconColormap = R_GetTranslationColormap(TC_RAINBOW, static_cast(SKINCOLOR_MUSTARD), GTC_CACHE); + stickerColormap = R_GetTranslationColormap(TC_DEFAULT, static_cast(SKINCOLOR_MUSTARD), GTC_CACHE); + } + + iconPatch = static_cast(W_CachePatchName(iconPatchName, PU_CACHE)); + textWidth = (INT32)V_ThinStringWidth(text, 0); + middleLeftEdge = x - iconWidth - textWidth - 8; + middleRightEdge = x - stickerHead->width; + middleWidth = middleRightEdge - middleLeftEdge; + + if (center) + { + // flip the right-hand sticker tail and keep it left-aligned + stickerHeadFlags |= V_FLIP; + stickerHeadOffset += stickerHead->width; + + // sliiightly extend the right side of the sticker + middleWidth += 2; + middleRightEdge += 2; + + // shift all components to the right so that our x coordinates are center-aligned + #define CENTER_SHIFT (stickerHead->width + middleWidth / 2) + x += CENTER_SHIFT; + middleLeftEdge += CENTER_SHIFT; + middleRightEdge += CENTER_SHIFT; + #undef CENTER_SHIFT + } + + // draw sticker + V_DrawMappedPatch(middleRightEdge + stickerHeadOffset, y - stickerHead->height, stickerHeadFlags, stickerHead, stickerColormap); + V_DrawStretchyFixedPatch( + middleLeftEdge << FRACBITS, + (y - stickerMiddle->height) << FRACBITS, + (middleWidth << FRACBITS) / stickerMiddle->width + 1, + FRACUNIT, + 0, stickerMiddle, stickerColormap + ); + V_DrawMappedPatch(middleLeftEdge - stickerTail->width, y - stickerTail->height, 0, stickerTail, stickerColormap); + + // draw icon and text + V_DrawMappedPatch(x - iconPatch->width - 6, y - iconPatch->height + 4, 0, iconPatch, iconColormap); + V_DrawThinString(middleLeftEdge - 1, y - 9, 0, text); +} + void Y_DrawIntermissionHeader(fixed_t x, fixed_t y, boolean gotthrough, const char *headerstring, boolean showroundnum, boolean small) { const INT32 v_width = (small ? BASEVIDWIDTH/2 : BASEVIDWIDTH); @@ -1840,7 +2026,7 @@ skiptallydrawer: goto finalcounter; // Returns early if there's no roundqueue entries to draw - Y_RoundQueueDrawer(&data, 0, true, false); + Y_RoundQueueDrawer(&data, 0, true, false, false); if (netgame) { @@ -2081,7 +2267,9 @@ void Y_Ticker(void) // Basic bitch points if (data.increase[data.num[q]]) { - if (--data.increase[data.num[q]]) + data.increase[data.num[q]] = std::max(data.increase[data.num[q]] - 3, 0); + + if (data.increase[data.num[q]] != 0) kaching = false; } } @@ -2117,6 +2305,35 @@ boolean Y_ShouldDoIntermission(void) return true; } +// +// Y_GetIntermissionType +// +// Returns the intermission type from the current gametype. +// +intertype_t Y_GetIntermissionType(void) +{ + intertype_t ret = static_cast(gametypes[gametype]->intermission); + + if (ret == int_scoreortimeattack) + { + UINT8 i = 0, nump = 0; + + for (i = 0; i < MAXPLAYERS; i++) + { + if (!playeringame[i] || players[i].spectator) + { + continue; + } + + nump++; + } + + ret = (nump < 2 ? int_time : int_score); + } + + return ret; +} + // // Y_DetermineIntermissionType // @@ -2131,21 +2348,7 @@ void Y_DetermineIntermissionType(void) return; } - // set initially - intertype = static_cast(gametypes[gametype]->intermission); - - // special cases - if (intertype == int_scoreortimeattack) - { - UINT8 i = 0, nump = 0; - for (i = 0; i < MAXPLAYERS; i++) - { - if (!playeringame[i] || players[i].spectator) - continue; - nump++; - } - intertype = (nump < 2 ? int_time : int_score); - } + intertype = Y_GetIntermissionType(); } static UINT8 Y_PlayersBestPossiblePosition(player_t *const player) @@ -2433,9 +2636,10 @@ void Y_StartIntermission(void) } K_CashInPowerLevels(); - SV_BumpMatchStats(); } + SV_BumpMatchStats(); + if (!timer) { Y_EndIntermission(); diff --git a/src/y_inter.h b/src/y_inter.h index 572b4a6d1..b98c130d3 100644 --- a/src/y_inter.h +++ b/src/y_inter.h @@ -54,16 +54,14 @@ void Y_Ticker(void); // Specific sub-drawers void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset); -void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, boolean widescreen); +void Y_RoundQueueDrawer(y_data_t *standings, INT32 offset, boolean doanimations, boolean widescreen, boolean adminmode); void Y_DrawIntermissionButton(INT32 startslide, INT32 through, boolean widescreen); +void Y_DrawRankMode(INT32 x, INT32 y, boolean center); void Y_StartIntermission(void); void Y_MidIntermission(void); void Y_EndIntermission(void); -boolean Y_ShouldDoIntermission(void); -void Y_DetermineIntermissionType(void); - void Y_PlayIntermissionMusic(void); boolean Y_IntermissionPlayerLock(void); @@ -78,6 +76,10 @@ typedef enum extern intertype_t intertype; +boolean Y_ShouldDoIntermission(void); +intertype_t Y_GetIntermissionType(void); +void Y_DetermineIntermissionType(void); + #ifdef __cplusplus } // extern "C" #endif