From b33597e225f82b5df3cd96b4ca63e36739862295 Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 24 Sep 2023 15:43:48 +0100 Subject: [PATCH 01/98] Beginnings of system for using Chao Keys on large tiles - M_UpdateChallengeGridExtraData: Register whether major unlock tiles have had every surrounding Challenge cleared (CHE_ALLCLEAR) - M_DrawChallengeTile: For locked tiles with CHE_ALLCLEAR, show a little dot (temporary drawer) --- src/k_menudraw.c | 6 ++++ src/m_cond.c | 74 ++++++++++++++++++++++++++++++++++++++---------- src/m_cond.h | 1 + 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 0083ae643..2f1a10d91 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5224,6 +5224,12 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili colormap ); + if (challengesmenu.extradata[id].flags & CHE_ALLCLEAR) + { + // Temporary drawer for "key should be usable" + V_DrawFill(x + 5, y + 5, 2, 2, 255); + } + pat = missingpat; colormap = NULL; diff --git a/src/m_cond.c b/src/m_cond.c index eecc73ae6..51da95621 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -417,7 +417,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { id = (i * CHALLENGEGRIDHEIGHT) + j; - extradata[id].flags = CHE_NONE; + num = gamedata->challengegrid[id]; + if (num >= MAXUNLOCKABLES || unlockables[num].majorunlock == false) + { + extradata[id].flags = CHE_NONE; + continue; + } + + // We only do this for large tiles, to reduce the complexity + // of most standard tile challenge comparisons + extradata[id].flags = CHE_ALLCLEAR; } } @@ -468,12 +477,22 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) if (extradata[id].flags == CHE_HINT) { + // CHE_ALLCLEAR has already been removed, + // and CHE_HINT has already been applied, + // so nothing more needs to be done here. continue; } } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } @@ -495,9 +514,13 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); // If we haven't already updated our id, it's the one to our left. - if (extradata[id].flags == CHE_HINT) + if (extradata[id].flags & CHE_HINT) { - extradata[tempid].flags = CHE_HINT; + extradata[tempid].flags |= CHE_HINT; + } + if (!(extradata[id].flags & CHE_ALLCLEAR)) + { + extradata[tempid].flags &= ~CHE_ALLCLEAR; } extradata[id].flags = CHE_CONNECTEDLEFT; id = tempid; @@ -505,16 +528,25 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) /*else CONS_Printf(" %d - %d to left of %d is invalid\n", work, tempid, id);*/ } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } // Since we're not modifying id past this point, the conditions become much simpler. - if ((extradata[id].flags & (CHE_HINT|CHE_DONTDRAW)) == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { + // CHE_ALLCLEAR has already been removed, + // and CHE_HINT has already been applied, + // so nothing more needs to be done here. continue; } @@ -528,10 +560,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { ; } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } @@ -551,10 +589,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { ; } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } } diff --git a/src/m_cond.h b/src/m_cond.h index b330b942e..00960abbb 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -347,6 +347,7 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata); #define CHE_CONNECTEDLEFT (1<<1) #define CHE_CONNECTEDUP (1<<2) #define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP) +#define CHE_ALLCLEAR (1<<3) char *M_BuildConditionSetString(UINT16 unlockid); #define DESCRIPTIONWIDTH 170 From 33dfb697bf0823307d382f9d35205bf5745a7c68 Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 24 Sep 2023 15:47:31 +0100 Subject: [PATCH 02/98] M_UpdateChallengeGridExtraData: Slight optimisation in initial setup --- src/m_cond.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 51da95621..90faf6298 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -418,14 +418,14 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; - if (num >= MAXUNLOCKABLES || unlockables[num].majorunlock == false) + if (num >= MAXUNLOCKABLES || unlockables[num].majorunlock == false || gamedata->unlocked[num] == true) { extradata[id].flags = CHE_NONE; continue; } - // We only do this for large tiles, to reduce the complexity - // of most standard tile challenge comparisons + // We only do this for locked large tiles, to reduce the + // complexity of most standard tile challenge comparisons extradata[id].flags = CHE_ALLCLEAR; } } From 1ce41bdfb4eb20390e9ea6f29172590e87aa0b3d Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 24 Sep 2023 17:17:05 +0100 Subject: [PATCH 03/98] Large Challenge Tiles can now be Key'd. - Must have every challenge surrounding it unlocked. (CHE_ALLCLEAR, see previous commits) - Takes 10 Chao Keys. - Has a swag animation. In addition: - Replace the DEVELOP-only Z button behaviour with "add a free key", since large tiles are no longer permalocked, only expensive. --- src/k_menudraw.c | 87 ++++++++++++++++++++++++----------- src/menus/extras-challenges.c | 37 ++++++++------- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 2f1a10d91..b4ee6f974 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6080,46 +6080,81 @@ challengedesc: if (offs < challengekeybarwidth) V_DrawFadeFill(1+offs, 25, challengekeybarwidth-offs, 2, 0, 31, challengetransparentstrength); - if (challengesmenu.chaokeyhold) + if (challengesmenu.currentunlock < MAXUNLOCKABLES && challengesmenu.chaokeyhold) { - fixed_t keyholdrotation = 0, radius = challengesgridstep; + fixed_t tilex = selectx, tiley = selecty; - if (challengesmenu.chaokeyhold < CHAOHOLD_BEGIN) + fixed_t baseradius = challengesgridstep; + + boolean major = false, ending = false; + if (unlockables[challengesmenu.currentunlock].majorunlock == true) { - radius = (challengesmenu.chaokeyhold*radius)*(FRACUNIT/CHAOHOLD_BEGIN); - keyx += challengesmenu.chaokeyhold*((selectx*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; - keyy += challengesmenu.chaokeyhold*((selecty*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; + major = true; + tilex += challengesgridstep/2; + tiley += challengesgridstep/2; + baseradius *= 2; } - else + + if (challengesmenu.chaokeyhold >= CHAOHOLD_MAX - CHAOHOLD_END) { - if (challengesmenu.chaokeyhold < CHAOHOLD_MAX - CHAOHOLD_END) + ending = true; + baseradius = ((CHAOHOLD_MAX - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); + } + + INT16 specifickeyholdtime = challengesmenu.chaokeyhold; + + for (i = 0; i < (major ? 10 : 1); i++, specifickeyholdtime -= 4) + { + fixed_t radius = baseradius; + fixed_t thiskeyx, thiskeyy; + fixed_t keyholdrotation = 0; + + if (specifickeyholdtime < CHAOHOLD_BEGIN) { - radius <<= FRACBITS; + if (specifickeyholdtime < 0) + { + // Nothing following will be relevant + break; + } - keyholdrotation = 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) - * (FRACUNIT/(CHAOHOLD_MAX - (CHAOHOLD_BEGIN + CHAOHOLD_END))); - - INT32 time = 3 - (keyholdrotation - 1) / (90 * FRACUNIT); - if (time <= 5 && time >= 0) - V_DrawScaledPatch(selectx + 2, selecty - 2, 0, kp_eggnum[time]); + radius = (specifickeyholdtime*radius)*(FRACUNIT/CHAOHOLD_BEGIN); + thiskeyx = keyx + specifickeyholdtime*((tilex*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; + thiskeyy = keyy + specifickeyholdtime*((tiley*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; } else { - radius = ((CHAOHOLD_MAX - challengesmenu.chaokeyhold)*radius)*(FRACUNIT/CHAOHOLD_END); + keyholdrotation = (-36 * i) * FRACUNIT; + + if (ending == false) + { + radius <<= FRACBITS; + + keyholdrotation += 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) + * (FRACUNIT/(CHAOHOLD_MAX - (CHAOHOLD_BEGIN + CHAOHOLD_END))); + + if (i == 0) + { + INT32 time = 3 - (keyholdrotation - 1) / (90 * FRACUNIT); + if (time <= 5 && time >= 0) + V_DrawScaledPatch(tilex + 2, tiley - 2, 0, kp_eggnum[time]); + } + } + + thiskeyx = tilex*FRACUNIT; + thiskeyy = tiley*FRACUNIT; } - keyx = selectx*FRACUNIT; - keyy = selecty*FRACUNIT; - } + if (radius != 0) + { + angle_t ang = (FixedAngle( + keyholdrotation + ) >> ANGLETOFINESHIFT) & FINEMASK; - if (radius) - { - angle_t ang = (FixedAngle( - keyholdrotation - ) >> ANGLETOFINESHIFT) & FINEMASK; + thiskeyx += FixedMul(radius, FINESINE(ang)); + thiskeyy -= FixedMul(radius, FINECOSINE(ang)); + } - keyx += FixedMul(radius, FINESINE(ang)); - keyy -= FixedMul(radius, FINECOSINE(ang)); + V_DrawFixedPatch(thiskeyx, thiskeyy, FRACUNIT, 0, key, NULL); } } diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 4ca2363e2..df428634e 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -290,10 +290,10 @@ void M_Challenges(INT32 choice) M_SetupNextMenu(&MISC_ChallengesDef, false); } -static boolean M_CanKeyHiliTile(boolean devskip) +static boolean M_CanKeyHiliTile(void) { // No keys to do it with? - if (gamedata->chaokeys == 0 && !devskip) + if (gamedata->chaokeys == 0) return false; // No tile data? @@ -308,18 +308,23 @@ static boolean M_CanKeyHiliTile(boolean devskip) if (gamedata->unlocked[challengesmenu.currentunlock] == true) return false; - // Marked as unskippable? - if (unlockables[challengesmenu.currentunlock].majorunlock == true && !devskip) - return false; - UINT16 i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; // Not a hinted tile OR a fresh board. if (!(challengesmenu.extradata[i].flags & CHE_HINT) - && (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] > 0) - && !devskip) + && (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] > 0)) return false; + // Marked as major? + if (unlockables[challengesmenu.currentunlock].majorunlock == true) + { + if (!(challengesmenu.extradata[i].flags & CHE_ALLCLEAR)) + return false; + + if (gamedata->chaokeys < 10) + return false; + } + // All good! return true; } @@ -370,12 +375,7 @@ void M_ChallengesTick(void) if (challengesmenu.chaokeyhold) { - boolean devskip = false; -#ifdef DEVELOP - devskip = M_MenuButtonHeld(pid, MBT_Z); -#endif - // A little messy, but don't freak out, this is just so devs don't crash the game on non-tiles - if ((devskip || M_MenuExtraHeld(pid)) && M_CanKeyHiliTile(devskip)) + if (M_MenuExtraHeld(pid) && M_CanKeyHiliTile()) { // Not pressed just this frame? if (!M_MenuExtraPressed(pid)) @@ -385,7 +385,8 @@ void M_ChallengesTick(void) if (challengesmenu.chaokeyhold > CHAOHOLD_MAX) { #ifndef CHAOKEYDEBUG - gamedata->chaokeys--; + gamedata->chaokeys -= (unlockables[challengesmenu.currentunlock].majorunlock == true) + ? 10 : 1; #endif challengesmenu.chaokeyhold = 0; challengesmenu.unlockcount[CC_CHAOANIM]++; @@ -616,7 +617,7 @@ boolean M_ChallengesInputs(INT32 ch) } else if (M_MenuExtraPressed(pid)) { - if (M_CanKeyHiliTile(false)) + if (M_CanKeyHiliTile()) { challengesmenu.chaokeyhold = 1; } @@ -642,7 +643,9 @@ boolean M_ChallengesInputs(INT32 ch) #ifdef DEVELOP else if (M_MenuButtonPressed(pid, MBT_Z)) { - challengesmenu.chaokeyhold = 1; + gamedata->chaokeys++; + challengesmenu.unlockcount[CC_CHAOANIM]++; + S_StartSound(NULL, sfx_dbgsal); return true; } #endif From 3e901b312cce5357b1c111a97cbc13dd5dab15f2 Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 24 Sep 2023 22:42:30 +0100 Subject: [PATCH 04/98] Catch all unconditional cv_playercolor[] visuals Resolves #704 Now converts to skin prefcolor in: - Player menu party - Challenges menu - Why it's done on this branch - want to avoid merge conflicts - Gamepad indicator - Actually fixes a bug at the same time for skins with nonstandard startcolors --- src/g_input.c | 15 +++++-- src/k_menu.h | 3 ++ src/k_menudraw.c | 74 ++++++++++++++++++++--------------- src/menus/extras-challenges.c | 9 +---- 4 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/g_input.c b/src/g_input.c index 6e1b3f8d9..e5f7a7c0a 100644 --- a/src/g_input.c +++ b/src/g_input.c @@ -21,6 +21,7 @@ #include "i_joy.h" // JOYAXISRANGE #include "r_draw.h" // GTC_ macros for assigning gamepad indicator colors #include "v_video.h" // V_GetColor for assigning gamepad indictaor colors +#include "r_skins.h" // skins[].prefcolor for assigning gamepad indicator colors #include "z_zone.h" // current state of the keys @@ -206,7 +207,6 @@ void G_SetDeviceForPlayer(INT32 player, INT32 device) void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) { INT32 device; - INT32 skin; UINT16 skincolor; UINT8 *colormap; byteColor_t byte_color; @@ -220,15 +220,24 @@ void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) return; } - skin = cv_skin[player].value; skincolor = cv_playercolor[player].value; - colormap = R_GetTranslationColormap(skin, skincolor, GTC_MENUCACHE); + if (skincolor == SKINCOLOR_NONE) + { + INT32 skin = cv_skin[player].value; + if (skin == -1) + skin = 0; + skincolor = skins[skin].prefcolor; + } + + // We use TC_DEFAULT here rather than player skin because... + colormap = R_GetTranslationColormap(TC_DEFAULT, skincolor, GTC_MENUCACHE); if (colormap == NULL) { return; } + // ...we're grabbing the same index as a reference point across remaps! byte_color = V_GetColor(colormap[104]).s; I_SetGamepadIndicatorColor(device, byte_color.red, byte_color.green, byte_color.blue); diff --git a/src/k_menu.h b/src/k_menu.h index 858749463..2cea14781 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1142,6 +1142,9 @@ void M_HandleImageDef(INT32 choice); #define recommendedflags V_GREENMAP #define warningflags V_GRAYMAP +// For some menu highlights +UINT16 M_GetCvPlayerColor(UINT8 pnum); + void M_UpdateMenuBGImage(boolean forceReset); void M_DrawMenuBackground(void); void M_DrawMenuForeground(void); diff --git a/src/k_menudraw.c b/src/k_menudraw.c index b4ee6f974..2f721c682 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -231,6 +231,22 @@ void M_DrawMenuBackground(void) } } +UINT16 M_GetCvPlayerColor(UINT8 pnum) +{ + if (pnum >= MAXSPLITSCREENPLAYERS) + return SKINCOLOR_NONE; + + UINT16 color = cv_playercolor[pnum].value; + if (color != SKINCOLOR_NONE) + return color; + + INT32 skin = R_SkinAvailable(cv_skin[pnum].string); + if (skin == -1) + return SKINCOLOR_NONE; + + return skins[skin].prefcolor; +} + static void M_DrawMenuParty(void) { const INT32 PLATTER_WIDTH = 19; @@ -253,6 +269,18 @@ static void M_DrawMenuParty(void) x = 2; y = BASEVIDHEIGHT - small->height - 2; + // Despite the work put into it, can't use M_GetCvPlayerColor directly - we need to reference skin always. + #define grab_skin_and_colormap(pnum) \ + { \ + skin = R_SkinAvailable(cv_skin[pnum].string); \ + color = cv_playercolor[pnum].value; \ + if (skin == -1) \ + skin = 0; \ + if (color == SKINCOLOR_NONE) \ + color = skins[skin].prefcolor; \ + colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); \ + } + switch (setup_numplayers) { case 1: @@ -260,9 +288,7 @@ static void M_DrawMenuParty(void) x -= 8; V_DrawScaledPatch(x, y, 0, small); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -273,15 +299,11 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, small); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, small); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -291,21 +313,15 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, large); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, small); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 12, y - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[2].string); - color = cv_playercolor[2].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(2); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -315,27 +331,19 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, large); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, large); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 12, y - PLATTER_STAGGER - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 12, y - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[3].string); - color = cv_playercolor[3].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(3); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[2].string); - color = cv_playercolor[2].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(2); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -346,6 +354,8 @@ static void M_DrawMenuParty(void) } } + #undef grab_skin_and_color + x += PLATTER_WIDTH; y += small->height; V_DrawScaledPatch(x + 16, y - 12, 0, W_CachePatchName(va("OPPRNK0%d", setup_numplayers % 10), PU_CACHE)); @@ -5476,7 +5486,7 @@ drawborder: buffer[7] = (skullAnimCounter/5) ? '2' : '1'; pat = W_CachePatchName(buffer, PU_CACHE); - colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + colormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); V_DrawFixedPatch( x*FRACUNIT, y*FRACUNIT, @@ -5877,7 +5887,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } else { - colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + colormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); V_DrawFixedPatch((x+40)<majorunlock && M_RandomChance(FRACUNIT/2)) ? 1 : 0; From f29430f29c54b411e66cfe9544e488fd553f659e Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 27 Sep 2023 20:54:32 +0100 Subject: [PATCH 05/98] Longer delay for using a Chao Key on a Major Challenge Tile Also tidies up a lot of the code and constants related to the Key usage timing --- src/k_menu.h | 8 +++++--- src/k_menudraw.c | 19 +++++++++++++------ src/menus/extras-challenges.c | 9 ++++++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/k_menu.h b/src/k_menu.h index 2cea14781..8c91a612c 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1214,9 +1214,11 @@ void M_DrawAddons(void); #define TILEFLIP_MAX 16 -#define CHAOHOLD_MAX (3*TICRATE/2) -#define CHAOHOLD_BEGIN 7 -#define CHAOHOLD_END 3 +#define CHAOHOLD_STANDARD (40) // (Close to 3*TICRATE/2 after padding, but adjusted to evenly divide by 10) +#define CHAOHOLD_MAJOR (60) //(3*CHAOHOLD_STANDARD/2) +#define CHAOHOLD_BEGIN (7) +#define CHAOHOLD_END (3) +#define CHAOHOLD_PADDING (CHAOHOLD_BEGIN + CHAOHOLD_END) extern struct timeattackmenu_s { diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 2f721c682..691ccf65e 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6102,18 +6102,25 @@ challengedesc: major = true; tilex += challengesgridstep/2; tiley += challengesgridstep/2; - baseradius *= 2; + baseradius = (7*baseradius)/4; } - if (challengesmenu.chaokeyhold >= CHAOHOLD_MAX - CHAOHOLD_END) + const INT32 chaohold_duration = + CHAOHOLD_PADDING + + (major + ? CHAOHOLD_MAJOR + : CHAOHOLD_STANDARD + ); + + if (challengesmenu.chaokeyhold >= chaohold_duration - CHAOHOLD_END) { ending = true; - baseradius = ((CHAOHOLD_MAX - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); + baseradius = ((chaohold_duration - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); } INT16 specifickeyholdtime = challengesmenu.chaokeyhold; - for (i = 0; i < (major ? 10 : 1); i++, specifickeyholdtime -= 4) + for (i = 0; i < (major ? 10 : 1); i++, specifickeyholdtime -= (CHAOHOLD_STANDARD/10)) { fixed_t radius = baseradius; fixed_t thiskeyx, thiskeyy; @@ -6140,11 +6147,11 @@ challengedesc: radius <<= FRACBITS; keyholdrotation += 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) - * (FRACUNIT/(CHAOHOLD_MAX - (CHAOHOLD_BEGIN + CHAOHOLD_END))); + * (FRACUNIT/(CHAOHOLD_STANDARD)); // intentionally not chaohold_duration if (i == 0) { - INT32 time = 3 - (keyholdrotation - 1) / (90 * FRACUNIT); + INT32 time = (major ? 5 : 3) - (keyholdrotation - 1) / (90 * FRACUNIT); if (time <= 5 && time >= 0) V_DrawScaledPatch(tilex + 2, tiley - 2, 0, kp_eggnum[time]); } diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 33a87fed5..6eba38d84 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -382,7 +382,14 @@ void M_ChallengesTick(void) { challengesmenu.chaokeyhold++; - if (challengesmenu.chaokeyhold > CHAOHOLD_MAX) + const UINT32 chaohold_duration = + CHAOHOLD_PADDING + + ((unlockables[challengesmenu.currentunlock].majorunlock == true) + ? CHAOHOLD_MAJOR + : CHAOHOLD_STANDARD + ); + + if (challengesmenu.chaokeyhold > chaohold_duration) { #ifndef CHAOKEYDEBUG gamedata->chaokeys -= (unlockables[challengesmenu.currentunlock].majorunlock == true) From 1a91f41dbbfd21bc24da83024e59cbe137df02fb Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 27 Sep 2023 21:19:39 +0100 Subject: [PATCH 06/98] Update Challenge Menu UI - Percentage for Challenges completion - Smaller footprint in either top corner - Uses mini rank numbers - Rounds to new Key meter is now vertical --- src/k_hud.c | 4 +- src/k_hud.h | 1 + src/k_menu.h | 2 +- src/k_menudraw.c | 78 +++++++++++++++++++++++------------ src/menus/extras-challenges.c | 31 +++++++------- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/k_hud.c b/src/k_hud.c index 42773bb77..38b80b035 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -75,8 +75,8 @@ static patch_t *kp_racefinish[6]; static patch_t *kp_positionnum[10][2][2]; // number, overlay or underlay, splitscreen -static patch_t *kp_facenum[MAXPLAYERS+1]; -patch_t *kp_facehighlight[8]; +patch_t *kp_facenum[MAXPLAYERS+1]; +static patch_t *kp_facehighlight[8]; static patch_t *kp_nocontestminimap; static patch_t *kp_spbminimap; diff --git a/src/k_hud.h b/src/k_hud.h index b16c15e2c..663690c7c 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -81,6 +81,7 @@ extern patch_t *kp_button_right[2]; extern patch_t *kp_button_left[2]; extern patch_t *kp_eggnum[6]; +extern patch_t *kp_facenum[MAXPLAYERS+1]; #ifdef __cplusplus } // extern "C" diff --git a/src/k_menu.h b/src/k_menu.h index 8c91a612c..8b43bd498 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1206,7 +1206,7 @@ void M_DrawAddons(void); #define CC_TOTAL 0 #define CC_UNLOCKED 1 -#define CC_TALLY 2 +#define CC_PERCENT 2 #define CC_ANIM 3 #define CC_CHAOANIM 4 #define CC_CHAONOPE 5 diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 691ccf65e..9a8e8f93e 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5896,7 +5896,6 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } #define challengesgridstep 22 -#define challengekeybarwidth 50 void M_DrawChallenges(void) { @@ -6046,11 +6045,23 @@ challengedesc: // Tally { - str = va("%d/%d", - challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY], - challengesmenu.unlockcount[CC_TOTAL] - ); - V_DrawRightAlignedTimerString(BASEVIDWIDTH-7, 9-challengesmenu.unlockcount[CC_ANIM], 0, str); + INT32 textx = BASEVIDWIDTH - 24, texty = 20-challengesmenu.unlockcount[CC_ANIM]; + UINT8 numbers[3]; + numbers[0] = ((challengesmenu.unlockcount[CC_PERCENT] / 100) % 10); + numbers[1] = ((challengesmenu.unlockcount[CC_PERCENT] / 10) % 10); + numbers[2] = (challengesmenu.unlockcount[CC_PERCENT] % 10); + + patch_t *percent = W_CachePatchName("K_SPDML1", PU_CACHE); + + V_DrawScaledPatch(textx + 3, texty, 0, percent); + + i = 3; + while (i) + { + i--; + textx -= 6; + V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); + } } // Chao Keys @@ -6061,34 +6072,50 @@ challengedesc: offs = -offs; offs /= 2; - if (gamedata->chaokeys > 9) - { - offs -= 6; - if (gamedata->chaokeys > 99) - offs -= 2; // as far as we can go - } - - fixed_t keyx = (8+offs)*FRACUNIT, keyy = 5*FRACUNIT; - - const char *timerstr = va("%u", gamedata->chaokeys); - - V_DrawTimerString((27+offs), 9-challengesmenu.unlockcount[CC_CHAOANIM], 0, timerstr); + fixed_t keyx = (3+offs)*FRACUNIT, keyy = 0; K_drawButton( - (27 + offs + V_TimerStringWidth(timerstr, 0) + 2) << FRACBITS, - 11 << FRACBITS, + (21 + offs + 2) << FRACBITS, + 8 << FRACBITS, 0, kp_button_c[1], M_MenuExtraHeld(pid) ); - offs = challengekeybarwidth; + #define challengekeybarheight 27 + + offs = challengekeybarheight; if (gamedata->chaokeys < GDMAX_CHAOKEYS) - offs = ((gamedata->pendingkeyroundoffset * challengekeybarwidth)/GDCONVERT_ROUNDSTOKEY); + offs = ((gamedata->pendingkeyroundoffset * challengekeybarheight)/GDCONVERT_ROUNDSTOKEY); if (offs > 0) - V_DrawFill(1, 25, offs, 2, 0); - if (offs < challengekeybarwidth) - V_DrawFadeFill(1+offs, 25, challengekeybarwidth-offs, 2, 0, 31, challengetransparentstrength); + V_DrawFill(1, 1 + (challengekeybarheight-offs), 2, offs, 0); + if (offs < challengekeybarheight) + V_DrawFadeFill(1, 1, 2, challengekeybarheight-offs, 0, 31, challengetransparentstrength); + + #undef challengekeybarheight + + { + INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CC_CHAOANIM]; + UINT8 numbers[4]; + numbers[0] = ((gamedata->chaokeys / 100) % 10); + numbers[1] = ((gamedata->chaokeys / 10) % 10); + numbers[2] = (gamedata->chaokeys % 10); + + numbers[3] = ((gamedata->chaokeys / 1000) % 10); + if (numbers[3] != 0) + { + V_DrawScaledPatch(textx - 1, texty, 0, kp_facenum[numbers[3]]); + textx += 5; + } + + i = 0; + while (i < 3) + { + V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); + textx += 6; + i++; + } + } if (challengesmenu.currentunlock < MAXUNLOCKABLES && challengesmenu.chaokeyhold) { @@ -6202,7 +6229,6 @@ challengedesc: #undef challengetransparentstrength #undef challengesgridstep -#undef challengekeybarwidth // Statistics menu diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 6eba38d84..4cdee237d 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -54,6 +54,14 @@ menu_t MISC_StatisticsDef = { struct challengesmenu_s challengesmenu; +static void M_UpdateChallengeGridVisuals(void) +{ + // Currently only updates the completion %. + challengesmenu.unlockcount[CC_PERCENT] = + (100 * challengesmenu.unlockcount[CC_UNLOCKED]) + /challengesmenu.unlockcount[CC_TOTAL]; +} + static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) { UINT16 i; @@ -263,6 +271,8 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) challengesmenu.unlockcount[CC_UNLOCKED]++; } + M_UpdateChallengeGridVisuals(); + if (challengesmenu.pending) M_ChallengesAutoFocus(newunlock, true); else if (newunlock >= MAXUNLOCKABLES && gamedata->pendingkeyrounds > 0 @@ -312,7 +322,7 @@ static boolean M_CanKeyHiliTile(void) // Not a hinted tile OR a fresh board. if (!(challengesmenu.extradata[i].flags & CHE_HINT) - && (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] > 0)) + && (challengesmenu.unlockcount[CC_UNLOCKED] > 0)) return false; // Marked as major? @@ -520,7 +530,9 @@ void M_ChallengesTick(void) Z_Free(challengesmenu.unlockcondition); challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); - challengesmenu.unlockcount[CC_TALLY]++; + challengesmenu.unlockcount[CC_UNLOCKED]++; + M_UpdateChallengeGridVisuals(); + challengesmenu.unlockcount[CC_ANIM]++; if (challengesmenu.extradata) @@ -582,15 +594,6 @@ void M_ChallengesTick(void) } else if (!challengesmenu.chaokeyhold) { - - // Tick down the tally. (currently not visible) - /*if ((challengesmenu.ticker & 1) - && challengesmenu.unlockcount[CC_TALLY] > 0) - { - challengesmenu.unlockcount[CC_TALLY]--; - challengesmenu.unlockcount[CC_UNLOCKED]++; - }*/ - if (challengesmenu.fade > 0) { // Fade decrease. @@ -631,10 +634,8 @@ boolean M_ChallengesInputs(INT32 ch) { gamedata->unlocked[challengesmenu.currentunlock] = gamedata->unlockpending[challengesmenu.currentunlock] = false; - if (challengesmenu.unlockcount[CC_TALLY] > 0) - challengesmenu.unlockcount[CC_TALLY]--; - else - challengesmenu.unlockcount[CC_UNLOCKED]--; + challengesmenu.unlockcount[CC_UNLOCKED]--; + M_UpdateChallengeGridVisuals(); } #endif } From 6bf364545eb23e0875c24b2f2190d742e487c999 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 09:57:32 +0100 Subject: [PATCH 07/98] Seperate our M_DrawChallengeKeys from the already pretty big M_DrawChallenges --- src/k_menudraw.c | 294 ++++++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 141 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 9a8e8f93e..82c4405ad 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5897,10 +5897,160 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) #define challengesgridstep 22 -void M_DrawChallenges(void) +static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) { const UINT8 pid = 0; + patch_t *key = W_CachePatchName("UN_CHA00", PU_CACHE); + INT32 offs = challengesmenu.unlockcount[CC_CHAONOPE]; + if (offs & 1) + offs = -offs; + offs /= 2; + + fixed_t keyx = (3+offs)*FRACUNIT, keyy = 0; + + // Button prompt + K_drawButton( + (21 + offs + 2) << FRACBITS, + 8 << FRACBITS, + 0, kp_button_c[1], + M_MenuExtraHeld(pid) + ); + + // Meter of rounds played that contribute to Chao Key generation + { + #define challengekeybarheight 27 + + offs = challengekeybarheight; + if (gamedata->chaokeys < GDMAX_CHAOKEYS) + offs = ((gamedata->pendingkeyroundoffset * challengekeybarheight)/GDCONVERT_ROUNDSTOKEY); + + if (offs > 0) + V_DrawFill(1, 1 + (challengekeybarheight-offs), 2, offs, 0); + if (offs < challengekeybarheight) + V_DrawFadeFill(1, 1, 2, challengekeybarheight-offs, 0, 31, challengetransparentstrength); + + #undef challengekeybarheight + } + + // Counter + { + INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CC_CHAOANIM]; + UINT8 numbers[4]; + numbers[0] = ((gamedata->chaokeys / 100) % 10); + numbers[1] = ((gamedata->chaokeys / 10) % 10); + numbers[2] = (gamedata->chaokeys % 10); + + numbers[3] = ((gamedata->chaokeys / 1000) % 10); + if (numbers[3] != 0) + { + V_DrawScaledPatch(textx - 1, texty, 0, kp_facenum[numbers[3]]); + textx += 5; + } + + UINT8 i = 0; + while (i < 3) + { + V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); + textx += 6; + i++; + } + } + + UINT8 keysbeingused = 0; + + // The Chao Key swooping animation + if (challengesmenu.currentunlock < MAXUNLOCKABLES && challengesmenu.chaokeyhold) + { + fixed_t baseradius = challengesgridstep; + + boolean major = false, ending = false; + if (unlockables[challengesmenu.currentunlock].majorunlock == true) + { + major = true; + tilex += challengesgridstep/2; + tiley += challengesgridstep/2; + baseradius = (7*baseradius)/4; + } + + const INT32 chaohold_duration = + CHAOHOLD_PADDING + + (major + ? CHAOHOLD_MAJOR + : CHAOHOLD_STANDARD + ); + + if (challengesmenu.chaokeyhold >= chaohold_duration - CHAOHOLD_END) + { + ending = true; + baseradius = ((chaohold_duration - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); + } + + INT16 specifickeyholdtime = challengesmenu.chaokeyhold; + + for (; keysbeingused < (major ? 10 : 1); keysbeingused++, specifickeyholdtime -= (CHAOHOLD_STANDARD/10)) + { + fixed_t radius = baseradius; + fixed_t thiskeyx, thiskeyy; + fixed_t keyholdrotation = 0; + + if (specifickeyholdtime < CHAOHOLD_BEGIN) + { + if (specifickeyholdtime < 0) + { + // Nothing following will be relevant + break; + } + + radius = (specifickeyholdtime*radius)*(FRACUNIT/CHAOHOLD_BEGIN); + thiskeyx = keyx + specifickeyholdtime*((tilex*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; + thiskeyy = keyy + specifickeyholdtime*((tiley*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; + } + else + { + keyholdrotation = (-36 * keysbeingused) * FRACUNIT; // 360/10 + + if (ending == false) + { + radius <<= FRACBITS; + + keyholdrotation += 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) + * (FRACUNIT/(CHAOHOLD_STANDARD)); // intentionally not chaohold_duration + + if (keysbeingused == 0) + { + INT32 time = (major ? 5 : 3) - (keyholdrotation - 1) / (90 * FRACUNIT); + if (time <= 5 && time >= 0) + V_DrawScaledPatch(tilex + 2, tiley - 2, 0, kp_eggnum[time]); + } + } + + thiskeyx = tilex*FRACUNIT; + thiskeyy = tiley*FRACUNIT; + } + + if (radius != 0) + { + angle_t ang = (FixedAngle( + keyholdrotation + ) >> ANGLETOFINESHIFT) & FINEMASK; + + thiskeyx += FixedMul(radius, FINESINE(ang)); + thiskeyy -= FixedMul(radius, FINECOSINE(ang)); + } + + V_DrawFixedPatch(thiskeyx, thiskeyy, FRACUNIT, 0, key, NULL); + } + } + + // The final Chao Key on the stack + { + V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, NULL); + } +} + +void M_DrawChallenges(void) +{ INT32 x = currentMenu->x, explodex, selectx = 0, selecty = 0; INT32 y; INT16 i, j; @@ -6064,146 +6214,8 @@ challengedesc: } } - // Chao Keys - { - patch_t *key = W_CachePatchName("UN_CHA00", PU_CACHE); - INT32 offs = challengesmenu.unlockcount[CC_CHAONOPE]; - if (offs & 1) - offs = -offs; - offs /= 2; - - fixed_t keyx = (3+offs)*FRACUNIT, keyy = 0; - - K_drawButton( - (21 + offs + 2) << FRACBITS, - 8 << FRACBITS, - 0, kp_button_c[1], - M_MenuExtraHeld(pid) - ); - - #define challengekeybarheight 27 - - offs = challengekeybarheight; - if (gamedata->chaokeys < GDMAX_CHAOKEYS) - offs = ((gamedata->pendingkeyroundoffset * challengekeybarheight)/GDCONVERT_ROUNDSTOKEY); - - if (offs > 0) - V_DrawFill(1, 1 + (challengekeybarheight-offs), 2, offs, 0); - if (offs < challengekeybarheight) - V_DrawFadeFill(1, 1, 2, challengekeybarheight-offs, 0, 31, challengetransparentstrength); - - #undef challengekeybarheight - - { - INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CC_CHAOANIM]; - UINT8 numbers[4]; - numbers[0] = ((gamedata->chaokeys / 100) % 10); - numbers[1] = ((gamedata->chaokeys / 10) % 10); - numbers[2] = (gamedata->chaokeys % 10); - - numbers[3] = ((gamedata->chaokeys / 1000) % 10); - if (numbers[3] != 0) - { - V_DrawScaledPatch(textx - 1, texty, 0, kp_facenum[numbers[3]]); - textx += 5; - } - - i = 0; - while (i < 3) - { - V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); - textx += 6; - i++; - } - } - - if (challengesmenu.currentunlock < MAXUNLOCKABLES && challengesmenu.chaokeyhold) - { - fixed_t tilex = selectx, tiley = selecty; - - fixed_t baseradius = challengesgridstep; - - boolean major = false, ending = false; - if (unlockables[challengesmenu.currentunlock].majorunlock == true) - { - major = true; - tilex += challengesgridstep/2; - tiley += challengesgridstep/2; - baseradius = (7*baseradius)/4; - } - - const INT32 chaohold_duration = - CHAOHOLD_PADDING - + (major - ? CHAOHOLD_MAJOR - : CHAOHOLD_STANDARD - ); - - if (challengesmenu.chaokeyhold >= chaohold_duration - CHAOHOLD_END) - { - ending = true; - baseradius = ((chaohold_duration - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); - } - - INT16 specifickeyholdtime = challengesmenu.chaokeyhold; - - for (i = 0; i < (major ? 10 : 1); i++, specifickeyholdtime -= (CHAOHOLD_STANDARD/10)) - { - fixed_t radius = baseradius; - fixed_t thiskeyx, thiskeyy; - fixed_t keyholdrotation = 0; - - if (specifickeyholdtime < CHAOHOLD_BEGIN) - { - if (specifickeyholdtime < 0) - { - // Nothing following will be relevant - break; - } - - radius = (specifickeyholdtime*radius)*(FRACUNIT/CHAOHOLD_BEGIN); - thiskeyx = keyx + specifickeyholdtime*((tilex*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; - thiskeyy = keyy + specifickeyholdtime*((tiley*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; - } - else - { - keyholdrotation = (-36 * i) * FRACUNIT; - - if (ending == false) - { - radius <<= FRACBITS; - - keyholdrotation += 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) - * (FRACUNIT/(CHAOHOLD_STANDARD)); // intentionally not chaohold_duration - - if (i == 0) - { - INT32 time = (major ? 5 : 3) - (keyholdrotation - 1) / (90 * FRACUNIT); - if (time <= 5 && time >= 0) - V_DrawScaledPatch(tilex + 2, tiley - 2, 0, kp_eggnum[time]); - } - } - - thiskeyx = tilex*FRACUNIT; - thiskeyy = tiley*FRACUNIT; - } - - if (radius != 0) - { - angle_t ang = (FixedAngle( - keyholdrotation - ) >> ANGLETOFINESHIFT) & FINEMASK; - - thiskeyx += FixedMul(radius, FINESINE(ang)); - thiskeyy -= FixedMul(radius, FINECOSINE(ang)); - } - - V_DrawFixedPatch(thiskeyx, thiskeyy, FRACUNIT, 0, key, NULL); - } - } - - V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, NULL); - } + // Chao Key information + M_DrawChallengeKeys(selectx, selecty); // Derived from M_DrawCharSelectPreview x = 40; From bd7be2dd6f0498e5bbee0733bd289ff404c5315a Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 09:58:21 +0100 Subject: [PATCH 08/98] M_DrawChallengeKeys: Grey out the last Chao Key on the stack if you're either out of them, or going to be out of them after the animation is over --- src/k_menudraw.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 82c4405ad..762361bdc 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5996,7 +5996,7 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) if (specifickeyholdtime < CHAOHOLD_BEGIN) { - if (specifickeyholdtime < 0) + if (specifickeyholdtime <= 0) { // Nothing following will be relevant break; @@ -6045,7 +6045,15 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) // The final Chao Key on the stack { - V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, NULL); + UINT8 *lastkeycolormap = NULL; + + if (gamedata->chaokeys <= keysbeingused) + { + // Greyed out if there's going to be none left + lastkeycolormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE); + } + + V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, lastkeycolormap); } } From cf1367fb8df94cb2ec83b850ad28a4c44c6a8b2b Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 12:28:14 +0100 Subject: [PATCH 09/98] Challenges Menu: Chao Medal icon in the top right Fills up as you complete challenges. Unlock all Challenges without using Chao Keys on a major tile to get the Beginner Chao Medal. Do so without using any Chao Keys at all and you get 101%, with the Challenge Chao Medal! (This can retroactively apply if you complete the relevant tasks legitimately at a later date.) Also makes CC_s into an enum (now prefixed with CMC_, since CC_MAX exists in a header file already). --- src/k_menu.h | 29 ++++++--- src/k_menudraw.c | 51 ++++++++++++--- src/menus/extras-challenges.c | 114 ++++++++++++++++++++++++---------- 3 files changed, 147 insertions(+), 47 deletions(-) diff --git a/src/k_menu.h b/src/k_menu.h index 8b43bd498..c6cda78d6 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1204,13 +1204,26 @@ void M_DrawAddons(void); #define RIGHTUNLOCKSCROLL 3 #define LEFTUNLOCKSCROLL (RIGHTUNLOCKSCROLL-1) -#define CC_TOTAL 0 -#define CC_UNLOCKED 1 -#define CC_PERCENT 2 -#define CC_ANIM 3 -#define CC_CHAOANIM 4 -#define CC_CHAONOPE 5 -#define CC_MAX 6 +typedef enum +{ + CMC_TOTAL = 0, + CMC_UNLOCKED, + + CMC_KEYED, + CMC_MAJORSKIPPED, + + CMC_PERCENT, + + CMC_MEDALID, + CMC_MEDALBLANK, + CMC_MEDALFILLED, + + CMC_ANIM, + CMC_CHAOANIM, + CMC_CHAONOPE, + + CMC_MAX, +} challengesmenucount_e; #define TILEFLIP_MAX 16 @@ -1251,7 +1264,7 @@ extern struct challengesmenu_s { boolean requestflip; - UINT16 unlockcount[CC_MAX]; + UINT16 unlockcount[CMC_MAX]; UINT8 fade; } challengesmenu; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 762361bdc..ed285ea73 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5902,7 +5902,7 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) const UINT8 pid = 0; patch_t *key = W_CachePatchName("UN_CHA00", PU_CACHE); - INT32 offs = challengesmenu.unlockcount[CC_CHAONOPE]; + INT32 offs = challengesmenu.unlockcount[CMC_CHAONOPE]; if (offs & 1) offs = -offs; offs /= 2; @@ -5935,7 +5935,7 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) // Counter { - INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CC_CHAOANIM]; + INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CMC_CHAOANIM]; UINT8 numbers[4]; numbers[0] = ((gamedata->chaokeys / 100) % 10); numbers[1] = ((gamedata->chaokeys / 10) % 10); @@ -6201,13 +6201,50 @@ challengedesc: V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); } - // Tally + // Percentage { - INT32 textx = BASEVIDWIDTH - 24, texty = 20-challengesmenu.unlockcount[CC_ANIM]; + patch_t *medal = W_CachePatchName( + va("UN_MDL%c", '0' + challengesmenu.unlockcount[CMC_MEDALID]), + PU_CACHE + ); + + fixed_t medalchopy = 1; + + for (i = CMC_MEDALBLANK; i <= CMC_MEDALFILLED; i++) + { + if (challengesmenu.unlockcount[i] == 0) + continue; + + V_SetClipRect( + 0, + medalchopy << FRACBITS, + BASEVIDWIDTH << FRACBITS, + (medalchopy + challengesmenu.unlockcount[CMC_MEDALBLANK]) << FRACBITS, + 0 + ); + + UINT8 *medalcolormap = NULL; + if (i == CMC_MEDALBLANK) + { + medalcolormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE); + } + else if (challengesmenu.unlockcount[CMC_MEDALID] == 0) + { + medalcolormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); + } + + V_DrawFixedPatch((BASEVIDWIDTH - 32)*FRACUNIT, 1*FRACUNIT, FRACUNIT, 0, medal, medalcolormap); + + V_ClearClipRect(); + + medalchopy += challengesmenu.unlockcount[i]; + } + + INT32 textx = BASEVIDWIDTH - 24, texty = 20-challengesmenu.unlockcount[CMC_ANIM]; UINT8 numbers[3]; - numbers[0] = ((challengesmenu.unlockcount[CC_PERCENT] / 100) % 10); - numbers[1] = ((challengesmenu.unlockcount[CC_PERCENT] / 10) % 10); - numbers[2] = (challengesmenu.unlockcount[CC_PERCENT] % 10); + numbers[0] = ((challengesmenu.unlockcount[CMC_PERCENT] / 100) % 10); + numbers[1] = ((challengesmenu.unlockcount[CMC_PERCENT] / 10) % 10); + numbers[2] = (challengesmenu.unlockcount[CMC_PERCENT] % 10); patch_t *percent = W_CachePatchName("K_SPDML1", PU_CACHE); diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 4cdee237d..f450444ec 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -56,10 +56,79 @@ struct challengesmenu_s challengesmenu; static void M_UpdateChallengeGridVisuals(void) { - // Currently only updates the completion %. - challengesmenu.unlockcount[CC_PERCENT] = - (100 * challengesmenu.unlockcount[CC_UNLOCKED]) - /challengesmenu.unlockcount[CC_TOTAL]; + UINT16 i; + + challengesmenu.unlockcount[CMC_UNLOCKED] = 0; + challengesmenu.unlockcount[CMC_TOTAL] = 0; + + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (!unlockables[i].conditionset) + { + continue; + } + + challengesmenu.unlockcount[CMC_TOTAL]++; + + if (!gamedata->unlocked[i]) + { + continue; + } + + challengesmenu.unlockcount[CMC_UNLOCKED]++; + + if (M_Achieved(unlockables[i].conditionset - 1) == true) + { + continue; + } + + challengesmenu.unlockcount[CMC_KEYED]++; + + if (unlockables[i].majorunlock == false) + { + continue; + } + + challengesmenu.unlockcount[CMC_MAJORSKIPPED]++; + } + + challengesmenu.unlockcount[CMC_PERCENT] = + (100 * challengesmenu.unlockcount[CMC_UNLOCKED]) + /challengesmenu.unlockcount[CMC_TOTAL]; + + #define medalheight (19) + + challengesmenu.unlockcount[CMC_MEDALID] = 0; + + if (challengesmenu.unlockcount[CMC_PERCENT] == 100) + { + challengesmenu.unlockcount[CMC_MEDALFILLED] = medalheight; + + if (challengesmenu.unlockcount[CMC_KEYED] == 0) + { + challengesmenu.unlockcount[CMC_MEDALID] = 2; + challengesmenu.unlockcount[CMC_PERCENT]++; // 101% + } + else if (challengesmenu.unlockcount[CMC_MAJORSKIPPED] == 0) + { + challengesmenu.unlockcount[CMC_MEDALID] = 1; + } + } + else + { + challengesmenu.unlockcount[CMC_MEDALFILLED] = + (medalheight * challengesmenu.unlockcount[CMC_UNLOCKED]) + /challengesmenu.unlockcount[CMC_TOTAL]; + + if (challengesmenu.unlockcount[CMC_MEDALFILLED] == 0 && challengesmenu.unlockcount[CMC_UNLOCKED] != 0) + { + // Cheat to give you a sliver of pixel. + challengesmenu.unlockcount[CMC_MEDALFILLED] = 1; + } + } + + challengesmenu.unlockcount[CMC_MEDALBLANK] = + medalheight - challengesmenu.unlockcount[CMC_MEDALFILLED]; } static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) @@ -218,7 +287,7 @@ static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) { - UINT16 i, newunlock; + UINT16 newunlock; if (Playing()) return desiredmenu; @@ -254,22 +323,6 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) memset(setup_explosions, 0, sizeof(setup_explosions)); memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount)); - for (i = 0; i < MAXUNLOCKABLES; i++) - { - if (!unlockables[i].conditionset) - { - continue; - } - - challengesmenu.unlockcount[CC_TOTAL]++; - - if (!gamedata->unlocked[i]) - { - continue; - } - - challengesmenu.unlockcount[CC_UNLOCKED]++; - } M_UpdateChallengeGridVisuals(); @@ -322,7 +375,7 @@ static boolean M_CanKeyHiliTile(void) // Not a hinted tile OR a fresh board. if (!(challengesmenu.extradata[i].flags & CHE_HINT) - && (challengesmenu.unlockcount[CC_UNLOCKED] > 0)) + && (challengesmenu.unlockcount[CMC_UNLOCKED] > 0)) return false; // Marked as major? @@ -353,7 +406,7 @@ void M_ChallengesTick(void) if (setup_explosions[i].tics > 0) setup_explosions[i].tics--; } - for (i = CC_ANIM; i < CC_MAX; i++) + for (i = CMC_ANIM; i < CMC_MAX; i++) { if (challengesmenu.unlockcount[i] > 0) challengesmenu.unlockcount[i]--; @@ -406,7 +459,7 @@ void M_ChallengesTick(void) ? 10 : 1; #endif challengesmenu.chaokeyhold = 0; - challengesmenu.unlockcount[CC_CHAOANIM]++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; S_StartSound(NULL, sfx_chchng); @@ -419,7 +472,7 @@ void M_ChallengesTick(void) else { challengesmenu.chaokeyhold = 0; - challengesmenu.unlockcount[CC_CHAONOPE] = 6; + challengesmenu.unlockcount[CMC_CHAONOPE] = 6; S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2 } } @@ -479,7 +532,7 @@ void M_ChallengesTick(void) S_StartSound(NULL, sfx_achiev); gamedata->keyspending--; gamedata->chaokeys++; - challengesmenu.unlockcount[CC_CHAOANIM]++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; if (gamedata->musicstate < GDMUSIC_KEYG) gamedata->musicstate = GDMUSIC_KEYG; @@ -529,11 +582,9 @@ void M_ChallengesTick(void) if (challengesmenu.unlockcondition) Z_Free(challengesmenu.unlockcondition); challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); - - challengesmenu.unlockcount[CC_UNLOCKED]++; M_UpdateChallengeGridVisuals(); - challengesmenu.unlockcount[CC_ANIM]++; + challengesmenu.unlockcount[CMC_ANIM]++; if (challengesmenu.extradata) { @@ -626,7 +677,7 @@ boolean M_ChallengesInputs(INT32 ch) } else { - challengesmenu.unlockcount[CC_CHAONOPE] = 6; + challengesmenu.unlockcount[CMC_CHAONOPE] = 6; S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2 #ifdef CHAOKEYDEBUG @@ -634,7 +685,6 @@ boolean M_ChallengesInputs(INT32 ch) { gamedata->unlocked[challengesmenu.currentunlock] = gamedata->unlockpending[challengesmenu.currentunlock] = false; - challengesmenu.unlockcount[CC_UNLOCKED]--; M_UpdateChallengeGridVisuals(); } #endif @@ -645,7 +695,7 @@ boolean M_ChallengesInputs(INT32 ch) else if (M_MenuButtonPressed(pid, MBT_Z)) { gamedata->chaokeys++; - challengesmenu.unlockcount[CC_CHAOANIM]++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; S_StartSound(NULL, sfx_dbgsal); return true; } From 54ddf724668f126a7bdb6c2144184bba2062c44a Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 12:29:44 +0100 Subject: [PATCH 10/98] Challenges Menu: Do not show any conditiontext underlay for empty tile spots --- src/k_menudraw.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index ed285ea73..d94a586c7 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6088,6 +6088,7 @@ void M_DrawChallenges(void) } // Do underlay for everything else early so the bottom of the reticule doesn't get shaded over. + if (challengesmenu.currentunlock < MAXUNLOCKABLES) { y = 120; @@ -6191,14 +6192,10 @@ challengedesc: { str = "???"; //M_CreateSecretMenuOption(str); } - } - else - { - str = "---"; - } - offset = V_LSTitleLowStringWidth(str, 0) / 2; - V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); + offset = V_LSTitleLowStringWidth(str, 0) / 2; + V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); + } } // Percentage From aca676a7b60eac984c1dd694fafd3eeb72615d79 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 21:40:24 +0100 Subject: [PATCH 11/98] M_DrawChallengePreview: Fix inverted presence of Alt-character stats dot on Character Select preview --- src/k_menudraw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index d94a586c7..6f5bae2f0 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5600,7 +5600,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) break; } - M_DrawCharacterIconAndEngine(4, BASEVIDHEIGHT-(4+16), i, colormap, (i == skin)); + M_DrawCharacterIconAndEngine(4, BASEVIDHEIGHT-(4+16), i, colormap, (i != skin)); } break; } From 20f47a69dd9270531fddf02be7ef6e8b2c00eb8a Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 21:54:42 +0100 Subject: [PATCH 12/98] Challenges Menu: Cooler winged counters for Chao Keys and Completion Percentage Moves rounds-to-key metyr back to being horizontal and under the counter again, but integrated into the wing --- src/k_menudraw.c | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 6f5bae2f0..d2f739e71 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5907,30 +5907,28 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) offs = -offs; offs /= 2; - fixed_t keyx = (3+offs)*FRACUNIT, keyy = 0; + fixed_t keyx = (8+offs)*FRACUNIT, keyy = 0; // Button prompt K_drawButton( - (21 + offs + 2) << FRACBITS, - 8 << FRACBITS, + 24 << FRACBITS, + 16 << FRACBITS, 0, kp_button_c[1], M_MenuExtraHeld(pid) ); - // Meter of rounds played that contribute to Chao Key generation + // Metyr of rounds played that contribute to Chao Key generation { - #define challengekeybarheight 27 + const INT32 keybarlen = 36, keybary = 28; - offs = challengekeybarheight; + offs = keybarlen; if (gamedata->chaokeys < GDMAX_CHAOKEYS) - offs = ((gamedata->pendingkeyroundoffset * challengekeybarheight)/GDCONVERT_ROUNDSTOKEY); + offs = ((gamedata->pendingkeyroundoffset * keybarlen)/GDCONVERT_ROUNDSTOKEY); if (offs > 0) - V_DrawFill(1, 1 + (challengekeybarheight-offs), 2, offs, 0); - if (offs < challengekeybarheight) - V_DrawFadeFill(1, 1, 2, challengekeybarheight-offs, 0, 31, challengetransparentstrength); - - #undef challengekeybarheight + V_DrawFill(1, keybary, offs, 1, 0); + if (offs < keybarlen) + V_DrawFadeFill(1+offs, keybary, keybarlen-offs, 1, 0, 31, challengetransparentstrength); } // Counter @@ -5944,8 +5942,8 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) numbers[3] = ((gamedata->chaokeys / 1000) % 10); if (numbers[3] != 0) { - V_DrawScaledPatch(textx - 1, texty, 0, kp_facenum[numbers[3]]); - textx += 5; + V_DrawScaledPatch(textx - 4, texty, 0, kp_facenum[numbers[3]]); + textx += 2; } UINT8 i = 0; @@ -6198,6 +6196,18 @@ challengedesc: } } + // Wings + { + const INT32 endy = 18, endlen = 38; + patch_t *endwing = W_CachePatchName("K_BOSB01", PU_CACHE); + + V_DrawFill(0, endy, endlen, 11, 24); + V_DrawFixedPatch(endlen*FRACUNIT, endy*FRACUNIT, FRACUNIT, V_FLIP, endwing, NULL); + + V_DrawFill(BASEVIDWIDTH - endlen, endy, endlen, 11, 24); + V_DrawFixedPatch((BASEVIDWIDTH - endlen)*FRACUNIT, endy*FRACUNIT, FRACUNIT, 0, endwing, NULL); + } + // Percentage { patch_t *medal = W_CachePatchName( @@ -6230,14 +6240,14 @@ challengedesc: medalcolormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); } - V_DrawFixedPatch((BASEVIDWIDTH - 32)*FRACUNIT, 1*FRACUNIT, FRACUNIT, 0, medal, medalcolormap); + V_DrawFixedPatch((BASEVIDWIDTH - 31)*FRACUNIT, 1*FRACUNIT, FRACUNIT, 0, medal, medalcolormap); V_ClearClipRect(); medalchopy += challengesmenu.unlockcount[i]; } - INT32 textx = BASEVIDWIDTH - 24, texty = 20-challengesmenu.unlockcount[CMC_ANIM]; + INT32 textx = BASEVIDWIDTH - 21, texty = 20-challengesmenu.unlockcount[CMC_ANIM]; UINT8 numbers[3]; numbers[0] = ((challengesmenu.unlockcount[CMC_PERCENT] / 100) % 10); numbers[1] = ((challengesmenu.unlockcount[CMC_PERCENT] / 10) % 10); @@ -6245,7 +6255,7 @@ challengedesc: patch_t *percent = W_CachePatchName("K_SPDML1", PU_CACHE); - V_DrawScaledPatch(textx + 3, texty, 0, percent); + V_DrawScaledPatch(textx + 2, texty, 0, percent); i = 3; while (i) From 07afe6cb0698abefecc145040d00f585a458318c Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 28 Sep 2023 22:30:26 +0100 Subject: [PATCH 13/98] Challenge Grid is 5 tiles high now again I'm sorry, it was just TOO SWAG. Gamedata minor version increment --- src/g_game.c | 6 +++--- src/k_menudraw.c | 4 ++-- src/m_cond.h | 2 +- src/menus/extras-challenges.c | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index d78bc3f11..3474d6ca9 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4388,7 +4388,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 6 // Change every format update +#define GD_VERSIONMINOR 7 // Change every format update typedef enum { @@ -4481,7 +4481,7 @@ void G_LoadGameData(void) FIL_WriteFile(va("%s" PATHSEP "%s.bak", srb2home, gamedatafilename), save.buffer, save.size); } - if ((versionMinor == 0 || versionMinor == 1) + if ((versionMinor <= 6) #ifdef DEVELOP || M_CheckParm("-resetchallengegrid") #endif @@ -4593,7 +4593,7 @@ void G_LoadGameData(void) if (gridunusable) { UINT16 burn = READUINT16(save.p); // Previous challengegridwidth - UINT8 height = (versionMinor > 0) ? CHALLENGEGRIDHEIGHT : 5; + UINT8 height = (versionMinor && versionMinor <= 6) ? 4 : CHALLENGEGRIDHEIGHT; save.p += (burn * height * unlockreadsize); // Step over previous grid data gamedata->challengegridwidth = 0; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index d2f739e71..681be183a 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5572,7 +5572,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) addflags ^= V_FLIP; // This sprite is left/right flipped! } - V_DrawFixedPatch(x*FRACUNIT, (y+6)*FRACUNIT, FRACUNIT, addflags, patch, NULL); + V_DrawFixedPatch(x*FRACUNIT, (y+7)*FRACUNIT, FRACUNIT, addflags, patch, NULL); return; } @@ -6105,7 +6105,7 @@ void M_DrawChallenges(void) y = currentMenu->y; - V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength); + V_DrawFadeFill(0, y-2, BASEVIDWIDTH, (challengesgridstep * CHALLENGEGRIDHEIGHT) + 2, 0, 31, challengetransparentstrength); x -= (challengesgridstep-1); diff --git a/src/m_cond.h b/src/m_cond.h index 00960abbb..fb4d00195 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -219,7 +219,7 @@ typedef enum #define MAXEMBLEMS (MAXCONDITIONSETS*2) #define MAXUNLOCKABLES MAXCONDITIONSETS -#define CHALLENGEGRIDHEIGHT 4 +#define CHALLENGEGRIDHEIGHT 5 #ifdef DEVELOP #define CHALLENGEGRIDLOOPWIDTH 3 #else diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index f450444ec..c0cd98b2e 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -21,7 +21,7 @@ menu_t MISC_ChallengesDef = { &MainDef, 0, MISC_ChallengesStatsDummyMenu, - BASEVIDWIDTH/2, 30, + BASEVIDWIDTH/2, 11, 0, 0, 0, "UNLOCK", From 911588a5fec981ce9444afcabaca830ffa7f6f69 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 29 Sep 2023 00:25:51 +0100 Subject: [PATCH 14/98] Challenges Tutorial messages Currently exists for: - Generating a Chao Key - Attempting to use a Chao Key on a Major Challenge - Only shows after Generating a Chao Key just so it doesn't fire before you know what's going on Text is preliminary --- src/g_game.c | 9 +++++ src/k_menu.h | 2 +- src/k_menudraw.c | 2 +- src/m_cond.c | 2 + src/m_cond.h | 2 + src/menus/extras-challenges.c | 74 ++++++++++++++++++++++++++++++++++- 6 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index 3474d6ca9..ad519524d 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4390,12 +4390,15 @@ void G_LoadGameSettings(void) #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual #define GD_VERSIONMINOR 7 // Change every format update +// You can't rearrange these without a special format update typedef enum { GDEVER_ADDON = 1, GDEVER_CREDITS = 1<<1, GDEVER_REPLAY = 1<<2, GDEVER_SPECIAL = 1<<3, + GDEVER_KEYTUTORIAL = 1<<4, + GDEVER_KEYMAJORSKIP = 1<<5, } gdeverdone_t; static const char *G_GameDataFolder(void) @@ -4528,6 +4531,8 @@ void G_LoadGameData(void) gamedata->everfinishedcredits = !!(everflags & GDEVER_CREDITS); gamedata->eversavedreplay = !!(everflags & GDEVER_REPLAY); gamedata->everseenspecial = !!(everflags & GDEVER_SPECIAL); + gamedata->chaokeytutorial = !!(everflags & GDEVER_KEYTUTORIAL); + gamedata->majorkeyskipattempted = !!(everflags & GDEVER_KEYMAJORSKIP); } else { @@ -5206,6 +5211,10 @@ void G_SaveGameData(void) everflags |= GDEVER_REPLAY; if (gamedata->everseenspecial) everflags |= GDEVER_SPECIAL; + if (gamedata->chaokeytutorial) + everflags |= GDEVER_KEYTUTORIAL; + if (gamedata->majorkeyskipattempted) + everflags |= GDEVER_KEYMAJORSKIP; WRITEUINT32(save.p, everflags); // 4 } diff --git a/src/k_menu.h b/src/k_menu.h index c6cda78d6..b154332be 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1259,7 +1259,7 @@ extern struct challengesmenu_s { boolean pending; boolean requestnew; - boolean chaokeyadd; + boolean chaokeyadd, keywasadded; UINT8 chaokeyhold; boolean requestflip; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 681be183a..19c0815a3 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5914,7 +5914,7 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) 24 << FRACBITS, 16 << FRACBITS, 0, kp_button_c[1], - M_MenuExtraHeld(pid) + menumessage.active == false && M_MenuExtraHeld(pid) == true ); // Metyr of rounds played that contribute to Chao Key generation diff --git a/src/m_cond.c b/src/m_cond.c index 90faf6298..38bebcc28 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -658,6 +658,8 @@ void M_ClearStats(void) gamedata->eversavedreplay = false; gamedata->everseenspecial = false; gamedata->evercrashed = false; + gamedata->chaokeytutorial = false; + gamedata->majorkeyskipattempted = false; gamedata->musicstate = GDMUSIC_NONE; gamedata->importprofilewins = false; diff --git a/src/m_cond.h b/src/m_cond.h index fb4d00195..b34ea5e0f 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -311,6 +311,8 @@ struct gamedata_t boolean eversavedreplay; boolean everseenspecial; boolean evercrashed; + boolean chaokeytutorial; + boolean majorkeyskipattempted; gdmusic_t musicstate; // BACKWARDS COMPAT ASSIST diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index c0cd98b2e..779838056 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -308,6 +308,7 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) challengesmenu.requestflip = false; challengesmenu.requestnew = false; challengesmenu.chaokeyadd = false; + challengesmenu.keywasadded = false; challengesmenu.chaokeyhold = 0; challengesmenu.currentunlock = MAXUNLOCKABLES; challengesmenu.unlockcondition = NULL; @@ -392,6 +393,55 @@ static boolean M_CanKeyHiliTile(void) return true; } +enum { + CCTUTORIAL_KEYGEN = 0, + CCTUTORIAL_MAJORSKIP, +} cctutorial_e; + +static void M_ChallengesTutorial(UINT8 option) +{ + switch (option) + { + case CCTUTORIAL_KEYGEN: + { + M_StartMessage("Challenges & Chao Keys", + va(M_GetText( + "You just generated a Chao Key!\n" + "\n" + "They can be used to skip your way past\n" + "any Challenges you can see a hint for.\n" + "\n" + "But use them wisely - it'll take\n" + "%u rounds to pick up another.\n" + ), GDCONVERT_ROUNDSTOKEY + ), NULL, MM_NOTHING, NULL, NULL); + gamedata->chaokeytutorial = true; + break; + } + case CCTUTORIAL_MAJORSKIP: + { + M_StartMessage("Major Challenges & Chao Keys", + M_GetText( + "The larger tiles are Major Challenges.\n" + "They are significant tests of skill.\n" + "\n" + "If you're struggling and need to skip one,\n" + "not only will it cost you 10 Chao Keys, but\n" + "every nearby Challenge must be unlocked!\n" + ), NULL, MM_NOTHING, NULL, "Wow, that's a lot"); + gamedata->majorkeyskipattempted = true; + break; + } + default: + { + M_StartMessage("M_ChallengesTutorial ERROR", + "Invalid argument!?\n", + NULL, MM_NOTHING, NULL, NULL); + break; + } + } +} + void M_ChallengesTick(void) { const UINT8 pid = 0; @@ -536,6 +586,8 @@ void M_ChallengesTick(void) if (gamedata->musicstate < GDMUSIC_KEYG) gamedata->musicstate = GDMUSIC_KEYG; + + challengesmenu.keywasadded = true; } } } @@ -652,6 +704,12 @@ void M_ChallengesTick(void) { // Play music the moment control returns. M_PlayMenuJam(); + + if (gamedata->chaokeytutorial == false + && challengesmenu.keywasadded == true) + { + M_ChallengesTutorial(CCTUTORIAL_KEYGEN); + } } } } @@ -671,7 +729,15 @@ boolean M_ChallengesInputs(INT32 ch) } else if (M_MenuExtraPressed(pid)) { - if (M_CanKeyHiliTile()) + if (gamedata->chaokeytutorial == true + && gamedata->majorkeyskipattempted == false + && challengesmenu.currentunlock < MAXUNLOCKABLES + && gamedata->unlocked[challengesmenu.currentunlock] == false + && unlockables[challengesmenu.currentunlock].majorunlock == true) + { + M_ChallengesTutorial(CCTUTORIAL_MAJORSKIP); + } + else if (M_CanKeyHiliTile()) { challengesmenu.chaokeyhold = 1; } @@ -696,6 +762,12 @@ boolean M_ChallengesInputs(INT32 ch) { gamedata->chaokeys++; challengesmenu.unlockcount[CMC_CHAOANIM]++; + + if (gamedata->chaokeytutorial == false) + { + M_ChallengesTutorial(CCTUTORIAL_KEYGEN); + } + S_StartSound(NULL, sfx_dbgsal); return true; } From 5ac1854b1b76d3762d238f08da9723910feb6f16 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 29 Sep 2023 10:03:05 +0100 Subject: [PATCH 15/98] The conversion rate for Rounds to Keys is now 32:1 per popular consensus --- src/k_menudraw.c | 12 +++++++++--- src/m_cond.h | 6 +----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 19c0815a3..bbf41ddbb 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5919,16 +5919,22 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) // Metyr of rounds played that contribute to Chao Key generation { - const INT32 keybarlen = 36, keybary = 28; + const INT32 keybarlen = 32, keybary = 28; offs = keybarlen; if (gamedata->chaokeys < GDMAX_CHAOKEYS) + { + #if (GDCONVERT_ROUNDSTOKEY != 32) offs = ((gamedata->pendingkeyroundoffset * keybarlen)/GDCONVERT_ROUNDSTOKEY); + #else + offs = gamedata->pendingkeyroundoffset; + #endif + } if (offs > 0) - V_DrawFill(1, keybary, offs, 1, 0); + V_DrawFill(1+2, keybary, offs, 1, 0); if (offs < keybarlen) - V_DrawFadeFill(1+offs, keybary, keybarlen-offs, 1, 0, 31, challengetransparentstrength); + V_DrawFadeFill(1+2+offs, keybary, keybarlen-offs, 1, 0, 31, challengetransparentstrength); } // Counter diff --git a/src/m_cond.h b/src/m_cond.h index b34ea5e0f..164658427 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -240,11 +240,7 @@ typedef enum { #define GDMAX_RINGS 999999999 #define GDMAX_CHAOKEYS 9999 -#ifdef DEVELOP -#define GDCONVERT_ROUNDSTOKEY 20 -#else -#define GDCONVERT_ROUNDSTOKEY 50 -#endif +#define GDCONVERT_ROUNDSTOKEY 32 typedef enum { GDGT_RACE, From f1241d692f64f5065577226ed732363e6bf58950 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 29 Sep 2023 10:03:53 +0100 Subject: [PATCH 16/98] Improved tutorial messaging They're now called Big Challenges, because Chrome used it for space reasons and it's funny to be reminded of the Sanrio character --- src/menus/extras-challenges.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 779838056..0c25f3c5f 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -407,12 +407,10 @@ static void M_ChallengesTutorial(UINT8 option) M_StartMessage("Challenges & Chao Keys", va(M_GetText( "You just generated a Chao Key!\n" + "These can clear tough Challenges.\n" "\n" - "They can be used to skip your way past\n" - "any Challenges you can see a hint for.\n" - "\n" - "But use them wisely - it'll take\n" - "%u rounds to pick up another.\n" + "Use them wisely - it'll take\n" + "%u rounds to pick up another!\n" ), GDCONVERT_ROUNDSTOKEY ), NULL, MM_NOTHING, NULL, NULL); gamedata->chaokeytutorial = true; @@ -420,15 +418,14 @@ static void M_ChallengesTutorial(UINT8 option) } case CCTUTORIAL_MAJORSKIP: { - M_StartMessage("Major Challenges & Chao Keys", + M_StartMessage("Big Challenges & Chao Keys", M_GetText( - "The larger tiles are Major Challenges.\n" - "They are significant tests of skill.\n" + "Watch out! You need 10 Chao Keys.\n" + "to break open Big Challenge tiles.\n" "\n" - "If you're struggling and need to skip one,\n" - "not only will it cost you 10 Chao Keys, but\n" - "every nearby Challenge must be unlocked!\n" - ), NULL, MM_NOTHING, NULL, "Wow, that's a lot"); + "You'll also need to unlock\n" + "the surrounding tiles first.\n" + ), NULL, MM_NOTHING, NULL, NULL); gamedata->majorkeyskipattempted = true; break; } From c26b4036b733ec7694ef776b46775a6f78bf1ebb Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 29 Sep 2023 11:01:20 +0100 Subject: [PATCH 17/98] Challenges Menu: Subtle glowverlay on the Chao Key in the top left if you're able to skip the currently selected tile --- src/k_menu.h | 1 + src/k_menudraw.c | 25 ++++++++++++++++++------- src/menus/extras-challenges.c | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/k_menu.h b/src/k_menu.h index b154332be..f2fd07acf 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1274,6 +1274,7 @@ void M_Challenges(INT32 choice); void M_DrawChallenges(void); void M_ChallengesTick(void); boolean M_ChallengesInputs(INT32 ch); +boolean M_CanKeyHiliTile(void); typedef enum { diff --git a/src/k_menudraw.c b/src/k_menudraw.c index bbf41ddbb..1362161d5 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5234,12 +5234,6 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili colormap ); - if (challengesmenu.extradata[id].flags & CHE_ALLCLEAR) - { - // Temporary drawer for "key should be usable" - V_DrawFill(x + 5, y + 5, 2, 2, 255); - } - pat = missingpat; colormap = NULL; @@ -5572,7 +5566,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) addflags ^= V_FLIP; // This sprite is left/right flipped! } - V_DrawFixedPatch(x*FRACUNIT, (y+7)*FRACUNIT, FRACUNIT, addflags, patch, NULL); + V_DrawFixedPatch(x*FRACUNIT, (y+2)*FRACUNIT, FRACUNIT, addflags, patch, NULL); return; } @@ -6058,6 +6052,23 @@ static void M_DrawChallengeKeys(INT32 tilex, INT32 tiley) } V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, lastkeycolormap); + + // Extra glowverlay if you can use a Chao Key + if (keysbeingused == 0 && M_CanKeyHiliTile()) + { + INT32 trans = (((challengesmenu.ticker/5) % 6) - 3); + if (trans) + { + trans = ((trans < 0) + ? (10 + trans) + : (10 - trans) + ) << V_ALPHASHIFT; + + V_DrawFixedPatch(keyx, keyy, FRACUNIT, trans, key, + R_GetTranslationColormap(TC_ALLWHITE, 0, GTC_MENUCACHE) + ); + } + } } } diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 0c25f3c5f..a2830807b 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -354,7 +354,7 @@ void M_Challenges(INT32 choice) M_SetupNextMenu(&MISC_ChallengesDef, false); } -static boolean M_CanKeyHiliTile(void) +boolean M_CanKeyHiliTile(void) { // No keys to do it with? if (gamedata->chaokeys == 0) From caa8d66035c8368c70893571cc66f35d69fa39f7 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 14:19:46 +0100 Subject: [PATCH 18/98] G_DoCompleted: Refactor in anticipation of new Challenge conditions - Stop sounds first, as before. - Do most player updates before M_UpdateUnlockablesAndExtraEmblems is called - Allows us to make Challenges dependent on end-of-round Standings - Then Challenges and gamedata... - Then, group all important game state updates together. - THEN, finally assign PF_NOCONTEST when exitlevel occours. - Prevents No Contest-based Challenges from firing cheesily - Finally, Intermission-related material (also as before). --- src/g_game.c | 132 +++++++++++++++++++++++++++++----------------- src/k_grandprix.c | 10 +++- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index ad519524d..a26f8a377 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4022,48 +4022,21 @@ static void G_DoCompleted(void) { INT32 i; - if (modeattacking && pausedelay) - pausedelay = 0; - // We do this here so Challenges-related sounds aren't insta-killed S_StopSounds(); - if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) - { - if (gametype != GT_TUTORIAL) - { - UINT8 roundtype = GDGT_CUSTOM; - - if (gametype == GT_RACE) - roundtype = GDGT_RACE; - else if (gametype == GT_BATTLE) - roundtype = (battleprisons ? GDGT_PRISONS : GDGT_BATTLE); - else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) - roundtype = GDGT_SPECIAL; - - gamedata->roundsplayed[roundtype]++; - } - gamedata->pendingkeyrounds++; - - // Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve - M_UpdateUnlockablesAndExtraEmblems(true, true); - gamedata->deferredsave = true; - } - - if (gamedata->deferredsave) - G_SaveGameData(); - - legitimateexit = false; - - gameaction = ga_nothing; - - if (metalplayback) - G_StopMetalDemo(); - if (metalrecording) - G_StopMetalRecording(false); - - G_SetGamestate(GS_NULL); - wipegamestate = GS_NULL; + // First, loop over all players to: + // - fake bot results + // - set client power add + // - grand prix updates (for those who have finished) + // - for bots + // - set up difficulty increase (if applicable) + // - for humans + // - update Rings + // - award Lives + // - update over-all GP rank + // - wipe some level-only player struct data + // (The common thread is it needs to be done before Challenges updates.) for (i = 0; i < MAXPLAYERS; i++) { @@ -4074,8 +4047,7 @@ static void G_DoCompleted(void) player_t *const player = &players[i]; - // Exitlevel shouldn't get you the points - if (player->exiting == false && (player->pflags & PF_NOCONTEST) == 0) + if ((player->exiting == 0) && (player->pflags & PF_NOCONTEST) == 0) { clientPowerAdd[i] = 0; @@ -4083,13 +4055,9 @@ static void G_DoCompleted(void) { K_FakeBotResults(player); } - else - { - player->pflags |= PF_NOCONTEST; - } } - if (grandprixinfo.gp == true && grandprixinfo.wonround == true && player->exiting == true) + if (grandprixinfo.gp == true && grandprixinfo.wonround == true && player->exiting) { if (player->bot == true) { @@ -4125,11 +4093,77 @@ static void G_DoCompleted(void) G_PlayerFinishLevel(i); // take away cards and stuff } - if (automapactive) - AM_Stop(); + // Then, do gamedata-relevant material. + // This has to be done second because some Challenges + // are dependent on round standings. + if (legitimateexit && !demo.playback && !mapreset) + { + if (gametype != GT_TUTORIAL) + { + UINT8 roundtype = GDGT_CUSTOM; - prevmap = (INT16)(gamemap-1); + if (gametype == GT_RACE) + roundtype = GDGT_RACE; + else if (gametype == GT_BATTLE) + roundtype = (battleprisons ? GDGT_PRISONS : GDGT_BATTLE); + else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) + roundtype = GDGT_SPECIAL; + gamedata->roundsplayed[roundtype]++; + } + gamedata->pendingkeyrounds++; + + M_UpdateUnlockablesAndExtraEmblems(true, true); + gamedata->deferredsave = true; + } + + // This isn't in the above block because other + // mechanisms can queue up a gamedata save. + if (gamedata->deferredsave) + G_SaveGameData(); + + // Then, update some important game state. + { + legitimateexit = false; + + if (modeattacking && pausedelay) + pausedelay = 0; + + gameaction = ga_nothing; + + if (metalplayback) + G_StopMetalDemo(); + if (metalrecording) + G_StopMetalRecording(false); + + if (automapactive) + AM_Stop(); + + G_SetGamestate(GS_NULL); + wipegamestate = GS_NULL; + + prevmap = (INT16)(gamemap-1); + } + + // Finally, if you're not exiting, guarantee NO CONTEST. + // We do this seperately from the loop above Challenges, + // so NOCONTEST-related Challenges don't fire on exitlevel. + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false) + { + continue; + } + + if (players[i].exiting || (players[i].pflags & PF_NOCONTEST)) + { + continue; + } + + players[i].pflags |= PF_NOCONTEST; + } + + // And lastly, everything in anticipation for Intermission/level change. if (!demo.playback) { // Set up power level gametype scrambles diff --git a/src/k_grandprix.c b/src/k_grandprix.c index 41522f3d8..f43c82656 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -763,9 +763,15 @@ void K_FakeBotResults(player_t *bot) } } - if (besttime == UINT32_MAX // No one finished, so you don't finish either. - || bot->distancetofinish >= worstdist) // Last place, you aren't going to finish. + if (besttime == UINT32_MAX) // No one finished, so you don't finish either. { + // We don't apply PF_NOCONTEST in the exitlevel case - that's done for all players in G_DoCompleted. + return; + } + + if (bot->distancetofinish >= worstdist) // Last place, you aren't going to finish. + { + // This was a successful murder! bot->pflags |= PF_NOCONTEST; return; } From 764141946db0c2080164b19058b185d57e2feabc Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 14:23:46 +0100 Subject: [PATCH 19/98] M_GetConditionString cleanup - Remove pointless "BUILDCONDITIONTITLE" macro for M_BuildConditionTitle - Replace simple R_SkinUsable checks with M_GetConditionCharacter - Supports knowing a character's name via them being the Rival for a currently unlocked character, if that Challenge doesn't require them unlocked to interact --- src/m_cond.c | 70 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 38bebcc28..b0fb857b4 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1274,6 +1274,54 @@ static char *M_BuildConditionTitle(UINT16 map) return title; } +static const char *M_GetConditionCharacter(INT32 skin, boolean directlyrequires) +{ + // First we check for direct unlock. + boolean permitname = R_SkinUsable(-1, skin, false); + + if (permitname == false && directlyrequires == false) + { + // If there's no direct unlock, we CAN check for if the + // character is the Rival of somebody we DO have unlocked... + + UINT8 i, j; + for (i = 0; i < numskins; i++) + { + if (i == skin) + continue; + + if (R_SkinUsable(-1, i, false) == false) + continue; + + for (j = 0; j < SKINRIVALS; j++) + { + const char *rivalname = skins[i].rivals[j]; + INT32 rivalnum = R_SkinAvailable(rivalname); + + if (rivalnum != skin) + continue; + + // We can see this character as a Rival! + break; + } + + if (j == SKINRIVALS) + continue; + + // "break" our way up the nesting... + break; + } + + // We stopped before the end, we can see it! + if (i != numskins) + permitname = true; + } + + return (permitname) + ? skins[skin].realname + : "???"; +} + static const char *M_GetNthType(UINT8 position) { if (position == 1) @@ -1294,8 +1342,6 @@ static const char *M_GetConditionString(condition_t *cn) // If this function returns NULL, it stops building the condition and just does ???'s. -#define BUILDCONDITIONTITLE(i) (M_BuildConditionTitle(i)) - switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x @@ -1366,7 +1412,7 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); if (cn->type == UC_MAPSPBATTACK) prefix = (M_SecretUnlocked(SECRET_SPBATTACK, true) ? "SPB ATTACK: " : "???: "); @@ -1393,7 +1439,7 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->extrainfo1 >= nummapheaders || !mapheaderinfo[cn->extrainfo1]) return va("INVALID MAP CONDITION \"%d:%d:%d\"", cn->type, cn->extrainfo1, cn->requirement); - title = BUILDCONDITIONTITLE(cn->extrainfo1); + title = M_BuildConditionTitle(cn->extrainfo1); work = va("beat %s in %i:%02i.%02i", title, G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), @@ -1407,9 +1453,7 @@ static const char *M_GetConditionString(condition_t *cn) { if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d:%d\"", cn->type, cn->requirement, cn->extrainfo1); - work = (R_SkinUsable(-1, cn->requirement, false)) - ? skins[cn->requirement].realname - : "???"; + work = M_GetConditionCharacter(cn->requirement, true); return va("win %d Round%s as %s", cn->extrainfo1, cn->extrainfo1 == 1 ? "" : "s", @@ -1465,7 +1509,7 @@ static const char *M_GetConditionString(condition_t *cn) if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) return va("INVALID MEDAL MAP \"%d:%d\"", cn->requirement, checkLevel); - title = BUILDCONDITIONTITLE(checkLevel); + title = M_BuildConditionTitle(checkLevel); switch (emblemlocations[i].type) { case ET_MAP: @@ -1600,7 +1644,7 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\":", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); work = va("%s:", title); Z_Free(title); return work; @@ -1608,16 +1652,14 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); work = va("on %s", title); Z_Free(title); return work; case UCRP_ISCHARACTER: if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); - work = (R_SkinUsable(-1, cn->requirement, false)) - ? skins[cn->requirement].realname - : "???"; + work = M_GetConditionCharacter(cn->requirement, true); return va("as %s", work); case UCRP_ISENGINECLASS: return va("with engine class %c", 'A' + cn->requirement); @@ -1757,8 +1799,6 @@ static const char *M_GetConditionString(condition_t *cn) } // UC_MAPTRIGGER and UC_CONDITIONSET are explicitly very hard to support proper descriptions for return va("UNSUPPORTED CONDITION \"%d\"", cn->type); - -#undef BUILDCONDITIONTITLE } char *M_BuildConditionSetString(UINT16 unlockid) From b937b1a7bc4ecf1c16173cfd1630346b8a503fdb Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 14:36:00 +0100 Subject: [PATCH 20/98] Add UCRP_MAKERETIRE - `Condition1 = MakeRetire Eggrobo` - Fires when: - Not a cooperative context - You've finished in good standing - Another player has both - PF_NOCONTEST - The skin specified in the condition Also makes rivalname-handling for K_InitGrandPrixBots `const char *`, since the author of this commit had to reference that code. --- src/deh_soc.c | 7 ++++++ src/k_grandprix.c | 2 +- src/m_cond.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++ src/m_cond.h | 2 ++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 76488462e..b80c6d16c 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2777,6 +2777,13 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) //PARAMCHECK(1); ty = UCRP_FINISHCOOL + offset; } + else if (fastcmp(params[0], "MAKERETIRE")) + { + PARAMCHECK(1); + ty = UCRP_MAKERETIRE; + stringvar = Z_StrDup(params[1]); + re = -1; + } else if ((offset=0) || fastcmp(params[0], "FINISHPLACE") || (++offset && fastcmp(params[0], "FINISHPLACEEXACT"))) { diff --git a/src/k_grandprix.c b/src/k_grandprix.c index f43c82656..f6520ef7a 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -217,7 +217,7 @@ void K_InitGrandPrixBots(void) for (j = 0; j < numplayers; j++) { player_t *p = &players[competitors[j]]; - char *rivalname = skins[p->skin].rivals[i]; + const char *rivalname = skins[p->skin].rivals[i]; INT32 rivalnum = R_SkinAvailable(rivalname); // Intentionally referenced before (currently dummied out) unlock check. Such a tease! diff --git a/src/m_cond.c b/src/m_cond.c index b0fb857b4..f4de68b5b 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -864,6 +864,7 @@ void M_UpdateConditionSetsPending(void) { case UC_CHARACTERWINS: case UCRP_ISCHARACTER: + case UCRP_MAKERETIRE: { cn->requirement = R_SkinAvailable(cn->stringvar); @@ -1153,6 +1154,58 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && numtargets >= maptargets); case UCRP_NOCONTEST: return (player->pflags & PF_NOCONTEST); + + case UCRP_MAKERETIRE: + { + // You can't "make" someone retire in coop. + if (K_Cooperative() == true) + { + return false; + } + + // The following is basically UCRP_FINISHCOOL, + // but without the M_NotFreePlay check since this + // condition is already dependent on other players. + if ((player->exiting + && !(player->pflags & PF_NOCONTEST) + && !K_IsPlayerLosing(player)) == false) + { + return false; + } + + UINT8 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false) + { + continue; + } + + // This player is ME! + if (player == players+i) + { + continue; + } + + // This player didn't NO CONTEST. + if (!(players[i].pflags & PF_NOCONTEST)) + { + continue; + } + + // This player doesn't have the right skin. + if (players[i].skin != cn->requirement) + { + continue; + } + + // Okay, the right player is dead! + break; + } + + return (i != MAXPLAYERS); + } + case UCRP_FINISHPLACE: return (player->exiting && !(player->pflags & PF_NOCONTEST) @@ -1748,6 +1801,16 @@ static const char *M_GetConditionString(condition_t *cn) return "break every prison"; case UCRP_NOCONTEST: return "NO CONTEST"; + + case UCRP_MAKERETIRE: + { + if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) + return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); + + work = M_GetConditionCharacter(cn->requirement, false); + return va("make %s retire", work); + } + case UCRP_FINISHPLACE: case UCRP_FINISHPLACEEXACT: return va("finish in %d%s%s", cn->requirement, M_GetNthType(cn->requirement), diff --git a/src/m_cond.h b/src/m_cond.h index 164658427..e8670888f 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -89,6 +89,8 @@ typedef enum UCRP_FINISHALLPRISONS, // Break all prisons UCRP_NOCONTEST, // No Contest + UCRP_MAKERETIRE, // Make another player of [skin] No Contest + UCRP_FINISHPLACE, // Finish at least [place] UCRP_FINISHPLACEEXACT, // Finish at [place] exactly From 8bb41b787b9cc7d00864594de664e2eedef0a7fa Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 18:12:32 +0100 Subject: [PATCH 21/98] Fix SECRET_SKIN locks preventing Rivals from showing up Previously, there was a permanent exception for Eggrobo (the default bot skin). Now that exception is generalised for the specific skin the bot is being assigned, which we assume is intentful and correct. In addition, the randomclass PR_BOTS now controls K_RetireBots as well, matching the other two random calls done for bot skins. --- src/d_clisrv.c | 4 ++-- src/g_demo.c | 2 +- src/k_bot.cpp | 2 +- src/k_grandprix.c | 4 +++- src/r_skins.c | 8 ++++---- src/r_skins.h | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 0a2cbf53d..e21da3b4d 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -917,7 +917,7 @@ static boolean CL_SendJoin(void) for (; i < MAXSPLITSCREENPLAYERS; i++) strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME); - memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, false), MAXAVAILABILITY*sizeof(UINT8)); + memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, -1), MAXAVAILABILITY*sizeof(UINT8)); // Don't leak old signatures from prior sessions. memset(&netbuffer->u.clientcfg.challengeResponse, 0, sizeof(((clientconfig_pak *)0)->challengeResponse)); @@ -4332,7 +4332,7 @@ boolean SV_SpawnServer(void) // strictly speaking, i'm not convinced the following is necessary // but I'm not confident enough to remove it entirely in case it breaks something { - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, false); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, -1); SINT8 node = 0; for (; node < MAXNETNODES; node++) result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, diff --git a/src/g_demo.c b/src/g_demo.c index 95f5a0ac3..2e23d698d 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -2236,7 +2236,7 @@ static void G_SaveDemoSkins(UINT8 **pp) { char skin[16]; UINT8 i; - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true, false); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true, -1); WRITEUINT8((*pp), numskins); for (i = 0; i < numskins; i++) diff --git a/src/k_bot.cpp b/src/k_bot.cpp index 440a646d0..5ab7d74e9 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -59,7 +59,7 @@ void K_SetBot(UINT8 newplayernum, UINT8 skinnum, UINT8 difficulty, botStyle_e st playernode[newplayernum] = servernode; // this will permit unlocks - memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, true), MAXAVAILABILITY*sizeof(UINT8)); + memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8)); players[newplayernum].splitscreenindex = 0; players[newplayernum].bot = true; diff --git a/src/k_grandprix.c b/src/k_grandprix.c index f6520ef7a..9f2ed102f 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -714,11 +714,13 @@ void K_RetireBots(void) if (usableskins > 0) { - UINT8 index = P_RandomKey(PR_RULESCRAMBLE, usableskins); + UINT8 index = P_RandomKey(PR_BOTS, usableskins); skinnum = grabskins[index]; grabskins[index] = grabskins[--usableskins]; } + memcpy(&bot->availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8)); + bot->botvars.difficulty = newDifficulty; bot->botvars.diffincrease = 0; diff --git a/src/r_skins.c b/src/r_skins.c index 30c4fa487..0ae7cd9a9 100644 --- a/src/r_skins.c +++ b/src/r_skins.c @@ -185,13 +185,13 @@ void R_InitSkins(void) M_UpdateConditionSetsPending(); } -UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots) +UINT8 *R_GetSkinAvailabilities(boolean demolock, INT32 botforcecharacter) { UINT16 i; UINT8 shif, byte; INT32 skinid; static UINT8 responsebuffer[MAXAVAILABILITY]; - UINT8 defaultbotskin = R_BotDefaultSkin(); + const boolean forbots = (botforcecharacter != -1); memset(&responsebuffer, 0, sizeof(responsebuffer)); @@ -206,7 +206,7 @@ UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots) continue; if ((forbots - ? (M_CheckNetUnlockByID(i) || skinid == defaultbotskin) // Assert the host's lock. + ? (M_CheckNetUnlockByID(i) || skinid == botforcecharacter) // Assert the host's lock. : gamedata->unlocked[i]) // Assert the local lock. != true && !demolock) continue; @@ -451,7 +451,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum) if (P_IsLocalPlayer(player)) CONS_Alert(CONS_WARNING, M_GetText("Requested skin %d not found\n"), skinnum); - else if(server || IsPlayerAdmin(consoleplayer)) + else if (server || IsPlayerAdmin(consoleplayer)) CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum); SetSkin(player, GetPlayerDefaultSkin(playernum)); // not found put the eggman skin diff --git a/src/r_skins.h b/src/r_skins.h index 9a1c45ce6..89f1c2b91 100644 --- a/src/r_skins.h +++ b/src/r_skins.h @@ -113,7 +113,7 @@ void R_PatchSkins(UINT16 wadnum, boolean mainfile); // Access INT32 R_SkinAvailable(const char *name); boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins); -UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots); +UINT8 *R_GetSkinAvailabilities(boolean demolock, INT32 botforcecharacter); // Setting void SetPlayerSkin(INT32 playernum,const char *skinname); From 21a4e7ae43515c3fd172e15b58deb71740566c64 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 22:57:39 +0100 Subject: [PATCH 22/98] Remove last vestiges of Emerald Hunt behaviour from vanilla SRB2 Was in places the author of this commit needed to look --- src/doomstat.h | 2 -- src/g_game.c | 5 ----- src/p_setup.c | 3 --- 3 files changed, 10 deletions(-) diff --git a/src/doomstat.h b/src/doomstat.h index 3dd1eb1a4..fd5527266 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -763,8 +763,6 @@ extern UINT8 useSeal; extern UINT8 use1upSound; extern UINT8 maxXtraLife; // Max extra lives from rings -extern mobj_t *hunt1, *hunt2, *hunt3; // Emerald hunt locations - struct exitcondition_t { boolean losing; diff --git a/src/g_game.c b/src/g_game.c index a26f8a377..7a1962946 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -270,11 +270,6 @@ UINT8 introtoplay; UINT8 creditscutscene; UINT8 useSeal = 1; -// Emerald locations -mobj_t *hunt1; -mobj_t *hunt2; -mobj_t *hunt3; - tic_t racecountdown, exitcountdown, musiccountdown; // for racing exitcondition_t g_exit; diff --git a/src/p_setup.c b/src/p_setup.c index 0dc168034..94d5e84d7 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -7548,9 +7548,6 @@ static void P_InitLevelSettings(void) nummapspraycans = 0; - // emerald hunt - hunt1 = hunt2 = hunt3 = NULL; - // circuit, race and competition stuff numcheatchecks = 0; timeinmap = 0; From 0b1be76442384e4ba9c62a31d939b6ed143caa8b Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 23:00:32 +0100 Subject: [PATCH 23/98] K_FollowerHornTaunt: fix mistaken port priority on horns Incorrect behaviour sometimes prevented you from hearing somebody else's horn based on player slot order --- src/k_follower.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/k_follower.c b/src/k_follower.c index bed6518dc..3018566f4 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -773,12 +773,21 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim) honk->fuse = TICRATE/2; honk->renderflags |= RF_DONTDRAW; - if (P_IsDisplayPlayer(victim) || P_IsDisplayPlayer(taunter)) - S_StartSound(NULL, fl->hornsound); - honk->flags2 |= MF2_AMBUSH; } - honk->renderflags &= ~K_GetPlayerDontDrawFlag(victim); + UINT32 dontdrawflag = K_GetPlayerDontDrawFlag(victim); + + // A display player is affected! + if (dontdrawflag != 0) + { + // Only play the sound for the first seen display player + if ((honk->renderflags & RF_DONTDRAW) == RF_DONTDRAW) + { + S_StartSound(NULL, fl->hornsound); + } + + honk->renderflags &= ~dontdrawflag; + } } } From e96bae393cc66d646517534e3e1ce16d318cf5f4 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 5 Oct 2023 23:07:35 +0100 Subject: [PATCH 24/98] Ancient Shrine implementation beginning - Players will glance at Ancient Shrines near/behind them. - Look back at these Shrines to activate your follower's horn! - Currently, this even works if horns are turned off, as a sort of tutorial. - TODO: A special horn will be able to activate them... --- src/deh_tables.c | 4 +++ src/info.c | 33 +++++++++++++++++++++- src/info.h | 7 ++++- src/k_follower.c | 33 ++++++++++++++++++---- src/k_follower.h | 5 ++-- src/k_kart.c | 73 ++++++++++++++++++++++++++---------------------- src/p_mobj.c | 21 ++++++++++++++ 7 files changed, 133 insertions(+), 43 deletions(-) diff --git a/src/deh_tables.c b/src/deh_tables.c index 7a730b6ce..1d8d4eaec 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -1194,6 +1194,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi // Spray Can "S_SPRAYCAN", + // Ancient Shrine + "S_ANCIENTSHRINE", + // Chaos Emeralds "S_CHAOSEMERALD1", "S_CHAOSEMERALD2", @@ -4803,6 +4806,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_BLUEFLAG", // Blue CTF Flag "MT_EMBLEM", "MT_SPRAYCAN", + "MT_ANCIENTSHRINE", "MT_EMERALD", "MT_EMERALDSPARK", "MT_EMERALDFLARE", diff --git a/src/info.c b/src/info.c index 0a311d369..1121a075f 100644 --- a/src/info.c +++ b/src/info.c @@ -143,6 +143,8 @@ char sprnames[NUMSPRITES + 1][5] = "NCHP", // NiGHTS chip "NSTR", // NiGHTS star "EMBM", // Emblem + "SPCN", // Spray Can + "MMSH", // Ancient Shrine "EMRC", // Chaos Emeralds "SEMR", // Super Emeralds "ESPK", @@ -635,7 +637,6 @@ char sprnames[NUMSPRITES + 1][5] = "POKE", // Pokey "AUDI", // Audience members "DECO", // Old 1.0 Kart Decoratives + New misc ones - "SPCN", // Spray Can replaces all the old D00Dkart objects "SNES", // Sprites for SNES remake maps "GBAS", // Sprites for GBA remake maps "SPRS", // Sapphire Coast Spring Shell @@ -1870,6 +1871,9 @@ state_t states[NUMSTATES] = // Spray Can {SPR_SPCN, FF_ANIMATE|FF_SEMIBRIGHT, -1, {NULL}, 15, 2, S_NULL}, // S_SPRAYCAN + // Ancient Shrine + {SPR_MMSH, 0, -1, {NULL}, 0, 0, S_NULL}, // S_ANCIENTSHRINE + // Chaos Emeralds {SPR_EMRC, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_CHAOSEMERALD2}, // S_CHAOSEMERALD1 {SPR_EMRC, FF_FULLBRIGHT|FF_ADD, 1, {NULL}, 0, 0, S_CHAOSEMERALD1}, // S_CHAOSEMERALD2 @@ -8309,6 +8313,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_ANCIENTSHRINE + 2256, // doomednum + S_ANCIENTSHRINE,// spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + 48*FRACUNIT, // radius + 80*FRACUNIT, // height + 0, // display offset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_SCENERY|MF_NOGRAVITY|MF_SOLID|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, + { // MT_EMERALD -1, // doomednum S_CHAOSEMERALD1, // spawnstate diff --git a/src/info.h b/src/info.h index 1a4054638..09e79d655 100644 --- a/src/info.h +++ b/src/info.h @@ -696,6 +696,8 @@ typedef enum sprite SPR_NCHP, // NiGHTS chip SPR_NSTR, // NiGHTS star SPR_EMBM, // Emblem + SPR_SPCN, // Spray Can + SPR_MMSH, // Ancient Shrine SPR_EMRC, // Chaos Emeralds SPR_SEMR, // Super Emeralds SPR_ESPK, @@ -1188,7 +1190,6 @@ typedef enum sprite SPR_POKE, // Pokey SPR_AUDI, // Audience members SPR_DECO, // Old 1.0 Kart Decoratives + New misc ones - SPR_SPCN, // Spray Can replaces all the old D00Dkart objects SPR_SNES, // Sprites for SNES remake maps SPR_GBAS, // Sprites for GBA remake maps SPR_SPRS, // Sapphire Coast Spring Shell @@ -2352,6 +2353,9 @@ typedef enum state // Spray Can S_SPRAYCAN, + // Ancient Shrine + S_ANCIENTSHRINE, + // Chaos Emeralds S_CHAOSEMERALD1, S_CHAOSEMERALD2, @@ -5996,6 +6000,7 @@ typedef enum mobj_type MT_BLUEFLAG, // Blue CTF Flag MT_EMBLEM, MT_SPRAYCAN, + MT_ANCIENTSHRINE, MT_EMERALD, MT_EMERALDSPARK, MT_EMERALDFLARE, diff --git a/src/k_follower.c b/src/k_follower.c index 3018566f4..2f679dbe3 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -710,25 +710,46 @@ void K_HandleFollower(player_t *player) } /*-------------------------------------------------- - void K_FollowerHornTaunt(player_t *taunter, player_t *victim) + void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) See header file for description. --------------------------------------------------*/ -void K_FollowerHornTaunt(player_t *taunter, player_t *victim) +void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) { + // Basic checks if ( - (cv_karthorns.value == 0) - || taunter == NULL + taunter == NULL || victim == NULL || taunter->followerskin < 0 || taunter->followerskin >= numfollowers + ) + { + return; + } + + const follower_t *fl = &followers[taunter->followerskin]; + + // Check mystic melody special status + if (mysticmelodyspecial == true) + { + /*mysticmelodyspecial = (fl->hornsound == sfx_melody) + + if (mysticmelodyspecial == true) + { + // Todo: The rest of the owl + }*/ + } + + // More expensive checks + if ( + (cv_karthorns.value == 0 && mysticmelodyspecial == false) || (P_IsDisplayPlayer(victim) == false && cv_karthorns.value != 2) || P_MobjWasRemoved(taunter->mo) == true || P_MobjWasRemoved(taunter->follower) == true ) + { return; - - const follower_t *fl = &followers[taunter->followerskin]; + } const boolean tasteful = (taunter->karthud[khud_taunthorns] == 0); diff --git a/src/k_follower.h b/src/k_follower.h index 9da3d4e73..af66b8113 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -227,19 +227,20 @@ void K_HandleFollower(player_t *player); void K_RemoveFollower(player_t *player); /*-------------------------------------------------- - void K_FollowerHornTaunt(player_t *taunter, player_t *victim) + void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) Plays horn and spawns object (MOSTLY non-netsynced) Input Arguments:- taunter - Source player with a follower victim - Player that hears and sees the honk + mysticmelodyspecial - Special Mystic Melody behaviour Return:- None --------------------------------------------------*/ -void K_FollowerHornTaunt(player_t *taunter, player_t *victim); +void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial); #ifdef __cplusplus } // extern "C" diff --git a/src/k_kart.c b/src/k_kart.c index 4afe202ff..ec4435df2 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -2022,10 +2022,10 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) { const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)); const angle_t blindSpotSize = ANG10; // ANG5 - UINT8 i; SINT8 glanceDir = 0; SINT8 lastValidGlance = 0; - boolean podiumspecial = (K_PodiumSequence() == true && glancePlayer->nextwaypoint == NULL && glancePlayer->speed == 0); + const boolean podiumspecial = (K_PodiumSequence() == true && glancePlayer->nextwaypoint == NULL && glancePlayer->speed == 0); + boolean mysticmelodyspecial = false; if (podiumspecial) { @@ -2044,43 +2044,47 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) // See if there's any players coming up behind us. // If so, your character will glance at 'em. - for (i = 0; i < MAXPLAYERS; i++) + mobj_t *victim = NULL, *victimnext = NULL; + + for (victim = trackercap; victim; victim = victimnext) { - player_t *p; + player_t *p = victim->player; angle_t back; angle_t diff; fixed_t distance; SINT8 dir = -1; - if (!playeringame[i]) + victimnext = victim->itnext; + + if (p != NULL) { - // Invalid player - continue; + if (p == glancePlayer) + { + // FOOL! Don't glance at yerself! + continue; + } + + if (p->spectator || p->hyudorotimer > 0) + { + // Not playing / invisible + continue; + } + + if (podiumspecial && p->position >= glancePlayer->position) + { + // On the podium, only look with envy, not condesencion + continue; + } } - - p = &players[i]; - - if (p == glancePlayer) + else if (victim->type != MT_ANCIENTSHRINE) { - // FOOL! Don't glance at yerself! - continue; - } - - if (!p->mo || P_MobjWasRemoved(p->mo)) - { - // Invalid mobj - continue; - } - - if (p->spectator || p->hyudorotimer > 0) - { - // Not playing / invisible + // Ancient Shrines are a special exception to glance logic. continue; } if (!podiumspecial) { - distance = R_PointToDist2(glancePlayer->mo->x, glancePlayer->mo->y, p->mo->x, p->mo->y); + distance = R_PointToDist2(glancePlayer->mo->x, glancePlayer->mo->y, victim->x, victim->y); if (distance > maxdistance) { @@ -2088,13 +2092,9 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) continue; } } - else if (p->position >= glancePlayer->position) - { - continue; - } back = glancePlayer->mo->angle + ANGLE_180; - diff = R_PointToAngle2(glancePlayer->mo->x, glancePlayer->mo->y, p->mo->x, p->mo->y) - back; + diff = R_PointToAngle2(glancePlayer->mo->x, glancePlayer->mo->y, victim->x, victim->y) - back; if (diff > ANGLE_180) { @@ -2114,7 +2114,7 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) continue; } - if (!podiumspecial && P_CheckSight(glancePlayer->mo, p->mo) == false) + if (!podiumspecial && P_CheckSight(glancePlayer->mo, victim) == false) { // Blocked by a wall, we can't glance at 'em! continue; @@ -2129,7 +2129,14 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) if (horn == true) { - K_FollowerHornTaunt(glancePlayer, p); + if (p != NULL) + { + K_FollowerHornTaunt(glancePlayer, p, false); + } + else if (victim->type == MT_ANCIENTSHRINE) + { + mysticmelodyspecial = true; + } } } @@ -2137,7 +2144,7 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) { const boolean tasteful = (glancePlayer->karthud[khud_taunthorns] == 0); - K_FollowerHornTaunt(glancePlayer, glancePlayer); + K_FollowerHornTaunt(glancePlayer, glancePlayer, mysticmelodyspecial); if (tasteful && glancePlayer->karthud[khud_taunthorns] < 2*TICRATE) glancePlayer->karthud[khud_taunthorns] = 2*TICRATE; diff --git a/src/p_mobj.c b/src/p_mobj.c index 504077f99..eb82c9815 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -5338,6 +5338,10 @@ static boolean P_IsTrackerType(INT32 type) case MT_JAWZ: return true; + // Players need to be able to glance at the Ancient Shrines + case MT_ANCIENTSHRINE: + return true; + // Primarily for minimap data, handle with care case MT_SPB: case MT_BATTLECAPSULE: @@ -5348,6 +5352,7 @@ static boolean P_IsTrackerType(INT32 type) case MT_PLAYER: return true; + // HUD tracking case MT_OVERTIME_CENTER: case MT_MONITOR: case MT_EMERALD: @@ -13060,6 +13065,22 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) nummapspraycans++; break; } + case MT_ANCIENTSHRINE: + { + angle_t remainderangle = (mobj->angle % ANGLE_90); + + if (remainderangle) + { + // Always lock to 90 degree grid. + if (remainderangle > ANGLE_45) + mobj->angle += ANGLE_90; + mobj->angle -= remainderangle; + } + + P_SetScale(mobj, mobj->destscale = 2*mobj->scale); + + break; + } case MT_SKYBOX: { P_InitSkyboxPoint(mobj, mthing); From 8f1bea71de2af7189a6baa0f4f22793b8e2c4b2c Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 18:47:55 +0100 Subject: [PATCH 25/98] G_UpdateRecords: Do not destroy existing record data if you experience other types of modeattacking on the same map Should not have been possible to trigger with current menu setup, but was a hypothetical danger --- src/g_game.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index 7a1962946..de0711af5 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -529,20 +529,12 @@ void G_UpdateRecords(void) && (time < UINT32_MAX)) // DNF mapheaderinfo[gamemap-1]->records.time = time; } - else - { - mapheaderinfo[gamemap-1]->records.time = 0; - } if (modeattacking & ATTACKING_LAP) { if ((mapheaderinfo[gamemap-1]->records.lap == 0) || (bestlap < mapheaderinfo[gamemap-1]->records.lap)) mapheaderinfo[gamemap-1]->records.lap = bestlap; } - else - { - mapheaderinfo[gamemap-1]->records.lap = 0; - } // Check emblems when level data is updated if ((earnedEmblems = M_CheckLevelEmblems())) From 14b6c0f275c6a31fd5f0d4ba2393cf4f54419873 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 18:50:20 +0100 Subject: [PATCH 26/98] M_GetConditionString: Do not add a suffix for UC_MAPENCORE - that condition already has a guaranteed prefix saying the same thing. --- src/m_cond.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index f4de68b5b..a15f4815f 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1478,11 +1478,10 @@ static const char *M_GetConditionString(condition_t *cn) else if (cn->type == UC_MAPSPBATTACK) work = "conquer"; - work = va("%s%s %s%s", + work = va("%s%s %s", prefix, work, - title, - (cn->type == UC_MAPENCORE) ? " in Encore Mode" : ""); + title); Z_Free(title); return work; } From 66c415ea41e5b16323e3e1224b601a4f4a349b99 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 19:09:51 +0100 Subject: [PATCH 27/98] Emblem system, Spray Cans: prevent Exiting players from grabbing them Fixes an issue that as far as the author of this commit is aware was only reported on Discord --- src/p_inter.c | 21 ++++++++++++++++++--- src/p_mobj.c | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/p_inter.c b/src/p_inter.c index badd6d3a3..b76d4e4af 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -201,10 +201,19 @@ boolean P_CanPickupEmblem(player_t *player, INT32 emblemID) return false; } - if (player->bot) + if (player != NULL) { - // Your nefarious opponent puppy can't grab these for you. - return false; + if (player->bot) + { + // Your nefarious opponent puppy can't grab these for you. + return false; + } + + if (player->exiting) + { + // Yeah but YOU didn't actually do it now did you + return false; + } } return true; @@ -680,6 +689,12 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + if (player->exiting) + { + // Yeah but YOU didn't actually do it now did you + return; + } + if (!P_IsLocalPlayer(player)) { // Must be party. diff --git a/src/p_mobj.c b/src/p_mobj.c index eb82c9815..349831a23 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7230,7 +7230,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) mobj->frame &= ~FF_TRANSMASK; mobj->renderflags &= ~RF_TRANSMASK; - if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(&players[consoleplayer], mobj->health - 1)) + if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(NULL, mobj->health - 1)) { trans = tr_trans50; } From 6533e7e69f6aa060ae8fb466d3e81677654d7131 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 22:18:08 +0100 Subject: [PATCH 28/98] Consistancy(void): Use TypeIsNetSynced instead of MT_OVERLAY-specific check In addition, don't use frame - I'm pretty sure this was something we've already violated to no ill effect. --- src/d_clisrv.c | 21 ++++++++++++--------- src/p_saveg.c | 2 +- src/p_saveg.h | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index e21da3b4d..bb94c3dab 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -5966,6 +5966,9 @@ static INT16 Consistancy(void) mo = (mobj_t *)th; + if (TypeIsNetSynced(mo->type) == false) + continue; + if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY)) { ret -= mo->type; @@ -5979,7 +5982,7 @@ static INT16 Consistancy(void) ret -= mo->flags; ret += mo->flags2; ret -= mo->eflags; - if (mo->target) + if (mo->target && TypeIsNetSynced(mo->target->type)) { ret += mo->target->type; ret -= mo->target->x; @@ -5995,11 +5998,11 @@ static INT16 Consistancy(void) ret -= mo->target->state - states; ret += mo->target->tics; ret -= mo->target->sprite; - ret += mo->target->frame; + //ret += mo->target->frame; } else ret ^= 0x3333; - if (mo->tracer && mo->tracer->type != MT_OVERLAY) + if (mo->tracer && TypeIsNetSynced(mo->tracer->type)) { ret += mo->tracer->type; ret -= mo->tracer->x; @@ -6015,12 +6018,12 @@ static INT16 Consistancy(void) ret -= mo->tracer->state - states; ret += mo->tracer->tics; ret -= mo->tracer->sprite; - ret += mo->tracer->frame; + //ret += mo->tracer->frame; } else ret ^= 0xAAAA; // SRB2Kart: We use hnext & hprev very extensively - if (mo->hnext && mo->hnext->type != MT_OVERLAY) + if (mo->hnext && TypeIsNetSynced(mo->hnext->type)) { ret += mo->hnext->type; ret -= mo->hnext->x; @@ -6036,11 +6039,11 @@ static INT16 Consistancy(void) ret -= mo->hnext->state - states; ret += mo->hnext->tics; ret -= mo->hnext->sprite; - ret += mo->hnext->frame; + //ret += mo->hnext->frame; } else ret ^= 0x5555; - if (mo->hprev && mo->hprev->type != MT_OVERLAY) + if (mo->hprev && TypeIsNetSynced(mo->hprev->type)) { ret += mo->hprev->type; ret -= mo->hprev->x; @@ -6056,14 +6059,14 @@ static INT16 Consistancy(void) ret -= mo->hprev->state - states; ret += mo->hprev->tics; ret -= mo->hprev->sprite; - ret += mo->hprev->frame; + //ret += mo->hprev->frame; } else ret ^= 0xCCCC; ret -= mo->state - states; ret += mo->tics; ret -= mo->sprite; - ret += mo->frame; + //ret += mo->frame; } } } diff --git a/src/p_saveg.c b/src/p_saveg.c index 6c61e7f6a..f48fdaa3f 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -2624,7 +2624,7 @@ static UINT32 SaveSlope(const pslope_t *slope) return 0xFFFFFFFF; } -static boolean TypeIsNetSynced(mobjtype_t type) +boolean TypeIsNetSynced(mobjtype_t type) { // Ignore stationary hoops - these will be respawned from mapthings. if (type == MT_HOOP) diff --git a/src/p_saveg.h b/src/p_saveg.h index 66345367d..8657ee9d2 100644 --- a/src/p_saveg.h +++ b/src/p_saveg.h @@ -92,6 +92,8 @@ boolean P_SaveBufferFromFile(savebuffer_t *save, char const *name); void P_SaveBufferFree(savebuffer_t *save); size_t P_SaveBufferRemaining(const savebuffer_t *save); +boolean TypeIsNetSynced(mobjtype_t type); + #ifdef __cplusplus } // extern "C" #endif From fe62e76a39aed0de501b574c61edd8608e4b25bc Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 22:19:02 +0100 Subject: [PATCH 29/98] readfollower: Default horn to sfx_horn00 if an invalid enum is provided --- src/deh_soc.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/deh_soc.c b/src/deh_soc.c index b80c6d16c..c414fec9a 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3919,6 +3919,11 @@ if (!followers[numfollowers].field) \ NOSTATE(hitconfirmstate, "HITCONFIRMSTATE"); #undef NOSTATE + if (!followers[numfollowers].hornsound) + { + followers[numfollowers].hornsound = sfx_horn00; + } + CONS_Printf("Added follower '%s'\n", testname); if (followers[numfollowers].category < numfollowercategories) followercategories[followers[numfollowers].category].numincategory++; From d386800c1ed1bca270506cb7838ccd03cf5637b0 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 22:24:40 +0100 Subject: [PATCH 30/98] Ancient Shrine: Code the rest of the owl - If a follower with the specific Mystic Melody sound effect (DSMELODY) is used on the Shrine by a local player... - Activates the shrine after two seconds! - This is clientside only. - Stores on the mapheaderinfo's mapvisited record --- src/doomstat.h | 3 ++- src/info.c | 2 +- src/k_follower.c | 49 +++++++++++++++++++++++++++++++++++++++++------- src/k_menudraw.c | 6 ++++++ src/p_mobj.c | 15 +++++++++++++++ src/sounds.c | 1 + src/sounds.h | 1 + 7 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/doomstat.h b/src/doomstat.h index fd5527266..910abb18a 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -142,7 +142,8 @@ struct skinreference_t #define MV_BEATEN (1<<1) #define MV_ENCORE (1<<2) #define MV_SPBATTACK (1<<3) -#define MV_MAX (MV_VISITED|MV_BEATEN|MV_ENCORE|MV_SPBATTACK) +#define MV_MYSTICMELODY (1<<4) +#define MV_MAX (MV_VISITED|MV_BEATEN|MV_ENCORE|MV_SPBATTACK|MV_MYSTICMELODY) #define MV_FINISHNEEDED (1<<7) #define MV_PERSISTUNLOADED (MV_SPBATTACK|MV_FINISHNEEDED) diff --git a/src/info.c b/src/info.c index 1121a075f..ef194ca3f 100644 --- a/src/info.c +++ b/src/info.c @@ -8336,7 +8336,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = 0, // mass 0, // damage sfx_None, // activesound - MF_SCENERY|MF_NOGRAVITY|MF_SOLID|MF_DONTENCOREMAP, // flags + MF_NOGRAVITY|MF_SOLID|MF_DONTENCOREMAP, // flags S_NULL // raisestate }, diff --git a/src/k_follower.c b/src/k_follower.c index 2f679dbe3..499c1781f 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -729,15 +729,16 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelo const follower_t *fl = &followers[taunter->followerskin]; - // Check mystic melody special status + // Restrict mystic melody special status if (mysticmelodyspecial == true) { - /*mysticmelodyspecial = (fl->hornsound == sfx_melody) - - if (mysticmelodyspecial == true) - { - // Todo: The rest of the owl - }*/ + mysticmelodyspecial = ( + (demo.playback == false) // No downloading somebody else's replay + && (fl->hornsound == sfx_melody) // Must be the Mystic Melody + && (taunter->bot == false) // No getting your puppies to do it for you + && P_IsLocalPlayer(taunter) // Must be in your party + && !(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY) // Not already done + ); } // More expensive checks @@ -758,6 +759,40 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelo mobj_t *honk = taunter->follower->hprev; const fixed_t desiredscale = (2*taunter->mo->scale)/3; + if (mysticmelodyspecial == true) + { + mobj_t *mobj = NULL, *next = NULL; + + for (mobj = trackercap; mobj; mobj = next) + { + next = mobj->itnext; + if (mobj->type != MT_ANCIENTSHRINE) + { + // Not relevant + continue; + } + + if (P_MobjWasRemoved(mobj->tracer) == false) + { + // Already initiated + continue; + } + + // Cleverly a mobj type where TypeIsNetSynced is false + P_SetTarget(&mobj->tracer, P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_HORNCODE)); + + if (P_MobjWasRemoved(mobj->tracer) == true) + { + // Unrecoverable?! + continue; + } + + // This is a helper non-netsynced countdown + mobj->tracer->renderflags |= RF_DONTDRAW; + mobj->tracer->fuse = 2*TICRATE; + } + } + if (P_MobjWasRemoved(honk) == true) { honk = P_SpawnMobj( diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 1362161d5..6803f70f2 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6392,6 +6392,12 @@ static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y) } x -= 8; } + + if (mapheaderinfo[mapnum]->records.mapvisited & MV_MYSTICMELODY) + { + V_DrawScaledPatch(x, y, 0, W_CachePatchName("GOTMEL", PU_CACHE)); + x -= 8; + } } static void M_DrawStatsMaps(void) diff --git a/src/p_mobj.c b/src/p_mobj.c index 349831a23..ff0592b1e 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7255,6 +7255,21 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } + case MT_ANCIENTSHRINE: + { + if (P_MobjWasRemoved(mobj->tracer) == false + && mobj->tracer->fuse == 1) + { + if (!(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY)) + { + mapheaderinfo[gamemap-1]->records.mapvisited |= MV_MYSTICMELODY; + } + } + + mobj->frame = (mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY) ? 1 : 0; + + break; + } case MT_FLOATINGITEM: { P_ResetPitchRoll(mobj); diff --git a/src/sounds.c b/src/sounds.c index e802d0664..dc702c6cd 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1192,6 +1192,7 @@ sfxinfo_t S_sfx[NUMSFX] = {"clawk2", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SF_X8AWAYSOUND {"horn00", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // HORNCODE + {"melody", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // Mystic Melody {"monch", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""}, {"etexpl", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Game crash"}, diff --git a/src/sounds.h b/src/sounds.h index 7c55906c0..f4ec66986 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1262,6 +1262,7 @@ typedef enum sfx_clawk2, sfx_horn00, + sfx_melody, sfx_monch, sfx_etexpl, From ce3b6e304bf13063c5badc8f29107fde92722909 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 6 Oct 2023 22:32:43 +0100 Subject: [PATCH 31/98] Add UC_MAPMYSTICMELODY If you activate an Ancient Shrine on that level, the condition triggers and the Challenge is achieved. --- src/deh_soc.c | 3 ++- src/m_cond.c | 6 ++++++ src/m_cond.h | 1 + src/p_mobj.c | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index c414fec9a..513fd9201 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2517,7 +2517,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) else if ((offset=0) || fastcmp(params[0], "MAPVISITED") || (++offset && fastcmp(params[0], "MAPBEATEN")) || (++offset && fastcmp(params[0], "MAPENCORE")) - || (++offset && fastcmp(params[0], "MAPSPBATTACK"))) + || (++offset && fastcmp(params[0], "MAPSPBATTACK")) + || (++offset && fastcmp(params[0], "MAPMYSTICMELODY"))) { PARAMCHECK(1); ty = UC_MAPVISITED + offset; diff --git a/src/m_cond.c b/src/m_cond.c index a15f4815f..941ca1405 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1010,6 +1010,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UC_MAPBEATEN: // Requires map x to be beaten case UC_MAPENCORE: // Requires map x to be beaten in encore case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack + case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { UINT8 mvtype = MV_VISITED; if (cn->type == UC_MAPBEATEN) @@ -1018,6 +1019,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) mvtype = MV_ENCORE; else if (cn->type == UC_MAPSPBATTACK) mvtype = MV_SPBATTACK; + else if (cn->type == UC_MAPMYSTICMELODY) + mvtype = MV_MYSTICMELODY; return ((cn->requirement < nummapheaders) && (mapheaderinfo[cn->requirement]) @@ -1459,6 +1462,7 @@ static const char *M_GetConditionString(condition_t *cn) case UC_MAPBEATEN: // Requires map x to be beaten case UC_MAPENCORE: // Requires map x to be beaten in encore case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack + case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { const char *prefix = ""; @@ -1477,6 +1481,8 @@ static const char *M_GetConditionString(condition_t *cn) work = "visit"; else if (cn->type == UC_MAPSPBATTACK) work = "conquer"; + else if (cn->type == UC_MAPMYSTICMELODY) + work = "activate the ancient shrine in"; work = va("%s%s %s", prefix, diff --git a/src/m_cond.h b/src/m_cond.h index e8670888f..47140ba48 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -39,6 +39,7 @@ typedef enum UC_MAPBEATEN, // MAPBEATEN [map] UC_MAPENCORE, // MAPENCORE [map] UC_MAPSPBATTACK, // MAPSPBATTACK [map] + UC_MAPMYSTICMELODY, // MAPMYSTICMELODY [map] UC_MAPTIME, // MAPTIME [map] [time to beat, tics] UC_CHARACTERWINS, // CHARACTERWINS [character] [x rounds] diff --git a/src/p_mobj.c b/src/p_mobj.c index ff0592b1e..2fb2d4e55 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7263,6 +7263,10 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (!(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY)) { mapheaderinfo[gamemap-1]->records.mapvisited |= MV_MYSTICMELODY; + + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) + S_StartSound(NULL, sfx_ncitem); + gamedata->deferredsave = true; } } From 8fd809f8b17d799d41d60761a218802310b156b6 Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 8 Oct 2023 18:25:20 +0100 Subject: [PATCH 32/98] M_PrecacheLevelLocks(void): Implement a SECOND level of cacheing for Map and Cup unlocks Determining whether a map or cup is unlocked or not is now linear-time. - Attempted to cache SECRET_SKIN et al as well, but skins are loaded after unlocks... oh well. - Was staring down the barrel of a triple-nested loop for implementing SECRET_ALTMUSIC, so did this first to set good precedent. --- src/dehacked.c | 1 + src/doomstat.h | 3 +++ src/m_cond.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++--- src/p_setup.c | 2 ++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/dehacked.c b/src/dehacked.c index 66567fb05..bc3e3ae0a 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -491,6 +491,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) cup = Z_Calloc(sizeof (cupheader_t), PU_STATIC, NULL); cup->id = numkartcupheaders; cup->monitor = 1; + cup->cache_cuplock = MAXUNLOCKABLES; deh_strlcpy(cup->name, word2, sizeof(cup->name), va("Cup header %s: name", word2)); cup->namehash = hash; diff --git a/src/doomstat.h b/src/doomstat.h index 910abb18a..d62cd04a0 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -408,6 +408,8 @@ struct cupheader_t boolean playcredits; ///< Play the credits? + UINT16 cache_cuplock; ///< Cached Unlockable ID + cupwindata_t windata[4]; ///< Data for cup visitation cupheader_t *next; ///< Next cup in linked list }; @@ -534,6 +536,7 @@ struct mapheader_t UINT32 _saveid; ///< Purely assistive in gamedata save processes UINT16 cache_spraycan; ///< Cached Spraycan ID + UINT16 cache_maplock; ///< Cached Unlockable ID // Lua information UINT8 numCustomOptions; ///< Internal. For Lua custom value support. diff --git a/src/m_cond.c b/src/m_cond.c index 941ca1405..802f43aa5 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -684,7 +684,16 @@ void M_ClearSecrets(void) { if (!mapheaderinfo[i]) continue; + mapheaderinfo[i]->cache_spraycan = UINT16_MAX; + + mapheaderinfo[i]->cache_maplock = MAXUNLOCKABLES; + } + + cupheader_t *cup; + for (cup = kartcupheaders; cup; cup = cup->next) + { + cup->cache_cuplock = MAXUNLOCKABLES; } for (i = 0; i < numskincolors; i++) @@ -820,10 +829,54 @@ static void M_AssignSpraycans(void) } } +static void M_PrecacheLevelLocks(void) +{ + UINT16 i, j; + + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + switch (unlockables[i].type) + { + // SECRET_SKIN, SECRET_COLOR, SECRET_FOLLOWER are instantiated too late to use + case SECRET_MAP: + { + UINT16 map = M_UnlockableMapNum(&unlockables[i]); + if (map < nummapheaders + && mapheaderinfo[map]) + { + if (mapheaderinfo[map]->cache_maplock != MAXUNLOCKABLES) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_MAPs associated with Level %s\n", i, mapheaderinfo[map]->lumpname); + mapheaderinfo[map]->cache_maplock = i; + } + break; + } + + case SECRET_CUP: + { + cupheader_t *cup = M_UnlockableCup(&unlockables[i]); + if (cup) + { + if (cup->cache_cuplock != MAXUNLOCKABLES) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_CUPs associated with Cup %s\n", i, cup->name); + cup->cache_cuplock = i; + break; + } + break; + } + + default: + break; + } + } +} + void M_FinaliseGameData(void) { //M_PopulateChallengeGrid(); -- This can be done lazily when we actually need it + // Precache as many unlockables as is meaningfully feasible + M_PrecacheLevelLocks(); + // Place the spraycans, which CAN'T be done lazily. M_AssignSpraycans(); @@ -2341,8 +2394,6 @@ boolean M_SecretUnlocked(INT32 type, boolean local) boolean M_CupLocked(cupheader_t *cup) { - UINT16 i; - // Don't lock maps in dedicated servers. // That just makes hosts' lives hell. if (dedicated) @@ -2355,6 +2406,9 @@ boolean M_CupLocked(cupheader_t *cup) if (!cup) return false; +#if 0 // perfect uncached behaviour + UINT16 i; + for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_CUP) @@ -2363,14 +2417,16 @@ boolean M_CupLocked(cupheader_t *cup) continue; return !M_CheckNetUnlockByID(i); } +#else + if (cup->cache_cuplock < MAXUNLOCKABLES) + return !M_CheckNetUnlockByID(cup->cache_cuplock); +#endif return false; } boolean M_MapLocked(UINT16 mapnum) { - UINT16 i; - // Don't lock maps in dedicated servers. // That just makes hosts' lives hell. if (dedicated) @@ -2391,6 +2447,9 @@ boolean M_MapLocked(UINT16 mapnum) return M_CupLocked(mapheaderinfo[mapnum-1]->cup); } +#if 0 // perfect uncached behaviour + UINT16 i; + for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_MAP) @@ -2399,6 +2458,10 @@ boolean M_MapLocked(UINT16 mapnum) continue; return !M_CheckNetUnlockByID(i); } +#else + if (mapheaderinfo[mapnum-1]->cache_maplock < MAXUNLOCKABLES) + return !M_CheckNetUnlockByID(mapheaderinfo[mapnum-1]->cache_maplock); +#endif return false; } diff --git a/src/p_setup.c b/src/p_setup.c index 44ae440f0..b52c9b9a4 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -464,6 +464,8 @@ static void P_ClearSingleMapHeaderInfo(INT16 num) mapheaderinfo[num]->cache_spraycan = UINT16_MAX; + mapheaderinfo[num]->cache_maplock = MAXUNLOCKABLES; + mapheaderinfo[num]->customopts = NULL; mapheaderinfo[num]->numCustomOptions = 0; } From d784f6ad4abc58af2e4346bc424df91112eb716d Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 8 Oct 2023 21:22:58 +0100 Subject: [PATCH 33/98] SECRET_ALTMUSIC Restricts a map's alt music, in order of music definition. Supports out-of-order discoveries! --- src/deh_soc.c | 4 ++++ src/doomstat.h | 15 ++++++++------- src/m_cond.c | 31 +++++++++++++++++++++++++++++-- src/m_cond.h | 1 + src/p_setup.c | 28 +++++++++++++++++++++++++--- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 513fd9201..d8adff53e 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -1219,6 +1219,8 @@ void readlevelheader(MYFILE *f, char * name) break; deh_strlcpy(mapheaderinfo[num]->musname[j], tmp, sizeof(mapheaderinfo[num]->musname[j]), va("Level header %d: music", num)); + if (j) + mapheaderinfo[num]->cache_muslock[j - 1] = MAXUNLOCKABLES; j++; } while ((tmp = strtok(NULL,",")) != NULL); @@ -2354,6 +2356,8 @@ void readunlockable(MYFILE *f, INT32 num) unlockables[num].type = SECRET_CUP; else if (fastcmp(word2, "MAP")) unlockables[num].type = SECRET_MAP; + else if (fastcmp(word2, "ALTMUSIC")) + unlockables[num].type = SECRET_ALTMUSIC; else if (fastcmp(word2, "SKIN")) unlockables[num].type = SECRET_SKIN; else if (fastcmp(word2, "FOLLOWER")) diff --git a/src/doomstat.h b/src/doomstat.h index d62cd04a0..d39dfba28 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -495,13 +495,14 @@ struct mapheader_t char relevantskin[SKINNAMESIZE+1]; ///< Skin to use for tutorial (if not provided, uses Eggman.) // Music information - char musname[MAXMUSNAMES][7]; ///< Music tracks to play. First dimension is the track number, second is the music string. "" for no music. - char associatedmus[MAXMUSNAMES][7]; ///< Associated music tracks for sound test unlock. - char positionmus[7]; ///< Custom Position track. Doesn't play in Encore or other fun game-controlled contexts - UINT8 musname_size; ///< Number of music tracks defined - UINT8 associatedmus_size; ///< Number of associated music tracks defined - UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore. - UINT32 muspos; ///< Music position to jump to. + char musname[MAXMUSNAMES][7]; ///< Music tracks to play. First dimension is the track number, second is the music string. "" for no music. + UINT16 cache_muslock[MAXMUSNAMES-1]; ///< Cached Alt Music IDs + char associatedmus[MAXMUSNAMES][7]; ///< Associated music tracks for sound test unlock. + char positionmus[7]; ///< Custom Position track. Doesn't play in Encore or other fun game-controlled contexts + UINT8 musname_size; ///< Number of music tracks defined + UINT8 associatedmus_size; ///< Number of associated music tracks defined + UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore. + UINT32 muspos; ///< Music position to jump to. // Sky information UINT8 weather; ///< See preciptype_t diff --git a/src/m_cond.c b/src/m_cond.c index 802f43aa5..53d199d98 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -679,7 +679,7 @@ void M_ClearSecrets(void) gamedata->numspraycans = 0; gamedata->gotspraycans = 0; - UINT16 i; + UINT16 i, j; for (i = 0; i < nummapheaders; i++) { if (!mapheaderinfo[i]) @@ -688,6 +688,11 @@ void M_ClearSecrets(void) mapheaderinfo[i]->cache_spraycan = UINT16_MAX; mapheaderinfo[i]->cache_maplock = MAXUNLOCKABLES; + + for (j = 1; j < mapheaderinfo[i]->musname_size; j++) + { + mapheaderinfo[i]->cache_muslock[j-1] = MAXUNLOCKABLES; + } } cupheader_t *cup; @@ -851,6 +856,28 @@ static void M_PrecacheLevelLocks(void) break; } + case SECRET_ALTMUSIC: + { + UINT16 map = M_UnlockableMapNum(&unlockables[i]); + if (map < nummapheaders + && mapheaderinfo[map]) + { + for (j = 1; j < mapheaderinfo[map]->musname_size; j++) + { + if (mapheaderinfo[map]->cache_muslock[j - 1] != MAXUNLOCKABLES) + { + continue; + } + + mapheaderinfo[map]->cache_muslock[j - 1] = i; + break; + } + if (j == mapheaderinfo[map]->musname_size) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_ALTMUSICs associated with Level %s\n", i, mapheaderinfo[map]->lumpname); + } + break; + } + case SECRET_CUP: { cupheader_t *cup = M_UnlockableCup(&unlockables[i]); @@ -2712,7 +2739,7 @@ cupheader_t *M_UnlockableCup(unlockable_t *unlock) UINT16 M_UnlockableMapNum(unlockable_t *unlock) { - if (unlock->type != SECRET_MAP) + if (unlock->type != SECRET_MAP && unlock->type != SECRET_ALTMUSIC) { // This isn't a map unlockable... return NEXTMAP_INVALID; diff --git a/src/m_cond.h b/src/m_cond.h index 47140ba48..4d5fce77b 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -187,6 +187,7 @@ typedef enum // Level restrictions SECRET_CUP, // Permit access to entire cup (overrides SECRET_MAP) SECRET_MAP, // Permit access to single map + SECRET_ALTMUSIC, // Permit access to single map music track // Player restrictions SECRET_SKIN, // Permit this character diff --git a/src/p_setup.c b/src/p_setup.c index b52c9b9a4..767fcb460 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -8055,10 +8055,32 @@ static void P_InitMinimapInfo(void) void P_ResetLevelMusic(void) { + mapmusrng = 0; + if (mapheaderinfo[gamemap-1]->musname_size > 1) - mapmusrng = P_RandomKey(PR_MUSICSELECT, mapheaderinfo[gamemap-1]->musname_size); - else - mapmusrng = 0; + { + UINT8 tempmapmus[MAXMUSNAMES], tempmapmus_size = 1, i; + + tempmapmus[0] = 0; + + for (i = 1; i < mapheaderinfo[gamemap-1]->musname_size; i++) + { + if (mapheaderinfo[gamemap-1]->cache_muslock[i-1] < MAXUNLOCKABLES + && !M_CheckNetUnlockByID(mapheaderinfo[gamemap-1]->cache_muslock[i-1])) + continue; + + //CONS_Printf("TEST - %u\n", i); + + tempmapmus[tempmapmus_size++] = i; + } + + if (tempmapmus_size > 1) + { + mapmusrng = P_RandomKey(PR_MUSICSELECT, tempmapmus_size); + //CONS_Printf("Rolled position %u, maps to %u\n", mapmusrng, tempmapmus[mapmusrng]); + mapmusrng = tempmapmus[mapmusrng]; + } + } } void P_LoadLevelMusic(void) From 52706b4f6f075a5ca771c9793badf64e8bc72eda Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 9 Oct 2023 12:20:00 +0100 Subject: [PATCH 34/98] S_SoundTestDefLocked: Actually restrict tracks that should be restricted - POSITION!! music and the standard course track is always visible. - Alt Music is hidden behind the relevant SECRET_ALTMUSIC. - Associated music requires you to beat the level to listen. --- src/s_sound.c | 30 +++++++++++++++++++++++------- src/s_sound.h | 2 ++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/s_sound.c b/src/s_sound.c index d69e48e72..443ed0117 100644 --- a/src/s_sound.c +++ b/src/s_sound.c @@ -1224,7 +1224,7 @@ musicdef_t *musicdefstart = NULL; struct cursongcredit cursongcredit; // Currently displayed song credit info struct soundtest soundtest = {.tune = ""}; // Sound Test (sound test) -static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map, musicdef_t ***tail) +static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map, UINT8 altref, musicdef_t ***tail) { UINT8 i = 0; musicdef_t *def = S_FindMusicDef(musname, &i); @@ -1237,6 +1237,7 @@ static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map def->sequence.id = soundtest.sequence.id; def->sequence.map = map; + def->sequence.altref = altref; // So what we're doing here is to avoid iterating // for every insertion, we dereference the pointer @@ -1255,17 +1256,17 @@ static void S_InsertMapIntoSoundTestSequence(UINT16 map, musicdef_t ***tail) if (mapheaderinfo[map]->positionmus[0]) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->positionmus, map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->positionmus, map, 0, tail); } for (i = 0; i < mapheaderinfo[map]->musname_size; i++) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->musname[i], map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->musname[i], map, i, tail); } for (i = 0; i < mapheaderinfo[map]->associatedmus_size; i++) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->associatedmus[i], map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->associatedmus[i], map, ALTREF_REQUIRESBEATEN, tail); } } @@ -1352,6 +1353,7 @@ void S_PopulateSoundTestSequence(void) def->sequence.id = soundtest.sequence.id; def->sequence.map = NEXTMAP_INVALID; + def->sequence.altref = 0; def->sequence.next = soundtest.sequence.next; soundtest.sequence.next = def; @@ -1369,6 +1371,7 @@ void S_PopulateSoundTestSequence(void) def->sequence.id = soundtest.sequence.id; def->sequence.map = NEXTMAP_INVALID; + def->sequence.altref = 0; def->sequence.next = *tail; *tail = def; @@ -1380,14 +1383,27 @@ static boolean S_SoundTestDefLocked(musicdef_t *def) { // temporary - i'd like to find a way to conditionally hide // specific musicdefs that don't have any map associated. - if (def->sequence.map >= nummapheaders) + if (def->sequence.map >= nummapheaders || !mapheaderinfo[def->sequence.map]) return false; + mapheader_t *header = mapheaderinfo[def->sequence.map]; + // Is the level tied to SP progression? - if ((mapheaderinfo[def->sequence.map]->menuflags & (LF2_FINISHNEEDED|LF2_HIDEINMENU)) - && !(mapheaderinfo[def->sequence.map]->records.mapvisited & MV_BEATEN)) + if (( + (header->menuflags & (LF2_FINISHNEEDED|LF2_HIDEINMENU)) + || (def->sequence.altref == ALTREF_REQUIRESBEATEN) // Associated music only when completed + ) + && !(header->records.mapvisited & MV_BEATEN)) return true; + if (def->sequence.altref != 0 && def->sequence.altref < header->musname_size) + { + // Alt music requires unlocking the alt + if ((header->cache_muslock[def->sequence.altref - 1] < MAXUNLOCKABLES) + && gamedata->unlocked[header->cache_muslock[def->sequence.altref - 1]] == false) + return true; + } + // Finally, do a full-fat map check. return M_MapLocked(def->sequence.map+1); } diff --git a/src/s_sound.h b/src/s_sound.h index 7840596ad..bb20f8994 100644 --- a/src/s_sound.h +++ b/src/s_sound.h @@ -144,11 +144,13 @@ boolean S_MusicNotInFocus(void); #define MAXDEFTRACKS 3 +#define ALTREF_REQUIRESBEATEN UINT8_MAX struct soundtestsequence_t { UINT8 id; UINT16 map; + UINT8 altref; musicdef_t *next; size_t shuffleinfo; From bddb1d576d8424638353c66e63bb6fd330e76ce5 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 9 Oct 2023 12:54:02 +0100 Subject: [PATCH 35/98] Challenges Menu: Fix copypaste error with Chao Medal fill drawer --- src/k_menudraw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 6803f70f2..9131eeb48 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6243,7 +6243,7 @@ challengedesc: 0, medalchopy << FRACBITS, BASEVIDWIDTH << FRACBITS, - (medalchopy + challengesmenu.unlockcount[CMC_MEDALBLANK]) << FRACBITS, + (medalchopy + challengesmenu.unlockcount[i]) << FRACBITS, 0 ); From 46dcfe00004a64a7ae3908539c8be95fe11a0daf Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 9 Oct 2023 13:01:25 +0100 Subject: [PATCH 36/98] Experiment for Challenges menu: The completion percentage only counts unlocks, but the Chao Medal meter fill subtracts major unlock skips Should communicate the fact you're short of True Completion a little better. --- src/menus/extras-challenges.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index a2830807b..92b4d48ee 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -100,10 +100,14 @@ static void M_UpdateChallengeGridVisuals(void) challengesmenu.unlockcount[CMC_MEDALID] = 0; + challengesmenu.unlockcount[CMC_MEDALFILLED] = + (medalheight * ( + challengesmenu.unlockcount[CMC_UNLOCKED] + - challengesmenu.unlockcount[CMC_MAJORSKIPPED] + )) / challengesmenu.unlockcount[CMC_TOTAL]; + if (challengesmenu.unlockcount[CMC_PERCENT] == 100) { - challengesmenu.unlockcount[CMC_MEDALFILLED] = medalheight; - if (challengesmenu.unlockcount[CMC_KEYED] == 0) { challengesmenu.unlockcount[CMC_MEDALID] = 2; @@ -116,10 +120,6 @@ static void M_UpdateChallengeGridVisuals(void) } else { - challengesmenu.unlockcount[CMC_MEDALFILLED] = - (medalheight * challengesmenu.unlockcount[CMC_UNLOCKED]) - /challengesmenu.unlockcount[CMC_TOTAL]; - if (challengesmenu.unlockcount[CMC_MEDALFILLED] == 0 && challengesmenu.unlockcount[CMC_UNLOCKED] != 0) { // Cheat to give you a sliver of pixel. From 2cc796a042811edd7877425b326fda1009e63a47 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 11 Oct 2023 16:11:14 +0100 Subject: [PATCH 37/98] M_AssignSprayCans: Don't handle the prepended cans in reverse order Was messing around with part-duplicating this code for Prison Egg CDs, and realised a corruption/overwriting bug if you had more freebie Spray Cans than the standard ones. --- src/m_cond.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 53d199d98..330b65c5a 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -806,14 +806,15 @@ static void M_AssignSpraycans(void) M_Shuffle_UINT16(tempcanlist + prependoffset - (prependlen - 1), prependlen); // Put at the front of the main list - // (technically reverses the prepend order, but it + // (technically messes with the main order, but it // was LITERALLY just shuffled so it doesn't matter) - while (prependlen) + i = 0; + while (i < prependlen) { - prependlen--; - tempcanlist[listlen] = tempcanlist[prependlen]; - tempcanlist[prependlen] = tempcanlist[prependoffset - prependlen]; + tempcanlist[listlen] = tempcanlist[i]; + tempcanlist[i] = tempcanlist[prependoffset - i]; listlen++; + i++; } } From 71a95b30969aa9e59cbf6396228f8ac8a542ca79 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 11 Oct 2023 17:24:50 +0100 Subject: [PATCH 38/98] M_BuildConditionTitle: Support menuttl Example for menuttl "Controls": - For Tutorial levels, show "The Controls Tutorial" - Otherwise, show "CONTROLS" (allcaps) --- src/m_cond.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/m_cond.c b/src/m_cond.c index 330b65c5a..c203f923a 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1397,7 +1397,19 @@ static char *M_BuildConditionTitle(UINT16 map) || M_MapLocked(map+1)) return Z_StrDup("???"); - title = ref = G_BuildMapTitle(map+1); + if (mapheaderinfo[map]->menuttl[0]) + { + if (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) + { + // Intentionally not forced uppercase + return Z_StrDup(va("the %s Tutorial", mapheaderinfo[map]->menuttl)); + } + title = ref = Z_StrDup(mapheaderinfo[map]->menuttl); + } + else + { + title = ref = G_BuildMapTitle(map+1); + } if (!title) I_Error("M_BuildConditionTitle: out of memory"); From f8de2cfc8375e4f10d599e491fab5c56fa5ab004 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 12 Oct 2023 21:12:08 +0100 Subject: [PATCH 39/98] UC_PRISONEGGCD Gamedata minor version was updated again. (God this was a weirdly big amount of work and it's not even polished.) - Condition1 = PrisonEggCD [Level that has to be unlocked] - Approximately every 30 Prison Eggs destroyed, you get a shot at a Prison Egg Drop. - The only Prison Egg Drop implemented right now is an Alt Music CD. - Your [Wild Prize] is guaranteed to be selected only from conditions associated with levels that are unlocked! - Only spawns in Grand Prix Bonus Rounds, for netsync and game design. - The number is fuzzed. If you start the level with 0 Prison Eggs to destroy, it selects a random number of Prisons in the level to bust. - If you miss the pickup (such as into a deathpit), you'll get another shot in the immediate next Bonus Round you play. Also: - The number of Chao Keys you start your gamedata with is now part of the header file, not buried in the wiping function. - Removed the ACTUAL last object definition vestiges of the Emerald Hunt gamemode. --- src/deh_soc.c | 16 ++++ src/deh_tables.c | 9 +-- src/g_game.c | 14 +++- src/info.c | 53 ++++--------- src/info.h | 13 ++-- src/k_kart.c | 16 ++++ src/m_cond.c | 191 ++++++++++++++++++++++++++++++++++++++++++++++- src/m_cond.h | 16 ++++ src/p_floor.c | 1 - src/p_inter.c | 97 ++++++++++++++++++++++++ src/p_mobj.c | 51 +++++++++++++ src/p_saveg.c | 4 + src/sounds.c | 1 + src/sounds.h | 1 + 14 files changed, 427 insertions(+), 56 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index d8adff53e..27442f28f 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2643,6 +2643,22 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) // Force at head of the list? x1 = (params[2] && (params[2][0] == 'Y' || params[2][0] == 'T')) ? 1 : 0; } + else if (fastcmp(params[0], "PRISONEGGCD")) + { + ty = UC_PRISONEGGCD; + re = NEXTMAP_INVALID; + + if (params[1]) + { + re = G_MapNumber(params[1]); + + if (re >= nummapheaders) + { + deh_warning("Invalid level %s for condition ID %d", params[1], id+1); + return; + } + } + } else if ((offset=0) || fastcmp(params[0], "AND") || (++offset && fastcmp(params[0], "COMMA"))) { diff --git a/src/deh_tables.c b/src/deh_tables.c index fcb3b090b..970d73958 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -1218,10 +1218,8 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_EMERALDFLARE1", - // Emerald hunt shards - "S_SHRD1", - "S_SHRD2", - "S_SHRD3", + // Prison Egg Drops + "S_PRISONEGGDROP_CD", // Bubble Source "S_BUBBLES1", @@ -4841,8 +4839,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_EMERALD", "MT_EMERALDSPARK", "MT_EMERALDFLARE", - "MT_EMERHUNT", // Emerald Hunt - "MT_EMERALDSPAWN", // Emerald spawner w/ delay + "MT_PRISONEGGDROP", // Springs and others "MT_FAN", diff --git a/src/g_game.c b/src/g_game.c index ce2f31a00..fb320fc83 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4425,7 +4425,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 7 // Change every format update +#define GD_VERSIONMINOR 8 // Change every format update // You can't rearrange these without a special format update typedef enum @@ -4583,6 +4583,13 @@ void G_LoadGameData(void) save.p += 4; // no direct equivalent to matchesplayed } + // Prison Egg Pickups + if (versionMinor >= 8) + { + gamedata->thisprisoneggpickup = READUINT16(save.p); + gamedata->prisoneggstothispickup = READUINT16(save.p); + } + { // Quick & dirty hash for what mod this save file is for. UINT32 modID = READUINT32(save.p); @@ -5090,6 +5097,7 @@ void G_SaveGameData(void) (4*GDGT_MAX)+ 4+1+2+2+ 4+ + 2+2+ 4+ (MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+ 4+2); @@ -5256,6 +5264,10 @@ void G_SaveGameData(void) WRITEUINT32(save.p, everflags); // 4 } + // Prison Egg Pickups + WRITEUINT16(save.p, gamedata->thisprisoneggpickup); // 2 + WRITEUINT16(save.p, gamedata->prisoneggstothispickup); // 2 + WRITEUINT32(save.p, quickncasehash(timeattackfolder, 64)); // 4 // To save space, use one bit per collected/achieved/unlocked flag diff --git a/src/info.c b/src/info.c index 82ea4fbf4..1a8b0d47d 100644 --- a/src/info.c +++ b/src/info.c @@ -148,7 +148,9 @@ char sprnames[NUMSPRITES + 1][5] = "EMRC", // Chaos Emeralds "SEMR", // Super Emeralds "ESPK", - "SHRD", // Emerald Hunt + + // Prison Egg Drops + "ALTM", // Interactive Objects "BBLS", // water bubble source @@ -1920,10 +1922,8 @@ state_t states[NUMSTATES] = {SPR_LENS, FF_FULLBRIGHT|FF_ADD|FF_TRANS10|FF_ANIMATE|11, 8, {NULL}, 7, 1, S_GAINAX_MID2}, // S_EMERALDFLARE1 - // Emerald hunt shards - {SPR_SHRD, 0, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD1 - {SPR_SHRD, 1, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD2 - {SPR_SHRD, 2, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD3 + // Prison Egg Drops + {SPR_ALTM, 0|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_PRISONEGGDROP_CD // Bubble Source {SPR_BBLS, 0, 8, {A_BubbleSpawn}, 2048, 0, S_BUBBLES2}, // S_BUBBLES1 @@ -8482,9 +8482,9 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, - { // MT_EMERHUNT - 320, // doomednum - S_SHRD1, // spawnstate + { // MT_PRISONEGGDROP + -1, // doomednum + S_INVISIBLE, // spawnstate 1000, // spawnhealth S_NULL, // seestate sfx_None, // seesound @@ -8495,44 +8495,17 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = sfx_None, // painsound S_NULL, // meleestate S_NULL, // missilestate - S_SPRK1, // deathstate - S_NULL, // xdeathstate - sfx_cgot, // deathsound - 8, // speed - 12*FRACUNIT, // radius - 42*FRACUNIT, // height - 0, // display offset - 4, // mass - 0, // damage - sfx_None, // activesound - MF_SPECIAL|MF_NOGRAVITY, // flags - S_NULL // raisestate - }, - - { // MT_EMERALDSPAWN - 321, // doomednum - S_INVISIBLE, // spawnstate - 1000, // spawnhealth - S_NULL, // seestate - sfx_None, // seesound - 0, // reactiontime - sfx_None, // attacksound - S_NULL, // painstate - 0, // painchance - sfx_None, // painsound - S_NULL, // meleestate - S_NULL, // missilestate S_NULL, // deathstate S_NULL, // xdeathstate - sfx_None, // deathsound + sfx_s3k9c, // deathsound 0, // speed - 8, // radius - 8, // height + 65*FRACUNIT, // radius + 130*FRACUNIT, // height 0, // display offset - 10, // mass + 16, // mass 0, // damage sfx_None, // activesound - MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOSECTOR, // flags + MF_SPECIAL|MF_PICKUPFROMBELOW|MF_DONTENCOREMAP, // flags S_NULL // raisestate }, diff --git a/src/info.h b/src/info.h index 1308a9c15..f9a221151 100644 --- a/src/info.h +++ b/src/info.h @@ -703,7 +703,9 @@ typedef enum sprite SPR_EMRC, // Chaos Emeralds SPR_SEMR, // Super Emeralds SPR_ESPK, - SPR_SHRD, // Emerald Hunt + + // Prison Egg Drops + SPR_ALTM, // Interactive Objects SPR_BBLS, // water bubble source @@ -2400,10 +2402,8 @@ typedef enum state S_EMERALDFLARE1, - // Emerald hunt shards - S_SHRD1, - S_SHRD2, - S_SHRD3, + // Prison Egg Drops + S_PRISONEGGDROP_CD, // Bubble Source S_BUBBLES1, @@ -6062,8 +6062,7 @@ typedef enum mobj_type MT_EMERALD, MT_EMERALDSPARK, MT_EMERALDFLARE, - MT_EMERHUNT, // Emerald Hunt - MT_EMERALDSPAWN, // Emerald spawner w/ delay + MT_PRISONEGGDROP, // Springs and others MT_FAN, diff --git a/src/k_kart.c b/src/k_kart.c index 104c93318..be368aa19 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -267,6 +267,22 @@ void K_TimerInit(void) { K_SpawnDuelOnlyItems(); } + + if ( + battleprisons == true + && grandprixinfo.gp == true + && netgame == false + && gamedata->thisprisoneggpickup_cached != NULL + && gamedata->prisoneggstothispickup == 0 + && maptargets > 1 + ) + { + // This calculation is like this so... + // - You can't get a Prison Egg Drop on the last broken target + // - If it were 0 at minimum there'd be a slight bias towards the start of the round + // - This is bad because it benefits CD farming like in Brawl :D + gamedata->prisoneggstothispickup = 1 + M_RandomKey(maptargets - 1); + } } UINT32 K_GetPlayerDontDrawFlag(player_t *player) diff --git a/src/m_cond.c b/src/m_cond.c index c203f923a..6472f7bb6 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -679,6 +679,14 @@ void M_ClearSecrets(void) gamedata->numspraycans = 0; gamedata->gotspraycans = 0; + Z_Free(gamedata->prisoneggpickups); + gamedata->prisoneggpickups = NULL; + gamedata->numprisoneggpickups = 0; + gamedata->gettableprisoneggpickups = 0; + gamedata->thisprisoneggpickup = MAXCONDITIONSETS; + gamedata->thisprisoneggpickup_cached = NULL; + gamedata->thisprisoneggpickupgrabbed = false; + UINT16 i, j; for (i = 0; i < nummapheaders; i++) { @@ -713,7 +721,9 @@ void M_ClearSecrets(void) gamedata->pendingkeyrounds = 0; gamedata->pendingkeyroundoffset = 0; gamedata->keyspending = 0; - gamedata->chaokeys = 3; // Start with 3 !! + + gamedata->chaokeys = GDINIT_CHAOKEYS; + gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; } // For lack of a better idea on where to put this @@ -835,6 +845,171 @@ static void M_AssignSpraycans(void) } } +static void M_InitPrisonEggPickups(void) +{ + // Init ordered list of skincolors + UINT16 temppickups[MAXCONDITIONSETS]; + UINT16 listlen = 0; + + UINT32 i, j; + conditionset_t *c; + condition_t *cn; + + for (i = 0; i < MAXCONDITIONSETS; ++i) + { + // Optimisation - unlike Spray Cans, these are rebuilt every game launch/savedata wipe. + // Therefore, we don't need to re-store the ones that have been achieved. + if (gamedata->achieved[i]) + continue; + + c = &conditionSets[i]; + if (!c->numconditions) + continue; + + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + temppickups[listlen] = i; + listlen++; + break; + } + } + + if (!listlen) + { + return; + } + + // This list doesn't need to be shuffled because it's always being randomly grabbed. + // (Unlike Spray Cans, you don't know which CD you miss out on.) + + gamedata->prisoneggpickups = Z_Realloc( + gamedata->prisoneggpickups, + sizeof(UINT16) * listlen, + PU_STATIC, + NULL); + + while (gamedata->numprisoneggpickups < listlen) + { + gamedata->prisoneggpickups[gamedata->numprisoneggpickups] + = temppickups[gamedata->numprisoneggpickups]; + gamedata->numprisoneggpickups++; + } + + M_UpdateNextPrisonEggPickup(); +} + +void M_UpdateNextPrisonEggPickup(void) +{ + UINT16 i = gamedata->gettableprisoneggpickups, j, swap; + + conditionset_t *c; + condition_t *cn; + + boolean firstrun = true; + +cacheprisoneggpickup: + + // Check if the current roll is fine + gamedata->thisprisoneggpickup_cached = NULL; + if (gamedata->thisprisoneggpickup < MAXCONDITIONSETS) + { + //CONS_Printf("CACHE TEST: thisprisoneggpickup is set to %u\n", gamedata->thisprisoneggpickup); + if (gamedata->achieved[gamedata->thisprisoneggpickup] == false) + { + c = &conditionSets[gamedata->thisprisoneggpickup]; + if (c->numconditions) + { + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) + continue; + + // Good! Attach the cache. + gamedata->thisprisoneggpickup_cached = cn; + //CONS_Printf(" successfully set to cn!\n"); + break; + } + } + } + + if (gamedata->thisprisoneggpickup_cached == NULL) + { + gamedata->thisprisoneggpickup = MAXCONDITIONSETS; + gamedata->thisprisoneggpickupgrabbed = false; + } + } + + if (firstrun && gamedata->numprisoneggpickups && gamedata->thisprisoneggpickup == MAXCONDITIONSETS) + { + for (; i < gamedata->numprisoneggpickups; i++) + { + if (gamedata->achieved[gamedata->prisoneggpickups[i]] == false) + { + c = &conditionSets[gamedata->prisoneggpickups[i]]; + if (c->numconditions) + { + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + // Locked associated map? Keep in the rear end dimension! + if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) + break; // not continue intentionally + + // Okay, this should be available. + // Bring to the front! + swap = gamedata->prisoneggpickups[gamedata->gettableprisoneggpickups]; + gamedata->prisoneggpickups[gamedata->gettableprisoneggpickups] = + gamedata->prisoneggpickups[i]; + gamedata->prisoneggpickups[i] = swap; + + gamedata->gettableprisoneggpickups++; + + break; + } + + if (j < c->numconditions) + continue; + } + } + + // Fell all the way through? + // Push this all the way to the back, and lop it off! + + swap = gamedata->prisoneggpickups[gamedata->numprisoneggpickups]; + gamedata->prisoneggpickups[gamedata->numprisoneggpickups] = + gamedata->prisoneggpickups[i]; + gamedata->prisoneggpickups[i] = swap; + + gamedata->numprisoneggpickups--; + i--; // We run the loop again for this entry + } + + if (gamedata->gettableprisoneggpickups) + { + gamedata->thisprisoneggpickup = + gamedata->prisoneggpickups[ + M_RandomKey(gamedata->gettableprisoneggpickups) + ]; + + firstrun = false; + goto cacheprisoneggpickup; + } + } + + //CONS_Printf("thisprisoneggpickup = %u (MAXCONDITIONSETS is %u)\n", gamedata->thisprisoneggpickup, MAXCONDITIONSETS); +} + static void M_PrecacheLevelLocks(void) { UINT16 i, j; @@ -908,6 +1083,9 @@ void M_FinaliseGameData(void) // Place the spraycans, which CAN'T be done lazily. M_AssignSpraycans(); + // You could probably do the Prison Egg Pickups lazily, but it'd be a lagspike mid-combat. + M_InitPrisonEggPickups(); + // Don't consider loaded until it's a success! // It used to do this much earlier, but this would cause the gamedata // to save over itself when it I_Errors from corruption, which can @@ -1160,6 +1338,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return false; case UC_PASSWORD: return (cn->stringvar == NULL); + case UC_SPRAYCAN: { if (cn->requirement <= 0 @@ -1174,6 +1353,9 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (gamedata->spraycans[can_id].map < nummapheaders); } + case UC_PRISONEGGCD: + return ((gamedata->thisprisoneggpickupgrabbed == true) && (cn == gamedata->thisprisoneggpickup_cached)); + // Just for string building case UC_AND: case UC_COMMA: @@ -1748,6 +1930,7 @@ static const char *M_GetConditionString(condition_t *cn) return NULL; case UC_PASSWORD: return "enter a secret password"; + case UC_SPRAYCAN: { if (cn->requirement <= 0 @@ -1771,6 +1954,10 @@ static const char *M_GetConditionString(condition_t *cn) return va("grab %d Spray Cans", can_id + 1); } + case UC_PRISONEGGCD: + // :butterfly: "alternatively you could say 'grab a hot toooon' or 'smooth beeat'" + return "BONUS ROUND: grab a prize from a Prison Egg"; + case UC_AND: return "&"; case UC_COMMA: @@ -2161,6 +2348,8 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall) { response = M_CheckUnlockConditions(NULL); + M_UpdateNextPrisonEggPickup(); + if (gamedata->pendingkeyrounds == 0 || (gamedata->chaokeys >= GDMAX_CHAOKEYS)) { diff --git a/src/m_cond.h b/src/m_cond.h index 4d5fce77b..fedb6cd58 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -63,6 +63,8 @@ typedef enum UC_SPRAYCAN, // Grab a spraycan + UC_PRISONEGGCD, // Grab a CD from a Prison Egg + // Just for string building UC_AND, UC_COMMA, @@ -246,6 +248,9 @@ typedef enum { #define GDCONVERT_ROUNDSTOKEY 32 +#define GDINIT_CHAOKEYS 3 // Start with 3 Chao Keys !! +#define GDINIT_PRISONSTOPRIZE 30 // 30 Prison Eggs to your [Wild Prize] !! + typedef enum { GDGT_RACE, GDGT_BATTLE, @@ -287,6 +292,15 @@ struct gamedata_t UINT16 gotspraycans; candata_t* spraycans; + // PRISON EGG PICKUPS + UINT16 numprisoneggpickups; + UINT16 gettableprisoneggpickups; + UINT16 thisprisoneggpickup; + condition_t *thisprisoneggpickup_cached; + boolean thisprisoneggpickupgrabbed; + UINT16 prisoneggstothispickup; + UINT16* prisoneggpickups; + // CHALLENGE GRID UINT16 challengegridwidth; UINT16 *challengegrid; @@ -375,6 +389,8 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall); #define PENDING_CHAOKEYS (UINT16_MAX-1) UINT16 M_GetNextAchievedUnlock(boolean canskipchaokeys); +void M_UpdateNextPrisonEggPickup(void); + UINT16 M_CheckLevelEmblems(void); UINT16 M_CompletionEmblems(void); diff --git a/src/p_floor.c b/src/p_floor.c index 2d25e8208..4b96192c8 100644 --- a/src/p_floor.c +++ b/src/p_floor.c @@ -980,7 +980,6 @@ static mobj_t *SearchMarioNode(msecnode_t *node) case MT_THOK: case MT_GHOST: case MT_OVERLAY: - case MT_EMERALDSPAWN: case MT_ELEMENTAL_ORB: case MT_ATTRACT_ORB: case MT_FORCE_ORB: diff --git a/src/p_inter.c b/src/p_inter.c index 718959b1a..a94daafab 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -800,6 +800,49 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + case MT_PRISONEGGDROP: + { + if (demo.playback) + { + // Never collect emblems in replays. + return; + } + + if (player->bot) + { + // Your nefarious opponent puppy can't grab these for you. + return; + } + + if (!P_IsLocalPlayer(player)) + { + // Must be party. + return; + } + + if (special->hitlag || special->scale < mapobjectscale/2) + { + // Don't get during the initial activation + return; + } + + if ( + grandprixinfo.gp == true // Bonus Round + && netgame == false // game design + makes it easier to implement + && gamedata->thisprisoneggpickup_cached != NULL + ) + { + gamedata->thisprisoneggpickupgrabbed = true; + gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; + + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) + S_StartSound(NULL, sfx_ncitem); + gamedata->deferredsave = true; + } + + break; + } + case MT_LSZ_BUNGEE: Obj_BungeeSpecial(special, player); return; @@ -936,6 +979,11 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) K_SpawnBattlePoints(source->player, NULL, 1); } + if (gamedata->prisoneggstothispickup) + { + gamedata->prisoneggstothispickup--; + } + if (++numtargets >= maptargets) { P_DoAllPlayersExit(0, (grandprixinfo.gp == true)); @@ -948,6 +996,55 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) extratimeintics += 10*TICRATE; secretextratime = TICRATE/2; } + + if ( + grandprixinfo.gp == true // Bonus Round + && demo.playback == false // Not playback + && netgame == false // game design + makes it easier to implement + && gamedata->thisprisoneggpickup_cached != NULL + && gamedata->prisoneggstothispickup == 0 + && gamedata->thisprisoneggpickupgrabbed == false + ) + { + // Will be 0 for the next level + gamedata->prisoneggstothispickup = (maptargets - numtargets); + + mobj_t *secretpickup = P_SpawnMobj( + target->x, target->y, + target->z + ( + target->height + - FixedMul(mobjinfo[MT_PRISONEGGDROP].height, mapobjectscale) + ), + MT_PRISONEGGDROP + ); + + if (secretpickup) + { + secretpickup->hitlag = target->hitlag; + + P_SetScale(secretpickup, mapobjectscale/TICRATE); + // secretpickup->destscale = mapobjectscale; -- safe assumption it's already set? + secretpickup->scalespeed = (2*mapobjectscale)/(3*TICRATE); + + // NOT from the target - just in case it's just been placed on the ceiling as a gimmick + K_FlipFromObject(secretpickup, source); + + // Okay these have to use M_Random because replays... + // The spawning of these won't be recorded back! + const angle_t launchangle = FixedAngle(M_RandomRange(60, 80) * FRACUNIT); + const fixed_t launchmomentum = 20 * mapobjectscale; + + secretpickup->momz = P_MobjFlip(target) // THIS one uses target! + * P_ReturnThrustY(secretpickup, launchangle, launchmomentum); + + secretpickup->angle = FixedAngle(M_RandomKey(360) * FRACUNIT); + + P_InstaThrust( + secretpickup, secretpickup->angle, + P_ReturnThrustX(secretpickup, launchangle, launchmomentum) + ); + } + } } } diff --git a/src/p_mobj.c b/src/p_mobj.c index ca783b256..3c6cbffa7 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7638,6 +7638,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } case MT_EMERALD: + { Obj_EmeraldThink(mobj); if (P_MobjWasRemoved(mobj)) @@ -7645,6 +7646,56 @@ static boolean P_MobjRegularThink(mobj_t *mobj) return false; } break; + } + case MT_PRISONEGGDROP: + { + // If it gets any more complicated than this I'll make an objects/prisoneggdrop.c file, promise + // ~toast 121023 + + statenum_t teststate = S_NULL; + + if (mobj->flags2 & MF2_AMBUSH) + { + if (P_IsObjectOnGround(mobj)) + { + if (P_CheckDeathPitCollide(mobj)) + { + P_RemoveMobj(mobj); + return false; + } + + mobj->momx = mobj->momy = 0; + } + + teststate = (mobj->state-states); + } + else if (!netgame) + { + if (gamedata->thisprisoneggpickup_cached->type == UC_PRISONEGGCD) + { + teststate = S_PRISONEGGDROP_CD; + } + + P_SetMobjState(mobj, teststate); + + if (P_MobjWasRemoved(mobj)) + { + return false; + } + + S_StartSound(NULL, sfx_cdsprk); + + mobj->z += P_MobjFlip(mobj); + mobj->flags2 |= MF2_AMBUSH; + } + + if (teststate == S_PRISONEGGDROP_CD) + { + mobj->angle += ANGLE_MAX/TICRATE; + } + + break; + } case MT_EMERALDFLARE: Obj_EmeraldFlareThink(mobj); diff --git a/src/p_saveg.c b/src/p_saveg.c index 1ac21c929..523f9a5fb 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -2708,6 +2708,10 @@ boolean TypeIsNetSynced(mobjtype_t type) if (type == MT_HORNCODE) return false; + // MT_PRISONEGGDROP: Yeah these are completely local + if (type == MT_PRISONEGGDROP) + return false; + return true; } diff --git a/src/sounds.c b/src/sounds.c index afacfee07..52f44a70a 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1193,6 +1193,7 @@ sfxinfo_t S_sfx[NUMSFX] = {"horn00", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // HORNCODE {"melody", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // Mystic Melody + {"cdsprk", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // Prison Egg CD sparkling {"monch", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""}, {"etexpl", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Game crash"}, diff --git a/src/sounds.h b/src/sounds.h index ed959274a..769abc803 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1263,6 +1263,7 @@ typedef enum sfx_horn00, sfx_melody, + sfx_cdsprk, sfx_monch, sfx_etexpl, From 7d5f0ea3ba22212f7acb0b829d30588996a371ae Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 12 Oct 2023 21:12:59 +0100 Subject: [PATCH 40/98] M_DrawChallengeTile: Updates - Add SECRET_ALTMUSIC tile associations - Fix association for MISC --- src/k_menudraw.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 9131eeb48..7bb2ee4bd 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -5272,7 +5272,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili if (categoryside) { - char categoryid = '8'; + char categoryid = '0'; colormap = bgmap; switch (ref->type) { @@ -5310,6 +5310,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili case SECRET_SPBATTACK: categoryid = '7'; break; + case SECRET_ALTMUSIC: + categoryid = '9'; + break; } pat = W_CachePatchName(va("UN_RR0%c%c", categoryid, @@ -5372,6 +5375,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili case SECRET_MAP: iconid = 14; break; + case SECRET_ALTMUSIC: + iconid = 16; + break; case SECRET_HARDSPEED: iconid = 3; From 1c4750a0a212301ee7dd81e1ff1542f3f2e10e40 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 13 Oct 2023 16:21:18 +0100 Subject: [PATCH 41/98] readcondition: Support non-fragmented parameters that can be used for the Challenge Description directly - "DescriptionOverride" (new!) - Provide a full description in place of the params and this will be used - Any conditions that are before it will not be wiped, so you can prefix it if you need to - Can be used with any other set of Conditions - `Condition1 = DescriptionOverride Complete the sentence: "ring racers (???)"` - "Password" - Now supports passwords that contain spaces - `Condition1 = Password race as a ring!` - "WetPlayer" - Now supports liquids that contain spaces - HOWEVER, it comes with the following caveats as part of the change: - The strictness level must be provided first. - You can't leave the strictness out. The previous default behaviour now requires STANDARD to be explicitly written. - `Condition1 = WetPlayer Standard Mega Mack` can be used, now. --- src/deh_soc.c | 167 ++++++++++++++++++++++++++++++-------------------- src/m_cond.c | 13 +++- src/m_cond.h | 3 + 3 files changed, 114 insertions(+), 69 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 27442f28f..03f77b71c 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -72,6 +72,8 @@ fixed_t get_number(const char *word) #define PARAMCHECK(n) do { if (!params[n]) { deh_warning("Too few parameters, need %d", n); return; }} while (0) +#define EXTENDEDPARAMCHECK(spos, n) do { if (!spos || !(*spos)) { deh_warning("Missing extended parameter, need at least %d", n); return; }} while (0) + /* ======================================================================== */ // Load a dehacked file format /* ======================================================================== */ @@ -2421,43 +2423,121 @@ void readunlockable(MYFILE *f, INT32 num) Z_Free(s); } +// This is a home-grown strtok(" ") equivalent so we can isolate the first chunk without destroying the rest of the line. +static void conditiongetparam(char **params, UINT8 paramid, char **spos) +{ + if (*spos == NULL || *(*spos) == '\0') + { + params[paramid] = NULL; + return; + } + + params[paramid] = *spos; + while (*(*spos) != '\0' && *(*spos) != ' ') + { + *(*spos) = toupper(*(*spos)); + (*spos)++; + } + if (*(*spos) == ' ') + { + *(*spos) = '\0'; + (*spos)++; + + while (*(*spos) == ' ') + (*spos)++; + } +} + static void readcondition(UINT16 set, UINT32 id, char *word2) { INT32 i; - char *params[5]; // condition, requirement, extra info, extra info, stringvar - char *spos; + const UINT8 MAXCONDITIONPARAMS = 5; + char *params[MAXCONDITIONPARAMS]; // condition, requirement, extra info, extra info, stringvar + char *spos = NULL; char *stringvar = NULL; - conditiontype_t ty; + conditiontype_t ty = UC_NONE; INT32 re = 0; INT16 x1 = 0, x2 = 0; INT32 offset = 0; -#if 0 - char *endpos = word2 + strlen(word2); -#endif - - spos = strtok(word2, " "); - - for (i = 0; i < 5; ++i) + // Lop the leading spaces off + if (word2 && *word2) { - if (spos != NULL) - { - params[i] = spos; - spos = strtok(NULL, " "); - } - else - params[i] = NULL; + spos = word2; + while (*spos == ' ') + spos++; } + conditiongetparam(params, 0, &spos); + if (!params[0]) { deh_warning("condition line is empty for condition ID %d", id+1); return; } - if (fastcmp(params[0], "PLAYTIME")) + // We do free descriptions first. + + if (fastcmp(params[0], "DESCRIPTIONOVERRIDE")) + { + EXTENDEDPARAMCHECK(spos, 1); + ty = UC_DESCRIPTIONOVERRIDE; + + stringvar = Z_StrDup(spos); + } + else if (fastcmp(params[0], "PASSWORD")) + { + EXTENDEDPARAMCHECK(spos, 1); + ty = UC_PASSWORD; + + stringvar = Z_StrDup(spos); + re = -1; + } + + if (ty != UC_NONE) + goto setcondition; + + // Now conditions that take one standard param and one free description. + + conditiongetparam(params, 1, &spos); + + if (fastcmp(params[0], "WETPLAYER")) + { + PARAMCHECK(1); + //EXTENDEDPARAMCHECK(spos, 2); + ty = UCRP_WETPLAYER; + re = MFE_UNDERWATER; + x1 = 1; + + if (fastcmp(params[1], "STRICT")) + re |= MFE_TOUCHWATER; + else if (fastcmp(params[1], "STANDARD")) + ; + else + { + deh_warning("liquid strictness requirement \"%s\" invalid for condition ID %d", params[1], id+1); + return; + } + + if (spos && *spos) + stringvar = Z_StrDup(spos); + } + + if (ty != UC_NONE) + goto setcondition; + + // Now for all other conditions. + + for (i = 2; i < MAXCONDITIONPARAMS; i++) + { + conditiongetparam(params, i, &spos); + } + + if (ty != UC_NONE) + ; + else if (fastcmp(params[0], "PLAYTIME")) { PARAMCHECK(1); ty = UC_PLAYTIME + offset; @@ -2627,13 +2707,6 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) //PARAMCHECK(1); ty = UC_ADDON + offset; } - else if (fastcmp(params[0], "PASSWORD")) - { - PARAMCHECK(1); - ty = UC_PASSWORD; - stringvar = Z_StrDup(params[1]); - re = -1; - } else if (fastcmp(params[0], "SPRAYCAN")) { PARAMCHECK(1); @@ -2844,27 +2917,6 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id+1); return; } - - // The following undid the effects of strtok. - // Unfortunately, there is no way it can reasonably undo the effects of strupr. - // If we want custom descriptions for map execution triggers, we're gonna need a different method. -#if 0 - // undo affect of strtok - i = 5; - // so spos will still be the strtok from earlier - while (i >= 2) - { - if (!spos) - continue; - while (*spos != '\0') - spos++; - if (spos < endpos) - *spos = ' '; - spos = params[--i]; - } - - stringvar = Z_StrDup(params[2]); -#endif } else if ((offset=0) || fastcmp(params[0], "FALLOFF") || (++offset && fastcmp(params[0], "TOUCHOFFROAD")) @@ -2886,32 +2938,13 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) //PARAMCHECK(1); ty = UCRP_TRIPWIREHYUU + offset; } - else if (fastcmp(params[0], "WETPLAYER")) - { - PARAMCHECK(1); - ty = UCRP_WETPLAYER; - re = MFE_UNDERWATER; - x1 = 1; - - if (params[2]) - { - if (fastcmp(params[2], "STRICT")) - re |= MFE_TOUCHWATER; - else - { - deh_warning("liquid strictness requirement \"%s\" invalid for condition ID %d", params[2], id+1); - return; - } - } - - stringvar = Z_StrDup(params[1]); - } else { deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); return; } +setcondition: M_AddRawCondition(set, (UINT8)id, ty, re, x1, x2, stringvar); } @@ -2954,7 +2987,7 @@ void readconditionset(MYFILE *f, UINT16 setnum) // Now get the part after word2 = tmp += 2; - strupr(word2); + //strupr(word2); if (fastncmp(word, "CONDITION", 9)) { diff --git a/src/m_cond.c b/src/m_cond.c index 6472f7bb6..0fc90036c 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1241,6 +1241,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) { switch (cn->type) { + case UC_NONE: + return false; case UC_PLAYTIME: // Requires total playing time >= x return (gamedata->totalplaytime >= (unsigned)cn->requirement); case UC_ROUNDSPLAYED: // Requires any level completed >= x times @@ -1359,6 +1361,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) // Just for string building case UC_AND: case UC_COMMA: + case UC_DESCRIPTIONOVERRIDE: return true; case UCRP_PREFIX_GRANDPRIX: @@ -1550,7 +1553,7 @@ static boolean M_CheckConditionSet(conditionset_t *c, player_t *player) continue; // Skip entries that are JUST for string building - if (cn->type == UC_AND || cn->type == UC_COMMA) + if (cn->type == UC_AND || cn->type == UC_COMMA || cn->type == UC_DESCRIPTIONOVERRIDE) continue; lastID = cn->id; @@ -1962,6 +1965,8 @@ static const char *M_GetConditionString(condition_t *cn) return "&"; case UC_COMMA: return ","; + case UC_DESCRIPTIONOVERRIDE: + return cn->stringvar; case UCRP_PREFIX_GRANDPRIX: return "GRAND PRIX:"; @@ -2140,7 +2145,7 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", - cn->stringvar); + (cn->stringvar) ? cn->stringvar : "water"); default: break; @@ -2208,6 +2213,10 @@ char *M_BuildConditionSetString(UINT16 unlockid) stopasap = true; work = "???"; } + else if (cn->type == UC_DESCRIPTIONOVERRIDE) + { + stopasap = true; + } worklen = strlen(work); strncat(message, work, len); diff --git a/src/m_cond.h b/src/m_cond.h index fedb6cd58..d7224b563 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -28,6 +28,8 @@ extern "C" { // [required] typedef enum { + UC_NONE, + UC_PLAYTIME, // PLAYTIME [tics] UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played] UC_TOTALRINGS, // TOTALRINGS [x collected] @@ -68,6 +70,7 @@ typedef enum // Just for string building UC_AND, UC_COMMA, + UC_DESCRIPTIONOVERRIDE, UCRP_REQUIRESPLAYING, // All conditions below this can only be checked if (Playing() && gamestate == GS_LEVEL). From eeefb7beae76b08cf5996c8cbccc7f7b8075dacc Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 13 Oct 2023 18:35:07 +0100 Subject: [PATCH 42/98] Prison Egg CD drop: Visual polish - Initial launch - Spin faster - Spawn closer to the center of the Prison Egg - Pickup animation - Inspired by those star panels you had to use Flight Formation's Rocket Shoot into in Heroes - Semibright under regular conditions --- src/p_inter.c | 30 +++++++++++++++++++++++++----- src/p_mobj.c | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/p_inter.c b/src/p_inter.c index a94daafab..45fd9180c 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -826,6 +826,12 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + if (special->extravalue1) + { + // Don't get during destruction + return; + } + if ( grandprixinfo.gp == true // Bonus Round && netgame == false // game design + makes it easier to implement @@ -833,13 +839,30 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) ) { gamedata->thisprisoneggpickupgrabbed = true; - gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; + if (gamedata->prisoneggstothispickup < GDINIT_PRISONSTOPRIZE) + { + // Just in case it's set absurdly low for testing. + gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; + } if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) S_StartSound(NULL, sfx_ncitem); gamedata->deferredsave = true; } + statenum_t teststate = (special->state-states); + + if (teststate == S_PRISONEGGDROP_CD) + { + special->momz = P_MobjFlip(special) * 2 * mapobjectscale; + special->flags = (special->flags & ~MF_SPECIAL) | (MF_NOGRAVITY|MF_NOCLIPHEIGHT); + special->extravalue1 = 1; + + special->renderflags = (special->renderflags & ~RF_BRIGHTMASK) | (RF_ADD | RF_FULLBRIGHT); + + return; + } + break; } @@ -1011,10 +1034,7 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) mobj_t *secretpickup = P_SpawnMobj( target->x, target->y, - target->z + ( - target->height - - FixedMul(mobjinfo[MT_PRISONEGGDROP].height, mapobjectscale) - ), + target->z + target->height/2, MT_PRISONEGGDROP ); diff --git a/src/p_mobj.c b/src/p_mobj.c index 3c6cbffa7..89f6fa7c5 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7656,7 +7656,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (mobj->flags2 & MF2_AMBUSH) { - if (P_IsObjectOnGround(mobj)) + if (mobj->extravalue1 == 0 && P_IsObjectOnGround(mobj)) { if (P_CheckDeathPitCollide(mobj)) { @@ -7665,6 +7665,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } mobj->momx = mobj->momy = 0; + mobj->flags2 |= MF2_STRONGBOX; } teststate = (mobj->state-states); @@ -7674,6 +7675,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (gamedata->thisprisoneggpickup_cached->type == UC_PRISONEGGCD) { teststate = S_PRISONEGGDROP_CD; + mobj->renderflags |= RF_SEMIBRIGHT; } P_SetMobjState(mobj, teststate); @@ -7691,7 +7693,43 @@ static boolean P_MobjRegularThink(mobj_t *mobj) if (teststate == S_PRISONEGGDROP_CD) { - mobj->angle += ANGLE_MAX/TICRATE; + if (mobj->extravalue1) + { + ++mobj->extravalue1; + + INT32 trans = (mobj->extravalue1 * NUMTRANSMAPS) / (TICRATE); + if (trans >= NUMTRANSMAPS) + { + P_RemoveMobj(mobj); + return false; + } + + mobj->angle += ANGLE_MAX/(TICRATE/3); + mobj->renderflags = (mobj->renderflags & ~RF_TRANSMASK) | (trans << RF_TRANSSHIFT); + } + else + { + if (mobj->flags2 & MF2_STRONGBOX) + mobj->angle += ANGLE_MAX/TICRATE; + else + mobj->angle += ANGLE_MAX/(TICRATE/3); + + // Non-RNG-advancing equivalent of Obj_SpawnEmeraldSparks + if (leveltime % 3 == 0) + { + mobj_t *sparkle = P_SpawnMobjFromMobj( + mobj, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(0, 64) * FRACUNIT, + MT_SPARK + ); + P_SetMobjState(sparkle, mobjinfo[MT_EMERALDSPARK].spawnstate); + + sparkle->color = M_RandomChance(FRACUNIT/2) ? SKINCOLOR_ULTRAMARINE : SKINCOLOR_MAGENTA; + sparkle->momz += 8 * mobj->scale * P_MobjFlip(mobj); + } + } } break; From f1e3c547b34df445e483dbefdeb25bbd427c7aea Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 14 Oct 2023 18:32:28 +0100 Subject: [PATCH 43/98] M_DrawStatistics: Rename maps page to "COURSES & MEDALS" per Oni's request --- src/k_menudraw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 7bb2ee4bd..16974dc85 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -6859,7 +6859,7 @@ void M_DrawStatistics(void) case statisticspage_maps: { - pagename = "LEVELS & MEDALS"; + pagename = "COURSES & MEDALS"; M_DrawStatsMaps(); break; } From 52674c7bb2e7bf664d02996b04a3ae2f551a6212 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 14 Oct 2023 18:42:27 +0100 Subject: [PATCH 44/98] UC_UNLOCKPERCENT - `UnlockPercent 40 AltMusic` - "Get 40% of alternate music" - `UnlockPercent 1 Color` - "Get 1% of Spray Cans" - `UnlockPercent 20 Map` - "Get 20% of Courses" - `UnlockPercent 55 Cup` - "Get 55% of Cups" - `UnlockPercent 100` - "Get 100% completion" - Provide a percentage and, optionally, an Unlockable type - This only works for Unlockable Types where there are expected to be more than one per board --- src/deh_soc.c | 121 ++++++++++++++++++++++++++++++------------------- src/m_cond.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/m_cond.h | 7 ++- 3 files changed, 204 insertions(+), 47 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 03f77b71c..a0a1c5335 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2296,6 +2296,55 @@ void reademblemdata(MYFILE *f, INT32 num) Z_Free(s); } +static INT16 parseunlockabletype(char *type) +{ + if (fastcmp(type, "EXTRAMEDAL")) + return SECRET_EXTRAMEDAL; + else if (fastcmp(type, "CUP")) + return SECRET_CUP; + else if (fastcmp(type, "MAP")) + return SECRET_MAP; + else if (fastcmp(type, "ALTMUSIC")) + return SECRET_ALTMUSIC; + else if (fastcmp(type, "SKIN")) + return SECRET_SKIN; + else if (fastcmp(type, "FOLLOWER")) + return SECRET_FOLLOWER; + else if (fastcmp(type, "COLOR")) + return SECRET_COLOR; + + else if (fastcmp(type, "HARDSPEED")) + return SECRET_HARDSPEED; + else if (fastcmp(type, "MASTERMODE")) + return SECRET_MASTERMODE; + else if (fastcmp(type, "ENCORE")) + return SECRET_ENCORE; + else if (fastcmp(type, "TIMEATTACK")) + return SECRET_TIMEATTACK; + else if (fastcmp(type, "PRISONBREAK")) + return SECRET_PRISONBREAK; + else if (fastcmp(type, "SPECIALATTACK")) + return SECRET_SPECIALATTACK; + else if (fastcmp(type, "SPBATTACK")) + return SECRET_SPBATTACK; + else if (fastcmp(type, "ONLINE")) + return SECRET_ONLINE; + else if (fastcmp(type, "ADDONS")) + return SECRET_ADDONS; + else if (fastcmp(type, "EGGTV")) + return SECRET_EGGTV; + else if (fastcmp(type, "SOUNDTEST")) + return SECRET_SOUNDTEST; + else if (fastcmp(type, "ALTTITLE")) + return SECRET_ALTTITLE; + else if (fastcmp(type, "MEMETAUNTS")) + return SECRET_MEMETAUNTS; + else if (fastcmp(type, "ITEMFINDER")) + return SECRET_ITEMFINDER; + + return SECRET_NONE; +} + void readunlockable(MYFILE *f, INT32 num) { char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL); @@ -2350,52 +2399,7 @@ void readunlockable(MYFILE *f, INT32 num) unlockables[num].majorunlock = (UINT8)(i != 0 || word2[0] == 'T' || word2[0] == 'Y'); else if (fastcmp(word, "TYPE")) { - if (fastcmp(word2, "NONE")) - unlockables[num].type = SECRET_NONE; - else if (fastcmp(word2, "EXTRAMEDAL")) - unlockables[num].type = SECRET_EXTRAMEDAL; - else if (fastcmp(word2, "CUP")) - unlockables[num].type = SECRET_CUP; - else if (fastcmp(word2, "MAP")) - unlockables[num].type = SECRET_MAP; - else if (fastcmp(word2, "ALTMUSIC")) - unlockables[num].type = SECRET_ALTMUSIC; - else if (fastcmp(word2, "SKIN")) - unlockables[num].type = SECRET_SKIN; - else if (fastcmp(word2, "FOLLOWER")) - unlockables[num].type = SECRET_FOLLOWER; - else if (fastcmp(word2, "COLOR")) - unlockables[num].type = SECRET_COLOR; - else if (fastcmp(word2, "HARDSPEED")) - unlockables[num].type = SECRET_HARDSPEED; - else if (fastcmp(word2, "MASTERMODE")) - unlockables[num].type = SECRET_MASTERMODE; - else if (fastcmp(word2, "ENCORE")) - unlockables[num].type = SECRET_ENCORE; - else if (fastcmp(word2, "TIMEATTACK")) - unlockables[num].type = SECRET_TIMEATTACK; - else if (fastcmp(word2, "PRISONBREAK")) - unlockables[num].type = SECRET_PRISONBREAK; - else if (fastcmp(word2, "SPECIALATTACK")) - unlockables[num].type = SECRET_SPECIALATTACK; - else if (fastcmp(word2, "SPBATTACK")) - unlockables[num].type = SECRET_SPBATTACK; - else if (fastcmp(word2, "ONLINE")) - unlockables[num].type = SECRET_ONLINE; - else if (fastcmp(word2, "ADDONS")) - unlockables[num].type = SECRET_ADDONS; - else if (fastcmp(word2, "EGGTV")) - unlockables[num].type = SECRET_EGGTV; - else if (fastcmp(word2, "SOUNDTEST")) - unlockables[num].type = SECRET_SOUNDTEST; - else if (fastcmp(word2, "ALTTITLE")) - unlockables[num].type = SECRET_ALTTITLE; - else if (fastcmp(word2, "MEMETAUNTS")) - unlockables[num].type = SECRET_MEMETAUNTS; - else if (fastcmp(word2, "ITEMFINDER")) - unlockables[num].type = SECRET_ITEMFINDER; - else - unlockables[num].type = (INT16)i; + unlockables[num].type = parseunlockabletype(word2); unlockables[num].stringVarCache = -1; } else if (fastcmp(word, "VAR")) @@ -2699,6 +2703,31 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "UNLOCKPERCENT")) + { + PARAMCHECK(1); + ty = UC_UNLOCKPERCENT; + re = atoi(params[1]); + x1 = SECRET_NONE; + + // Valid percentages only! + if (re <= 0 || re > 100) + { + deh_warning("Condition percent %d out of range (1 - 100) for condition ID %d", re, id+1); + return; + } + + if (params[2] && params[2][0]) + { + x1 = parseunlockabletype(params[2]); + + if (x1 <= SECRET_NONE || x1 >= SECRET_ONEPERBOARD) + { + deh_warning("Condition challenge type \"%s\" invalid for condition ID %d", params[2], id+1); + return; + } + } + } else if ((offset=0) || fastcmp(params[0], "ADDON") || (++offset && fastcmp(params[0], "CREDITS")) || (++offset && fastcmp(params[0], "REPLAY")) diff --git a/src/m_cond.c b/src/m_cond.c index 0fc90036c..5d285f615 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1323,6 +1323,73 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UC_CONDITIONSET: // requires condition set x to already be achieved return M_Achieved(cn->requirement-1); + case UC_UNLOCKPERCENT: + { + UINT16 i, unlocked = 0, total = 0; + + // Special case for maps + if (cn->extrainfo1 == SECRET_MAP) + { + for (i = 0; i < basenummapheaders; i++) + { + if (!mapheaderinfo[i] || mapheaderinfo[i]->menuflags & (LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + + total++; + + // Check for completion + if ((mapheaderinfo[i]->menuflags & LF2_FINISHNEEDED) + && !(mapheaderinfo[i]->records.mapvisited & MV_BEATEN)) + continue; + + // Check for unlock + if (M_MapLocked(i+1)) + continue; + + unlocked++; + } + } + // Special case for raw Challenge count + else if (cn->extrainfo1 == SECRET_NONE) + { + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type == SECRET_NONE) + continue; + + total++; + + if (M_Achieved(unlockables[i].conditionset - 1) == false) + continue; + + unlocked++; + } + + unlocked++; // Try to account for this one too + } + else + { + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type != cn->extrainfo1) + continue; + + total++; + + if (gamedata->unlocked[i] == false) + continue; + + unlocked++; + } + } + + if (!total) + return false; + + // No need to do a pesky divide + return ((100 * unlocked) >= (total * cn->requirement)); + } + case UC_ADDON: return ((gamedata->everloadedaddon == true) && M_SecretUnlocked(SECRET_ADDONS, true)); @@ -1919,6 +1986,62 @@ static const char *M_GetConditionString(condition_t *cn) ? unlockables[cn->requirement-1].name : "???"); + case UC_UNLOCKPERCENT: + { + boolean checkavailable = false; + + switch (cn->extrainfo1) + { + case SECRET_NONE: + work = "completion"; + break; + case SECRET_EXTRAMEDAL: + work = "of Challenge Medals"; + break; + case SECRET_CUP: + work = "of Cups"; + break; + case SECRET_MAP: + work = "of Courses"; + break; + case SECRET_ALTMUSIC: + work = "of alternate music"; + checkavailable = true; + break; + case SECRET_SKIN: + work = "of Characters"; + checkavailable = true; + break; + case SECRET_FOLLOWER: + work = "of Followers"; + checkavailable = true; + break; + case SECRET_COLOR: + work = "of Spray Cans"; + checkavailable = true; + break; + default: + return va("INVALID CHALLENGE FOR PERCENT \"%d\"", cn->requirement); + } + + if (checkavailable == true) + { + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + if (unlockables[i].type != cn->extrainfo1) + continue; + if (gamedata->unlocked[i] == false) + continue; + break; + } + + if (i == MAXUNLOCKABLES) + work = "of ???"; + } + + return va("CHALLENGES: get %u%% %s", cn->requirement, work); + } + case UC_ADDON: if (!M_SecretUnlocked(SECRET_ADDONS, true)) return NULL; diff --git a/src/m_cond.h b/src/m_cond.h index d7224b563..39b1ecb02 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -56,6 +56,8 @@ typedef enum UC_UNLOCKABLE, // UNLOCKABLE [unlockable number] UC_CONDITIONSET, // CONDITIONSET [condition set number] + UC_UNLOCKPERCENT, // Unlock of [unlockable type] + UC_ADDON, // Ever loaded a custom file? UC_CREDITS, // Finish watching the credits UC_REPLAY, // Save a replay @@ -199,8 +201,11 @@ typedef enum SECRET_FOLLOWER, // Permit this follower SECRET_COLOR, // Permit this color + // Everything below this line is supposed to be only one per Challenges list + SECRET_ONEPERBOARD, + // Difficulty restrictions - SECRET_HARDSPEED, // Permit Hard gamespeed + SECRET_HARDSPEED = SECRET_ONEPERBOARD, // Permit Hard gamespeed SECRET_MASTERMODE, // Permit Master Mode bots in GP SECRET_ENCORE, // Permit Encore option From 70c0b0503d6beed84df9a2c0c6744b5e7cf01d78 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 14 Oct 2023 21:25:03 +0100 Subject: [PATCH 45/98] Ancient Shrine polish - Change the phrasing from "activate the ancient shrine" to "play a melody for the ancient shrine in" - Add ~~Morbius~~ Mystic Melody Orb decorations - A_FireShrink has been fixed to use mapobjectscale (and prevent dividing by 0) --- src/deh_tables.c | 16 +++++++++++++ src/info.c | 19 ++++++++++++++- src/info.h | 17 ++++++++++++++ src/m_cond.c | 2 +- src/p_enemy.c | 4 ++-- src/p_mobj.c | 61 +++++++++++++++++++++++++++++++++++++++--------- 6 files changed, 104 insertions(+), 15 deletions(-) diff --git a/src/deh_tables.c b/src/deh_tables.c index 970d73958..2145969b2 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -1198,6 +1198,22 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi // Ancient Shrine "S_ANCIENTSHRINE", + "S_MORB1", + "S_MORB2", + "S_MORB3", + "S_MORB4", + "S_MORB5", + "S_MORB6", + "S_MORB7", + "S_MORB8", + "S_MORB9", + "S_MORB10", + "S_MORB11", + "S_MORB12", + "S_MORB13", + "S_MORB14", + "S_MORB15", + // Chaos Emeralds "S_CHAOSEMERALD1", "S_CHAOSEMERALD2", diff --git a/src/info.c b/src/info.c index 1a8b0d47d..a3bc010e5 100644 --- a/src/info.c +++ b/src/info.c @@ -145,6 +145,7 @@ char sprnames[NUMSPRITES + 1][5] = "EMBM", // Emblem "SPCN", // Spray Can "MMSH", // Ancient Shrine + "MORB", // One Morbillion "EMRC", // Chaos Emeralds "SEMR", // Super Emeralds "ESPK", @@ -1902,6 +1903,22 @@ state_t states[NUMSTATES] = // Ancient Shrine {SPR_MMSH, 0, -1, {NULL}, 0, 0, S_NULL}, // S_ANCIENTSHRINE + {SPR_MORB, 0|FF_ADD, 1, {A_FireShrink}, 2*FRACUNIT/3, 12, S_MORB2}, // S_MORB1 + {SPR_MORB, 1|FF_ADD, 1, {NULL}, 0, 0, S_MORB3}, // S_MORB2 + {SPR_MORB, 2|FF_ADD, 1, {NULL}, 0, 0, S_MORB4}, // S_MORB3 + {SPR_MORB, 3|FF_ADD, 1, {NULL}, 0, 0, S_MORB5}, // S_MORB4 + {SPR_MORB, 4|FF_ADD, 1, {NULL}, 0, 0, S_MORB6}, // S_MORB5 + {SPR_MORB, 5|FF_ADD, 1, {NULL}, 0, 0, S_MORB7}, // S_MORB6 + {SPR_MORB, 6|FF_ADD, 1, {NULL}, 0, 0, S_MORB8}, // S_MORB7 + {SPR_MORB, 7|FF_ADD, 4, {NULL}, 0, 0, S_MORB9}, // S_MORB8 + {SPR_MORB, 6|FF_ADD, 1, {A_FireShrink}, 1, 12, S_MORB10}, // S_MORB9 + {SPR_MORB, 5|FF_ADD, 1, {NULL}, 0, 0, S_MORB11}, // S_MORB10 + {SPR_MORB, 4|FF_ADD, 1, {NULL}, 0, 0, S_MORB12}, // S_MORB11 + {SPR_MORB, 3|FF_ADD, 1, {NULL}, 0, 0, S_MORB13}, // S_MORB12 + {SPR_MORB, 2|FF_ADD, 1, {NULL}, 0, 0, S_MORB14}, // S_MORB13 + {SPR_MORB, 1|FF_ADD, 1, {NULL}, 0, 0, S_MORB15}, // S_MORB14 + {SPR_MORB, 0|FF_ADD, 1, {NULL}, 0, 0, S_NULL}, // S_MORB15 + // Chaos Emeralds {SPR_EMRC, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_CHAOSEMERALD2}, // S_CHAOSEMERALD1 {SPR_EMRC, FF_FULLBRIGHT|FF_ADD, 1, {NULL}, 0, 0, S_CHAOSEMERALD1}, // S_CHAOSEMERALD2 @@ -1923,7 +1940,7 @@ state_t states[NUMSTATES] = {SPR_LENS, FF_FULLBRIGHT|FF_ADD|FF_TRANS10|FF_ANIMATE|11, 8, {NULL}, 7, 1, S_GAINAX_MID2}, // S_EMERALDFLARE1 // Prison Egg Drops - {SPR_ALTM, 0|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_PRISONEGGDROP_CD + {SPR_ALTM, 0|FF_PAPERSPRITE|FF_SEMIBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_PRISONEGGDROP_CD // Bubble Source {SPR_BBLS, 0, 8, {A_BubbleSpawn}, 2048, 0, S_BUBBLES2}, // S_BUBBLES1 diff --git a/src/info.h b/src/info.h index f9a221151..e609e6ee3 100644 --- a/src/info.h +++ b/src/info.h @@ -700,6 +700,7 @@ typedef enum sprite SPR_EMBM, // Emblem SPR_SPCN, // Spray Can SPR_MMSH, // Ancient Shrine + SPR_MORB, // One Morbillion SPR_EMRC, // Chaos Emeralds SPR_SEMR, // Super Emeralds SPR_ESPK, @@ -2382,6 +2383,22 @@ typedef enum state // Ancient Shrine S_ANCIENTSHRINE, + S_MORB1, + S_MORB2, + S_MORB3, + S_MORB4, + S_MORB5, + S_MORB6, + S_MORB7, + S_MORB8, + S_MORB9, + S_MORB10, + S_MORB11, + S_MORB12, + S_MORB13, + S_MORB14, + S_MORB15, + // Chaos Emeralds S_CHAOSEMERALD1, S_CHAOSEMERALD2, diff --git a/src/m_cond.c b/src/m_cond.c index 5d285f615..1c6530a5b 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1827,7 +1827,7 @@ static const char *M_GetConditionString(condition_t *cn) else if (cn->type == UC_MAPSPBATTACK) work = "conquer"; else if (cn->type == UC_MAPMYSTICMELODY) - work = "activate the ancient shrine in"; + work = "play a melody for the ancient shrine in"; work = va("%s%s %s", prefix, diff --git a/src/p_enemy.c b/src/p_enemy.c index 084ccb169..81cd6c517 100644 --- a/src/p_enemy.c +++ b/src/p_enemy.c @@ -12652,11 +12652,11 @@ void A_FireShrink(mobj_t *actor) INT32 locvar1 = var1; INT32 locvar2 = var2; - if (LUA_CallAction(A_FIRESHRINK, actor)) + if (LUA_CallAction(A_FIRESHRINK, actor) || locvar2 == 0) return; actor->destscale = locvar1; - actor->scalespeed = FRACUNIT/locvar2; + actor->scalespeed = mapobjectscale/locvar2; } // Function: A_SpawnPterabytes diff --git a/src/p_mobj.c b/src/p_mobj.c index 89f6fa7c5..5b72fe747 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -7257,16 +7257,55 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } case MT_ANCIENTSHRINE: { - if (P_MobjWasRemoved(mobj->tracer) == false - && mobj->tracer->fuse == 1) - { - if (!(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY)) - { - mapheaderinfo[gamemap-1]->records.mapvisited |= MV_MYSTICMELODY; + boolean docolorized = false; - if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) - S_StartSound(NULL, sfx_ncitem); - gamedata->deferredsave = true; + if (P_MobjWasRemoved(mobj->tracer) == false) + { + if (mobj->tracer->fuse == 1) + { + if (!(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY)) + { + mapheaderinfo[gamemap-1]->records.mapvisited |= MV_MYSTICMELODY; + + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) + S_StartSound(NULL, sfx_ncitem); + gamedata->deferredsave = true; + } + } + + // Non-RNG-advancing equivalent of Obj_SpawnEmeraldSparks + if (leveltime % 3 == 0) + { + mobj_t *sparkle = P_SpawnMobjFromMobj( + mobj, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(0, 64) * FRACUNIT, + MT_SPARK + ); + P_SetMobjState(sparkle, S_MORB1); + + sparkle->color = SKINCOLOR_PLAGUE; + sparkle->momz += 6 * mobj->scale * P_MobjFlip(mobj); + P_SetScale(sparkle, 2); + } + + docolorized = !!(leveltime & 1); + } + + if (mobj->colorized != docolorized) + { + if (docolorized) + { + mobj->colorized = true; + mobj->color = SKINCOLOR_PLAGUE; + mobj->spriteyoffset = 1; + } + else + { + mobj->colorized = false; + mobj->color = SKINCOLOR_NONE; + mobj->spriteyoffset = 0; } } @@ -7678,7 +7717,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) mobj->renderflags |= RF_SEMIBRIGHT; } - P_SetMobjState(mobj, teststate); + P_SetMobjStateNF(mobj, teststate); if (P_MobjWasRemoved(mobj)) { @@ -7724,7 +7763,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) M_RandomRange(0, 64) * FRACUNIT, MT_SPARK ); - P_SetMobjState(sparkle, mobjinfo[MT_EMERALDSPARK].spawnstate); + P_SetMobjStateNF(sparkle, mobjinfo[MT_EMERALDSPARK].spawnstate); sparkle->color = M_RandomChance(FRACUNIT/2) ? SKINCOLOR_ULTRAMARINE : SKINCOLOR_MAGENTA; sparkle->momz += 8 * mobj->scale * P_MobjFlip(mobj); From e4a3b85adfa4e1e290e8372982a8c8dab8410306 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 14 Oct 2023 22:36:41 +0100 Subject: [PATCH 46/98] UC_UNLOCKPERCENT: Make SECRET_COLOR option actually count Spray Cans, not unlockables directly --- src/m_cond.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 1c6530a5b..bcce1c0b5 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1349,6 +1349,12 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) unlocked++; } } + // Special case for SECRET_COLOR + else if (cn->extrainfo1 == SECRET_COLOR) + { + total = gamedata->numspraycans; + unlocked = gamedata->gotspraycans; + } // Special case for raw Challenge count else if (cn->extrainfo1 == SECRET_NONE) { @@ -2017,8 +2023,8 @@ static const char *M_GetConditionString(condition_t *cn) checkavailable = true; break; case SECRET_COLOR: - work = "of Spray Cans"; - checkavailable = true; + work = (gamedata->gotspraycans == 0) ? "of ???" : "of Spray Cans"; + //checkavailable = true; break; default: return va("INVALID CHALLENGE FOR PERCENT \"%d\"", cn->requirement); From b2aac2285697adbde1ef855a4259599ccdec9f1b Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 14 Oct 2023 23:21:51 +0100 Subject: [PATCH 47/98] UCRP_FINISHPERFECT Complete a perfect round (1st every lap) to get this --- src/deh_soc.c | 1 + src/m_cond.c | 10 ++++++++++ src/m_cond.h | 1 + 3 files changed, 12 insertions(+) diff --git a/src/deh_soc.c b/src/deh_soc.c index a0a1c5335..7e1b228b6 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2894,6 +2894,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) ty = UCRP_PODIUMEMERALD + offset; } else if ((offset=0) || fastcmp(params[0], "FINISHCOOL") + || (++offset && fastcmp(params[0], "FINISHPERFECT")) || (++offset && fastcmp(params[0], "FINISHALLPRISONS")) || (++offset && fastcmp(params[0], "NOCONTEST"))) { diff --git a/src/m_cond.c b/src/m_cond.c index bcce1c0b5..0743f765a 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1489,6 +1489,14 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && !K_IsPlayerLosing(player)); + case UCRP_FINISHPERFECT: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay() + && (gamespeed != KARTSPEED_EASY) + && (player->tally.active == true) + && (player->tally.totalLaps > 0) // Only true if not Time Attack + && (player->tally.laps >= player->tally.totalLaps)); case UCRP_FINISHALLPRISONS: return (battleprisons && !(player->pflags & PF_NOCONTEST) @@ -2216,6 +2224,8 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_FINISHCOOL: return "finish in good standing"; + case UCRP_FINISHPERFECT: + return "finish a perfect round"; case UCRP_FINISHALLPRISONS: return "break every prison"; case UCRP_NOCONTEST: diff --git a/src/m_cond.h b/src/m_cond.h index 39b1ecb02..2c02c57af 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -94,6 +94,7 @@ typedef enum UCRP_PODIUMPRIZE, // Get to podium sequence with that cup's bonus (alternate string version of UCRP_PODIUMEMERALD UCRP_FINISHCOOL, // Finish in good standing + UCRP_FINISHPERFECT, // Finish a perfect race UCRP_FINISHALLPRISONS, // Break all prisons UCRP_NOCONTEST, // No Contest From 6b2e0a508d14f9da2ae2fdc9308a11fbe0334581 Mon Sep 17 00:00:00 2001 From: toaster Date: Sun, 15 Oct 2023 00:17:20 +0100 Subject: [PATCH 48/98] UCRP_RINGS, UCRP_RINGSEXACT `Rings 10` - "with at least 10 Rings" `RingsExact -14` - "with exactly -14 Rings" (`Rings 20` will also be parsed as "with exactly -20 Rings") --- src/deh_soc.c | 13 +++++++++++++ src/m_cond.c | 12 ++++++++++++ src/m_cond.h | 5 ++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 7e1b228b6..7475b7e1d 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2935,6 +2935,19 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if ((offset=0) || fastcmp(params[0], "RINGS") + || (++offset && fastcmp(params[0], "RINGSEXACT"))) + { + PARAMCHECK(1); + ty = UCRP_RINGS + offset; + re = get_number(params[1]); + + if (re < -20 || re > 20) + { + deh_warning("Invalid ring count %d for condition ID %d", re, id+1); + return; + } + } else if (fastcmp(params[0], "TRIGGER")) { PARAMCHECK(1); diff --git a/src/m_cond.c b/src/m_cond.c index 0743f765a..1dffc05c2 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1585,6 +1585,11 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && player->realtime < timelimitintics && (timelimitintics + extratimeintics + secretextratime - player->realtime) >= (unsigned)cn->requirement); + case UCRP_RINGS: + return (player->hudrings >= cn->requirement); + case UCRP_RINGSEXACT: + return (player->hudrings == cn->requirement); + case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); @@ -2260,6 +2265,13 @@ static const char *M_GetConditionString(condition_t *cn) G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); + case UCRP_RINGS: + if (cn->requirement != 20) + return va("with at least %d Rings", cn->requirement); + // FALLTHRU + case UCRP_RINGSEXACT: + return va("with exactly %d Rings", cn->requirement); + case UCRP_TRIGGER: return "do something special"; diff --git a/src/m_cond.h b/src/m_cond.h index 2c02c57af..41a05e159 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -107,6 +107,9 @@ typedef enum UCRP_FINISHTIMEEXACT, // Finish == [time, tics] UCRP_FINISHTIMELEFT, // Finish with at least [time, tics] to spare + UCRP_RINGS, // Finish >= [rings] + UCRP_RINGSEXACT, // Finish == [rings] + UCRP_TRIGGER, // Map execution trigger [id] UCRP_FALLOFF, // Fall off (or don't) @@ -119,7 +122,7 @@ typedef enum UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem - UCRP_WETPLAYER, // Don't touch [fluid] + UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; // Condition Set information From 3c5e03428a670a2451c69b4372c7226e65a0e90a Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 16 Oct 2023 15:48:08 +0100 Subject: [PATCH 49/98] UCRP_HITMIDAIR: Fix typo for object type check --- src/p_inter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/p_inter.c b/src/p_inter.c index 45fd9180c..a843a8490 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2621,7 +2621,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (inflictor && source && source->player) { if (source->player->roundconditions.hit_midair == false - && K_IsMissileOrKartItem(source) + && K_IsMissileOrKartItem(inflictor) && target->player->airtime > TICRATE/2 && source->player->airtime > TICRATE/2) { From d4e3d8433d8c46dc5287ceca7b8c88c7079c3d5e Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 16 Oct 2023 17:09:12 +0100 Subject: [PATCH 50/98] UCRP_TRACKHAZARD A series of 100 booleans on the roundconditions struct, one per possible lap. Allows for a full suite of track hazard touching conditions - see the following examples. - `Condition1 = Prefix_IsMap RR_MOTOBUGMOTORWAY - `Condition1 = TrackHazard No` - `Condition1 = IsMap RR_MOTOBUGMOTORWAY` - "MOTOBUG MOTORWAY: Don't touch any track hazard" - `Condition1 = Prefix_GrandPrix` - `Condition1 = IsMap RR_HARDBOILEDSTADIUM` - `Condition1 = TrackHazard Yes` - `Condition1 = And` - `Condition1 = FinishPlace 1` - "GRAND PRIX: On HARD-BOILED STADIUM, touch a track hazard every lap & finish in 1st" - `Condition1 = Prefix IsMap RR_DEATHEGG` - `Condition1 = Trackhazard No 8` - "DEATH EGG ZONE: Don't touch any track hazard on lap 8" - `Condition1 = Prefix_IsMap RR_SPEEDHIGHWAY - `Condition1 = TrackHazard No Final` - "SPEED HIGHWAY: Don't touch any track hazard on the final lap" --- src/d_player.h | 6 ++++++ src/deh_soc.c | 26 ++++++++++++++++++++++++ src/m_cond.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/m_cond.h | 2 ++ src/p_inter.c | 11 +++++++++- src/p_local.h | 3 --- 6 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index 89161e23c..f0b3e4edc 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -39,6 +39,10 @@ extern "C" { #endif +// Maximum laps per map. +// (done here as p_local.h, the previous host, has this as a dependency - but we must use it here) +#define MAX_LAPS 99 + // Extra abilities/settings for skins (combinable stuff) typedef enum { @@ -398,6 +402,8 @@ struct roundconditions_t boolean landmine_dunk; boolean hit_midair; + boolean hittrackhazard[MAX_LAPS+1]; + mobjeflag_t wet_player; // 32 triggers, one bit each, for map execution diff --git a/src/deh_soc.c b/src/deh_soc.c index 7475b7e1d..d6d92a031 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2981,6 +2981,32 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) //PARAMCHECK(1); ty = UCRP_TRIPWIREHYUU + offset; } + else if (fastcmp(params[0], "TRACKHAZARD")) + { + PARAMCHECK(1); + ty = UCRP_TRACKHAZARD; + re = 1; + x1 = -1; + + if (params[1][0] == 'F' || params[1][0] == 'N' || params[1][0] == '0') + re = 0; + + if (params[2]) + { + if (fastcmp(params[2], "FINAL")) + x1 = -2; + else + { + x1 = atoi(params[2]); + + if (re < 0 || re > MAX_LAPS) + { + deh_warning("Lap number %d out of range (0 - %u) for condition ID %d", x1, MAX_LAPS, id+1); + return; + } + } + } + } else { deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); diff --git a/src/m_cond.c b/src/m_cond.c index 1dffc05c2..3f38e696a 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1611,6 +1611,48 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_HITMIDAIR: return (player->roundconditions.hit_midair); + case UCRP_TRACKHAZARD: + { + INT16 requiredlap = cn->extrainfo1; + + if (requiredlap < 0) + { + // Prevents lowered numlaps from activating it + // (this also handles exiting, for all-laps situations) + requiredlap = max(mapheaderinfo[gamemap-1]->numlaps, numlaps); + } + + // cn->requirement is used as an offset here + // so if you need to get hit on lap x, the + // condition can fire while that lap is active + // but if you need to NOT get hit on lap X, + // it only fires once the lap is complete + if (player->laps <= (requiredlap - cn->requirement)) + return false; + + const boolean desired = (cn->requirement == 1); + if (cn->extrainfo1 == -1) + { + // Using cn->requirement as the first + // counted lap means that for conditions + // that require you to get hit every lap, + // that doesn't count POSITION - + // but if you can't get hit by a track + // hazard at all during the race, + // you're forbidden from getting hurt + // by a track hazard during POSITION. + for (; requiredlap >= cn->requirement; requiredlap--) + { + if (player->roundconditions.hittrackhazard[requiredlap] != desired) + return false; + } + + return true; + } + + return (player->roundconditions.hittrackhazard[requiredlap] == desired); + } + case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) && !player->roundconditions.fell_off); // Levels with water tend to texture their pits as water too @@ -2293,6 +2335,18 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; + case UCRP_TRACKHAZARD: + { + work = (cn->requirement == 1) ? "touch a track hazard" : "don't touch any track hazards"; + if (cn->extrainfo1 == -1) + return va("%s%s", work, (cn->requirement == 1) ? " on every lap" : ""); + if (cn->extrainfo1 == -2) + return va("%s on the final lap", work); + if (cn->extrainfo1 == 0) + return va("%s during POSITION", work); + return va("%s on lap %u", work, cn->extrainfo1); + } + case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", diff --git a/src/m_cond.h b/src/m_cond.h index 41a05e159..0a78c3ad2 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -122,6 +122,8 @@ typedef enum UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem + UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) + UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; diff --git a/src/p_inter.c b/src/p_inter.c index a843a8490..7b763ff62 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2618,9 +2618,10 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da } } - if (inflictor && source && source->player) + if (source && source->player) { if (source->player->roundconditions.hit_midair == false + && inflictor && K_IsMissileOrKartItem(inflictor) && target->player->airtime > TICRATE/2 && source->player->airtime > TICRATE/2) @@ -2629,6 +2630,14 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da source->player->roundconditions.checkthisframe = true; } } + else if (!(inflictor && inflictor->player) + && player->laps <= numlaps + && damagetype != DMG_DEATHPIT + && player->roundconditions.hittrackhazard[player->laps] == false) + { + player->roundconditions.hittrackhazard[player->laps] = true; + player->roundconditions.checkthisframe = true; + } // Instant-Death if ((damagetype & DMG_DEATHMASK)) diff --git a/src/p_local.h b/src/p_local.h index 15b338f9d..3c809034f 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -32,9 +32,6 @@ extern "C" { //#define VIEWHEIGHTS "41" -// Maximum laps per map. -#define MAX_LAPS 99 - // Maximum player score. #define MAXSCORE 99999990 // 999999990 From f0d1813752c31421163ae417b67f21b5fa57b391 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 16 Oct 2023 18:04:23 +0100 Subject: [PATCH 51/98] UCRP_TRACKHAZARD: Optimise implementation to not be 100 booleans Much like player->availabilities, use a set of bits on UINT8. --- src/d_player.h | 2 +- src/m_cond.c | 56 ++++++++++++++++++++++++++++++++++++++------------ src/p_inter.c | 11 ++++++---- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index f0b3e4edc..548ed8f7c 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -402,7 +402,7 @@ struct roundconditions_t boolean landmine_dunk; boolean hit_midair; - boolean hittrackhazard[MAX_LAPS+1]; + UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; mobjeflag_t wet_player; diff --git a/src/m_cond.c b/src/m_cond.c index 3f38e696a..6d867e254 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1630,27 +1630,57 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) if (player->laps <= (requiredlap - cn->requirement)) return false; - const boolean desired = (cn->requirement == 1); + UINT8 requiredbit = 1<<(requiredlap & 7); + requiredlap /= 8; + if (cn->extrainfo1 == -1) { - // Using cn->requirement as the first - // counted lap means that for conditions - // that require you to get hit every lap, - // that doesn't count POSITION - - // but if you can't get hit by a track - // hazard at all during the race, - // you're forbidden from getting hurt - // by a track hazard during POSITION. - for (; requiredlap >= cn->requirement; requiredlap--) + if (cn->requirement == 0) { - if (player->roundconditions.hittrackhazard[requiredlap] != desired) + // The "don't get hit on any lap" check is trivial. + for (; requiredlap > 0; requiredlap--) + { + if (player->roundconditions.hittrackhazard[requiredlap] != 0) + return false; + } + + return (player->roundconditions.hittrackhazard[0] == 0); + } + + // The following is my attempt at a major optimisation. + // The naive version was MAX_LAP bools, which is ridiculous. + + // Check the highest relevant byte for all necessary bits. + // We only do this if an == 0xFF/0xFE check wouldn't satisfy. + if (requiredbit != 7) + { + // Last bit MAYBE not needed, POSITION doesn't count. + const UINT8 finalbit = (requiredlap == 0) ? 1 : 0; + while (requiredbit != finalbit) + { + if (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit)) + return false; + requiredbit /= 2; + } + + if (requiredlap == 0) + return true; + + requiredlap--; + } + + // All bytes between the top and the bottom need to be checked for saturation. + for (; requiredlap > 0; requiredlap--) + { + if (player->roundconditions.hittrackhazard[requiredlap] != 0xFF) return false; } - return true; + // Last bit not needed, POSITION doesn't count. + return (player->roundconditions.hittrackhazard[0] == 0xFE); } - return (player->roundconditions.hittrackhazard[requiredlap] == desired); + return (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit) != (cn->requirement == 1)); } case UCRP_WETPLAYER: diff --git a/src/p_inter.c b/src/p_inter.c index 7b763ff62..6b2f248d8 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2632,11 +2632,14 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da } else if (!(inflictor && inflictor->player) && player->laps <= numlaps - && damagetype != DMG_DEATHPIT - && player->roundconditions.hittrackhazard[player->laps] == false) + && damagetype != DMG_DEATHPIT) { - player->roundconditions.hittrackhazard[player->laps] = true; - player->roundconditions.checkthisframe = true; + const UINT8 requiredbit = 1<<(player->laps & 7); + if (!(player->roundconditions.hittrackhazard[player->laps/8] & requiredbit)) + { + player->roundconditions.hittrackhazard[player->laps/8] |= requiredbit; + player->roundconditions.checkthisframe = true; + } } // Instant-Death From d11fe78e90e6678b7c232bae2e47431c45949a05 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 01:07:03 +0100 Subject: [PATCH 52/98] UCRP_UFOATTACKMETHOD `Condition1 = UfoAttackMethod [type]` - "smash a UFO Catcher using only [type]" - Combine with `Prefix_SealedStar` or `Prefix_IsMap [special stage stage]` - Shows up as "???" - Types supported: - `Boost` - "boost power" (sneakers) - `Whip` - "Insta-Whip" - `Banana` - "Bananas" - `Orbinaut`- "Orbinauts" - `Jawz` - "Jawz" - `SPB` - "Self Propelled Bombs" - Other types could be added on request, these were just the easy ones In addition, the prototype for P_MobjWasRemoved was moved to `p_mobj.h`. It's EXTREMELY important that we're able to safely check mobj pointers anywhere a mobj_t is possible to observe, without including the full `p_local.h`... --- src/d_player.h | 14 +++++++++++ src/deh_soc.c | 19 ++++++++++++++ src/m_cond.c | 47 +++++++++++++++++++++++++++++++++++ src/m_cond.h | 2 ++ src/objects/ufo.c | 63 ++++++++++++++++++++++++++++++++++++++++------- src/p_local.h | 1 - src/p_mobj.h | 3 +++ 7 files changed, 139 insertions(+), 10 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index 548ed8f7c..a4b9b0426 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -387,6 +387,18 @@ struct botvars_t // player_t struct for round-specific condition tracking +typedef enum +{ + UFOD_GENERIC = 1, + UFOD_BOOST = 1<<1, + UFOD_WHIP = 1<<2, + UFOD_BANANA = 1<<3, + UFOD_ORBINAUT = 1<<4, + UFOD_JAWZ = 1<<5, + UFOD_SPB = 1<<6, + // free up to and including 1<<31 +} ufodamaging_t; + struct roundconditions_t { // Reduce the number of checks by only updating when this is true @@ -404,6 +416,8 @@ struct roundconditions_t UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; + ufodamaging_t ufodamaging; + mobjeflag_t wet_player; // 32 triggers, one bit each, for map execution diff --git a/src/deh_soc.c b/src/deh_soc.c index d6d92a031..ea0b66dfe 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3007,6 +3007,25 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) } } } + else if (fastcmp(params[0], "UFOATTACKMETHOD")) + { + PARAMCHECK(1); + ty = UCRP_UFOATTACKMETHOD; + + // See ufodamaging_t + if ((offset=1) || fastcmp(params[1], "BOOST") + || (++offset && fastcmp(params[1], "WHIP")) + || (++offset && fastcmp(params[1], "BANANA")) + || (++offset && fastcmp(params[1], "ORBINAUT")) + || (++offset && fastcmp(params[1], "JAWZ")) + || (++offset && fastcmp(params[1], "SPB"))) + re = offset; + else + { + deh_warning("Unknown attack method %s for condition ID %d", params[1], id+1); + return; + } + } else { deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); diff --git a/src/m_cond.c b/src/m_cond.c index 6d867e254..9a5bd39ee 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1683,6 +1683,16 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit) != (cn->requirement == 1)); } + case UCRP_UFOATTACKMETHOD: + return ( + specialstageinfo.valid == true + && ( + P_MobjWasRemoved(specialstageinfo.ufo) + || specialstageinfo.ufo->health <= 1 + ) + && player->roundconditions.ufodamaging == (ufodamaging_t)(1<requirement) + ); + case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) && !player->roundconditions.fell_off); // Levels with water tend to texture their pits as water too @@ -2377,6 +2387,43 @@ static const char *M_GetConditionString(condition_t *cn) return va("%s on lap %u", work, cn->extrainfo1); } + case UCRP_UFOATTACKMETHOD: + { + if (!gamedata->everseenspecial) + return NULL; + + work = NULL; + + switch (cn->requirement) + { + case 1: + work = "boost power"; + break; + case 2: + work = "Insta-Whip"; + break; + case 3: + work = "Bananas"; + break; + case 4: + work = "Orbinauts"; + break; + case 5: + work = "Jawz"; + break; + case 6: + work = "Self Propelled Bombs"; + break; + default: + break; + } + + if (work == NULL) + return va("INVALID ATTACK CONDITION \"%d:%d\"", cn->type, cn->requirement); + + return va("smash the UFO Catcher using only %s", work); + } + case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", diff --git a/src/m_cond.h b/src/m_cond.h index 0a78c3ad2..a95d38cc2 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -124,6 +124,8 @@ typedef enum UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) + UCRP_UFOATTACKMETHOD, // Defeat a UFO Catcher using only one method + UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; diff --git a/src/objects/ufo.c b/src/objects/ufo.c index cb9351aed..5e1a721e6 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -763,52 +763,80 @@ static void UFOKillPieces(mobj_t *ufo) static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) { + UINT8 ret = 0; + ufodamaging_t ufodamaging = UFOD_GENERIC; + if (inflictor != NULL && P_MobjWasRemoved(inflictor) == false) { switch (inflictor->type) { + // Shields deal chip damage. case MT_JAWZ_SHIELD: + { + ufodamaging = UFOD_JAWZ; + ret = 10; + break; + } case MT_ORBINAUT_SHIELD: + { + ufodamaging = UFOD_ORBINAUT; + ret = 10; + break; + } case MT_INSTAWHIP: { - // Shields deal chip damage. - return 10; + ufodamaging = UFOD_WHIP; + ret = 10; + break; } case MT_JAWZ: { // Thrown Jawz deal a bit extra. - return 15; + ufodamaging = UFOD_JAWZ; + ret = 15; + break; } case MT_ORBINAUT: { // Thrown orbinauts deal double damage. - return 20; + ufodamaging = UFOD_ORBINAUT; + ret = 20; + break; } case MT_SPB: { // SPB deals triple damage. - return 30; + ufodamaging |= UFOD_SPB; + ret = 30; + break; } case MT_BANANA: { + ufodamaging = UFOD_BANANA; + // Banana snipes deal triple damage, // laid down bananas deal regular damage. if (inflictor->health > 1) { - return 30; + ret = 30; + break; } - return 10; + ret = 10; + break; } case MT_PLAYER: { // Players deal damage relative to how many sneakers they used. - return 15 * max(1, inflictor->player->numsneakers); + ufodamaging = UFOD_BOOST; + ret = 15 * max(1, inflictor->player->numsneakers); + break; } case MT_SPECIAL_UFO: { // UFODebugSetHealth - return 1; + ret = 1; + break; } default: { @@ -817,6 +845,23 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) } } + { + // We have to iterate over all players, otherwise a player who gets exactly one hit in will trick the Challenges system. + UINT8 i; + for (i = 0; i <= splitscreen; i++) + { + if (!playeringame[g_localplayers[i]]) + continue; + if (players[g_localplayers[i]].spectator) + continue; + players[i].roundconditions.ufodamaging |= ufodamaging; + players[i].roundconditions.checkthisframe = true; + } + } + + if (ret) + return ret; + // Guess from damage type. switch (damageType & DMG_TYPEMASK) { diff --git a/src/p_local.h b/src/p_local.h index 3c809034f..72abb1dec 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -260,7 +260,6 @@ void P_RecalcPrecipInSector(sector_t *sector); void P_PrecipitationEffects(void); void P_RemoveMobj(mobj_t *th); -boolean P_MobjWasRemoved(const mobj_t *th); void P_RemoveSavegameMobj(mobj_t *th); boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state); boolean P_SetMobjState(mobj_t *mobj, statenum_t state); diff --git a/src/p_mobj.h b/src/p_mobj.h index 585dff7c7..55d09e765 100644 --- a/src/p_mobj.h +++ b/src/p_mobj.h @@ -509,6 +509,9 @@ struct precipmobj_t tic_t lastThink; }; +// It's extremely important that all mobj_t*-reading code have access to this. +boolean P_MobjWasRemoved(const mobj_t *th); + struct actioncache_t { actioncache_t *next; From 3836cfddcba62f6eee7d2d14a2fb63f2b1318217 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 12:49:50 +0100 Subject: [PATCH 53/98] UCRP_UFOATTACKMETHOD: Only check roundconditions the frame the UFO is destroyed, not hit --- src/objects/ufo.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/objects/ufo.c b/src/objects/ufo.c index 5e1a721e6..10da8e7ff 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -855,7 +855,6 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) if (players[g_localplayers[i]].spectator) continue; players[i].roundconditions.ufodamaging |= ufodamaging; - players[i].roundconditions.checkthisframe = true; } } @@ -929,6 +928,18 @@ boolean Obj_SpecialUFODamage(mobj_t *ufo, mobj_t *inflictor, mobj_t *source, UIN // Destroy the UFO parts, and make the emerald collectible! UFOKillPieces(ufo); + { + UINT8 i; + for (i = 0; i <= splitscreen; i++) + { + if (!playeringame[g_localplayers[i]]) + continue; + if (players[g_localplayers[i]].spectator) + continue; + players[i].roundconditions.checkthisframe = true; + } + } + ufo->flags = (ufo->flags & ~MF_SHOOTABLE) | (MF_SPECIAL|MF_PICKUPFROMBELOW); ufo->shadowscale = FRACUNIT/3; From b91823540bddf0e4d7cda56a4f56231b513687e7 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 20:57:19 +0100 Subject: [PATCH 54/98] UC_PRISONEGGCD: Don't use K_FlipFromObject, this was heinously incorrect --- src/p_inter.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/p_inter.c b/src/p_inter.c index 6b2f248d8..c8c7212f2 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -1042,12 +1042,15 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) { secretpickup->hitlag = target->hitlag; + secretpickup->z -= secretpickup->height/2; + P_SetScale(secretpickup, mapobjectscale/TICRATE); // secretpickup->destscale = mapobjectscale; -- safe assumption it's already set? secretpickup->scalespeed = (2*mapobjectscale)/(3*TICRATE); - // NOT from the target - just in case it's just been placed on the ceiling as a gimmick - K_FlipFromObject(secretpickup, source); + // flags are NOT from the target - just in case it's just been placed on the ceiling as a gimmick + secretpickup->flags2 |= (source->flags2 & MF2_OBJECTFLIP); + secretpickup->eflags |= (source->eflags & MFE_VERTICALFLIP); // Okay these have to use M_Random because replays... // The spawning of these won't be recorded back! From 01b97cb829a1ef8fc7b1675daf8af906beb998f1 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 21:03:20 +0100 Subject: [PATCH 55/98] Update UCRP_UFOATTACKMETHOD into UCRP_TARGETATTACKMETHOD Supports Prison Eggs and Gachabom now as well. For the original behaviour, combine with the new UCRP_SMASHUFO. --- src/d_player.h | 5 +++-- src/deh_soc.c | 30 +++++++++++++++---------- src/m_cond.c | 47 ++++++++++++++++++++++++--------------- src/m_cond.h | 3 ++- src/objects/ufo.c | 39 ++++++++++++++------------------- src/p_inter.c | 56 +++++++++++++++++++++++++++++++++++++++++++---- src/p_local.h | 2 ++ 7 files changed, 124 insertions(+), 58 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index a4b9b0426..c0ffb3519 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -396,8 +396,9 @@ typedef enum UFOD_ORBINAUT = 1<<4, UFOD_JAWZ = 1<<5, UFOD_SPB = 1<<6, + UFOD_GACHABOM = 1<<7, // free up to and including 1<<31 -} ufodamaging_t; +} targetdamaging_t; struct roundconditions_t { @@ -416,7 +417,7 @@ struct roundconditions_t UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; - ufodamaging_t ufodamaging; + targetdamaging_t targetdamaging; mobjeflag_t wet_player; diff --git a/src/deh_soc.c b/src/deh_soc.c index ea0b66dfe..822747a40 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2896,7 +2896,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) else if ((offset=0) || fastcmp(params[0], "FINISHCOOL") || (++offset && fastcmp(params[0], "FINISHPERFECT")) || (++offset && fastcmp(params[0], "FINISHALLPRISONS")) - || (++offset && fastcmp(params[0], "NOCONTEST"))) + || (++offset && fastcmp(params[0], "NOCONTEST")) + || (++offset && fastcmp(params[0], "SMASHUFO"))) { //PARAMCHECK(1); ty = UCRP_FINISHCOOL + offset; @@ -3007,19 +3008,26 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) } } } - else if (fastcmp(params[0], "UFOATTACKMETHOD")) + else if (fastcmp(params[0], "TARGETATTACKMETHOD")) { PARAMCHECK(1); - ty = UCRP_UFOATTACKMETHOD; + ty = UCRP_TARGETATTACKMETHOD; - // See ufodamaging_t - if ((offset=1) || fastcmp(params[1], "BOOST") - || (++offset && fastcmp(params[1], "WHIP")) - || (++offset && fastcmp(params[1], "BANANA")) - || (++offset && fastcmp(params[1], "ORBINAUT")) - || (++offset && fastcmp(params[1], "JAWZ")) - || (++offset && fastcmp(params[1], "SPB"))) - re = offset; + // See targetdamaging_t + if (fastcmp(params[1], "BOOST")) + re = UFOD_BOOST; + else if (fastcmp(params[1], "WHIP")) + re = UFOD_WHIP; + else if (fastcmp(params[1], "BANANA")) + re = UFOD_BANANA; + else if (fastcmp(params[1], "ORBINAUT")) + re = UFOD_ORBINAUT; + else if (fastcmp(params[1], "JAWZ")) + re = UFOD_JAWZ; + else if (fastcmp(params[1], "SPB")) + re = UFOD_SPB; + else if (fastcmp(params[1], "GACHABOM")) + re = UFOD_GACHABOM; else { deh_warning("Unknown attack method %s for condition ID %d", params[1], id+1); diff --git a/src/m_cond.c b/src/m_cond.c index 9a5bd39ee..b11241373 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1505,6 +1505,15 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_NOCONTEST: return (player->pflags & PF_NOCONTEST); + case UCRP_SMASHUFO: + return ( + specialstageinfo.valid == true + && ( + P_MobjWasRemoved(specialstageinfo.ufo) + || specialstageinfo.ufo->health <= 1 + ) + ); + case UCRP_MAKERETIRE: { // You can't "make" someone retire in coop. @@ -1683,15 +1692,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit) != (cn->requirement == 1)); } - case UCRP_UFOATTACKMETHOD: - return ( - specialstageinfo.valid == true - && ( - P_MobjWasRemoved(specialstageinfo.ufo) - || specialstageinfo.ufo->health <= 1 - ) - && player->roundconditions.ufodamaging == (ufodamaging_t)(1<requirement) - ); + case UCRP_TARGETATTACKMETHOD: + return (player->roundconditions.targetdamaging == (targetdamaging_t)cn->requirement); case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) @@ -2314,10 +2316,15 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_FINISHPERFECT: return "finish a perfect round"; case UCRP_FINISHALLPRISONS: - return "break every prison"; + return "break every Prison Egg"; case UCRP_NOCONTEST: return "NO CONTEST"; + case UCRP_SMASHUFO: + if (!gamedata->everseenspecial) + return NULL; + return "smash the UFO Catcher"; + case UCRP_MAKERETIRE: { if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) @@ -2387,7 +2394,7 @@ static const char *M_GetConditionString(condition_t *cn) return va("%s on lap %u", work, cn->extrainfo1); } - case UCRP_UFOATTACKMETHOD: + case UCRP_TARGETATTACKMETHOD: { if (!gamedata->everseenspecial) return NULL; @@ -2396,24 +2403,28 @@ static const char *M_GetConditionString(condition_t *cn) switch (cn->requirement) { - case 1: + // See targetdamaging_t + case UFOD_BOOST: work = "boost power"; break; - case 2: + case UFOD_WHIP: work = "Insta-Whip"; break; - case 3: + case UFOD_BANANA: work = "Bananas"; break; - case 4: + case UFOD_ORBINAUT: work = "Orbinauts"; break; - case 5: + case UFOD_JAWZ: work = "Jawz"; break; - case 6: + case UFOD_SPB: work = "Self Propelled Bombs"; break; + case UFOD_GACHABOM: + work = "Gachabom"; + break; default: break; } @@ -2421,7 +2432,7 @@ static const char *M_GetConditionString(condition_t *cn) if (work == NULL) return va("INVALID ATTACK CONDITION \"%d:%d\"", cn->type, cn->requirement); - return va("smash the UFO Catcher using only %s", work); + return va("using only %s", work); } case UCRP_WETPLAYER: diff --git a/src/m_cond.h b/src/m_cond.h index a95d38cc2..008b996dc 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -97,6 +97,7 @@ typedef enum UCRP_FINISHPERFECT, // Finish a perfect race UCRP_FINISHALLPRISONS, // Break all prisons UCRP_NOCONTEST, // No Contest + UCRP_SMASHUFO, // Smash the UFO Catcher UCRP_MAKERETIRE, // Make another player of [skin] No Contest @@ -124,7 +125,7 @@ typedef enum UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) - UCRP_UFOATTACKMETHOD, // Defeat a UFO Catcher using only one method + UCRP_TARGETATTACKMETHOD, // Break targets/UFO using only one method UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; diff --git a/src/objects/ufo.c b/src/objects/ufo.c index 10da8e7ff..bff265b66 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -764,7 +764,7 @@ static void UFOKillPieces(mobj_t *ufo) static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) { UINT8 ret = 0; - ufodamaging_t ufodamaging = UFOD_GENERIC; + targetdamaging_t targetdamaging = UFOD_GENERIC; if (inflictor != NULL && P_MobjWasRemoved(inflictor) == false) { @@ -773,46 +773,52 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) // Shields deal chip damage. case MT_JAWZ_SHIELD: { - ufodamaging = UFOD_JAWZ; + targetdamaging = UFOD_JAWZ; ret = 10; break; } case MT_ORBINAUT_SHIELD: { - ufodamaging = UFOD_ORBINAUT; + targetdamaging = UFOD_ORBINAUT; ret = 10; break; } case MT_INSTAWHIP: { - ufodamaging = UFOD_WHIP; + targetdamaging = UFOD_WHIP; ret = 10; break; } case MT_JAWZ: { // Thrown Jawz deal a bit extra. - ufodamaging = UFOD_JAWZ; + targetdamaging = UFOD_JAWZ; ret = 15; break; } case MT_ORBINAUT: { // Thrown orbinauts deal double damage. - ufodamaging = UFOD_ORBINAUT; + targetdamaging = UFOD_ORBINAUT; ret = 20; break; } + case MT_GACHABOM: + { + // Thrown gachabom need to be tracked, but have no special damage value as of yet. + targetdamaging = UFOD_GACHABOM; + break; + } case MT_SPB: { // SPB deals triple damage. - ufodamaging |= UFOD_SPB; + targetdamaging |= UFOD_SPB; ret = 30; break; } case MT_BANANA: { - ufodamaging = UFOD_BANANA; + targetdamaging = UFOD_BANANA; // Banana snipes deal triple damage, // laid down bananas deal regular damage. @@ -828,7 +834,7 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) case MT_PLAYER: { // Players deal damage relative to how many sneakers they used. - ufodamaging = UFOD_BOOST; + targetdamaging = UFOD_BOOST; ret = 15 * max(1, inflictor->player->numsneakers); break; } @@ -845,20 +851,9 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) } } - { - // We have to iterate over all players, otherwise a player who gets exactly one hit in will trick the Challenges system. - UINT8 i; - for (i = 0; i <= splitscreen; i++) - { - if (!playeringame[g_localplayers[i]]) - continue; - if (players[g_localplayers[i]].spectator) - continue; - players[i].roundconditions.ufodamaging |= ufodamaging; - } - } + P_TrackRoundConditionTargetDamage(targetdamaging); - if (ret) + if (ret != 0) return ret; // Guess from damage type. diff --git a/src/p_inter.c b/src/p_inter.c index c8c7212f2..fbd81a9fd 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -977,10 +977,21 @@ void P_TouchCheatcheck(mobj_t *post, player_t *player, boolean snaptopost) player->cheatchecknum = post->health; } -static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) +void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging) { - (void)target; + UINT8 i; + for (i = 0; i <= splitscreen; i++) + { + if (!playeringame[g_localplayers[i]]) + continue; + if (players[g_localplayers[i]].spectator) + continue; + players[i].roundconditions.targetdamaging |= targetdamaging; + } +} +static void P_AddBrokenPrison(mobj_t *target, mobj_t *inflictor, mobj_t *source) +{ if (!battleprisons) return; @@ -1002,6 +1013,43 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) K_SpawnBattlePoints(source->player, NULL, 1); } + targetdamaging_t targetdamaging = UFOD_GENERIC; + if (P_MobjWasRemoved(inflictor) == true) + ; + else switch (inflictor->type) + { + case MT_GACHABOM: + targetdamaging = UFOD_GACHABOM; + break; + case MT_ORBINAUT: + case MT_ORBINAUT_SHIELD: + targetdamaging = UFOD_ORBINAUT; + break; + case MT_BANANA: + targetdamaging = UFOD_BANANA; + break; + case MT_INSTAWHIP: + targetdamaging = UFOD_WHIP; + break; + // This is only accessible for MT_CDUFO's touch! + case MT_PLAYER: + targetdamaging = UFOD_BOOST; + break; + // The following can't be accessed in standard play... + // but the cost of tracking them here is trivial :D + case MT_JAWZ: + case MT_JAWZ_SHIELD: + targetdamaging = UFOD_JAWZ; + break; + case MT_SPB: + targetdamaging = UFOD_SPB; + break; + default: + break; + } + + P_TrackRoundConditionTargetDamage(targetdamaging); + if (gamedata->prisoneggstothispickup) { gamedata->prisoneggstothispickup--; @@ -2008,7 +2056,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget S_StartSound(target, sfx_mbs60); - P_AddBrokenPrison(target, source); + P_AddBrokenPrison(target, inflictor, source); } break; @@ -2018,7 +2066,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget target->momz = -(3*mapobjectscale)/2; target->fuse = 2*TICRATE; - P_AddBrokenPrison(target, source); + P_AddBrokenPrison(target, inflictor, source); break; case MT_BATTLEBUMPER: diff --git a/src/p_local.h b/src/p_local.h index 72abb1dec..f063dc8a1 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -546,6 +546,8 @@ void P_UpdateLastPickup(player_t *player, UINT8 type); boolean P_CanPickupEmblem(player_t *player, INT32 emblemID); boolean P_EmblemWasCollected(INT32 emblemID); +void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging); + // // P_SPEC // From 3e494a342c43dcc06d0f3409a0fb64e0d63aad09 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 21:09:22 +0100 Subject: [PATCH 56/98] UCRP_GACHABOMMISER Break a target (Prison Egg/UFO) using exactly one Gachabom again and again Becomes invalid if an Insta-Whip collides with a target, or you throw two at the same time --- src/d_player.h | 2 ++ src/deh_soc.c | 5 +++++ src/k_kart.c | 4 ++++ src/m_cond.c | 9 +++++++++ src/m_cond.h | 1 + src/objects/gachabom-rebound.cpp | 2 ++ src/p_inter.c | 4 ++++ 7 files changed, 27 insertions(+) diff --git a/src/d_player.h b/src/d_player.h index c0ffb3519..7084542cb 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -417,7 +417,9 @@ struct roundconditions_t UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; + // Attack-based conditions targetdamaging_t targetdamaging; + UINT8 gachabom_miser; mobjeflag_t wet_player; diff --git a/src/deh_soc.c b/src/deh_soc.c index 822747a40..e520a73c0 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3034,6 +3034,11 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "GACHABOMMISER")) + { + //PARAMCHECK(1); + ty = UCRP_GACHABOMMISER; + } else { deh_warning("Invalid condition name %s for condition ID %d", params[0], id+1); diff --git a/src/k_kart.c b/src/k_kart.c index be368aa19..370224ccf 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -11722,6 +11722,10 @@ void K_MoveKartPlayer(player_t *player, boolean onground) K_ThrowKartItem(player, true, MT_GACHABOM, 0, 0, 0); K_PlayAttackTaunt(player->mo); player->itemamount--; + player->roundconditions.gachabom_miser = ( + (player->roundconditions.gachabom_miser == 0) + ? 1 : 0xFF + ); K_UpdateHnextList(player, false); } break; diff --git a/src/m_cond.c b/src/m_cond.c index b11241373..bd41ec9db 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1695,6 +1695,12 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_TARGETATTACKMETHOD: return (player->roundconditions.targetdamaging == (targetdamaging_t)cn->requirement); + case UCRP_GACHABOMMISER: + return ( + player->roundconditions.targetdamaging == UFOD_GACHABOM + && player->roundconditions.gachabom_miser != 0xFF + ); + case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) && !player->roundconditions.fell_off); // Levels with water tend to texture their pits as water too @@ -2435,6 +2441,9 @@ static const char *M_GetConditionString(condition_t *cn) return va("using only %s", work); } + case UCRP_GACHABOMMISER: + return "using exactly one Gachabom repeatedly"; + case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", diff --git a/src/m_cond.h b/src/m_cond.h index 008b996dc..8eaf93430 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -126,6 +126,7 @@ typedef enum UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) UCRP_TARGETATTACKMETHOD, // Break targets/UFO using only one method + UCRP_GACHABOMMISER, // Break targets/UFO using exactly one Gachabom repeatedly UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; diff --git a/src/objects/gachabom-rebound.cpp b/src/objects/gachabom-rebound.cpp index 07aec4868..7da4a3f81 100644 --- a/src/objects/gachabom-rebound.cpp +++ b/src/objects/gachabom-rebound.cpp @@ -73,6 +73,8 @@ bool award_target(mobj_t* mobj) { player->itemtype = KITEM_GACHABOM; player->itemamount++; + if (player->roundconditions.gachabom_miser == 1) + player->roundconditions.gachabom_miser = 0; return true; } diff --git a/src/p_inter.c b/src/p_inter.c index fbd81a9fd..04c87f4a1 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -987,6 +987,10 @@ void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging) if (players[g_localplayers[i]].spectator) continue; players[i].roundconditions.targetdamaging |= targetdamaging; + /* -- the following isn't needed because we can just check for targetdamaging == UFOD_GACHABOM + if (targetdamaging != UFOD_GACHABOM) + players[i].roundconditions.gachabom_miser = 0xFF; + */ } } From 54137ad468b1f8f17435bdff224275b05c9621df Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 22:19:16 +0100 Subject: [PATCH 57/98] Eggmark fixes - Update eggmanblame on K_EggmanTransfer - Currently blames the bumper, but could blame the source's eggmanblame if we wanted to change that - Use K_FlipFromObject for correct reverse gravity placement --- src/k_kart.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/k_kart.c b/src/k_kart.c index 370224ccf..1b2082e2b 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -8408,7 +8408,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) //player->flashing = 0; eggsexplode = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SPBEXPLOSION); eggsexplode->height = 2 * player->mo->height; - eggsexplode->color = player->mo->color; + K_FlipFromObject(eggsexplode, player->mo); if (player->eggmanblame >= 0 && player->eggmanblame < MAXPLAYERS @@ -12304,6 +12304,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim) K_AddHitLag(victim->mo, 2, true); K_DropItems(victim); victim->eggmanexplode = 6*TICRATE; + victim->eggmanblame = (source - players); K_StopRoulette(&victim->itemRoulette); if (P_IsDisplayPlayer(victim) && !demo.freecam) @@ -12311,6 +12312,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim) K_AddHitLag(source->mo, 2, true); source->eggmanexplode = 0; + source->eggmanblame = -1; K_StopRoulette(&source->itemRoulette); source->eggmanTransferDelay = 10; From 9e1aec42981be691d91926294223850077e0a9ff Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 22:25:47 +0100 Subject: [PATCH 58/98] UCRP_RETURNMARKTOSENDER "when cursed with Eggmark, blow up the racer responsible" Just a simple condition. Either it happens or it doesn't. MT_SPBEXPLOSION now tracks whether it was made by KITEM_EGGMAN or KITEM_SPB, too! --- src/d_player.h | 1 + src/deh_soc.c | 3 ++- src/k_kart.c | 4 ++++ src/m_cond.c | 4 ++++ src/m_cond.h | 1 + src/objects/spb.c | 2 ++ src/p_inter.c | 12 ++++++++++++ 7 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/d_player.h b/src/d_player.h index 7084542cb..cde2c9b80 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -414,6 +414,7 @@ struct roundconditions_t boolean spb_neuter; boolean landmine_dunk; boolean hit_midair; + boolean returntosender_mark; UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; diff --git a/src/deh_soc.c b/src/deh_soc.c index e520a73c0..61752866b 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2977,7 +2977,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) else if ((offset=0) || fastcmp(params[0], "TRIPWIREHYUU") || (++offset && fastcmp(params[0], "SPBNEUTER")) || (++offset && fastcmp(params[0], "LANDMINEDUNK")) - || (++offset && fastcmp(params[0], "HITMIDAIR"))) + || (++offset && fastcmp(params[0], "HITMIDAIR")) + || (++offset && fastcmp(params[0], "RETURNMARKTOSENDER"))) { //PARAMCHECK(1); ty = UCRP_TRIPWIREHYUU + offset; diff --git a/src/k_kart.c b/src/k_kart.c index 1b2082e2b..ba3deb9bc 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -8410,6 +8410,10 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) eggsexplode->height = 2 * player->mo->height; K_FlipFromObject(eggsexplode, player->mo); + eggsexplode->threshold = KITEM_EGGMAN; + + P_SetTarget(&eggsexplode->tracer, player->mo); + if (player->eggmanblame >= 0 && player->eggmanblame < MAXPLAYERS && playeringame[player->eggmanblame] diff --git a/src/m_cond.c b/src/m_cond.c index bd41ec9db..a7e8e3c2d 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1619,6 +1619,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (player->roundconditions.landmine_dunk); case UCRP_HITMIDAIR: return (player->roundconditions.hit_midair); + case UCRP_RETURNMARKTOSENDER: + return (player->roundconditions.returntosender_mark); case UCRP_TRACKHAZARD: { @@ -2387,6 +2389,8 @@ static const char *M_GetConditionString(condition_t *cn) return "dunk a Landmine on another racer's head"; case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; + case UCRP_RETURNMARKTOSENDER: + return "when cursed with Eggmark, blow up the racer responsible"; case UCRP_TRACKHAZARD: { diff --git a/src/m_cond.h b/src/m_cond.h index 8eaf93430..5914e07f5 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -122,6 +122,7 @@ typedef enum UCRP_SPBNEUTER, // Kill an SPB with Lightning UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem + UCRP_RETURNMARKTOSENDER, // Hit the player responsible for Eggman Marking you with that explosion UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) diff --git a/src/objects/spb.c b/src/objects/spb.c index 6877918d9..b90da6789 100644 --- a/src/objects/spb.c +++ b/src/objects/spb.c @@ -1025,6 +1025,8 @@ void Obj_SPBExplode(mobj_t *spb) P_SetTarget(&spbExplode->target, spb_owner(spb)); } + spbExplode->threshold = KITEM_SPB; + // Tell the explosion to use alternate knockback. spbExplode->movefactor = ((SPB_CHASETIMESCALE - spb_chasetime(spb)) * SPB_CHASETIMEMUL) / SPB_CHASETIMESCALE; diff --git a/src/p_inter.c b/src/p_inter.c index 04c87f4a1..0f75371cd 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2684,6 +2684,18 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da source->player->roundconditions.hit_midair = true; source->player->roundconditions.checkthisframe = true; } + + if (source == target + && !P_MobjWasRemoved(inflictor) + && inflictor->type == MT_SPBEXPLOSION + && inflictor->threshold == KITEM_EGGMAN + && !P_MobjWasRemoved(inflictor->tracer) + && inflictor->tracer != source + && inflictor->tracer->player) + { + inflictor->tracer->player->roundconditions.returntosender_mark = true; + inflictor->tracer->player->roundconditions.checkthisframe = true; + } } else if (!(inflictor && inflictor->player) && player->laps <= numlaps From 35ca8e6191cdb79047249af019fa11d7e7de0a5e Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 22:53:40 +0100 Subject: [PATCH 59/98] UCRP_SPEEDOMETER Provide a percentage between 100 and 999 inclusive. Reach that speed on the speedometer at any point during the race to achieve the condition. --- src/d_player.h | 2 ++ src/deh_soc.c | 12 ++++++++++++ src/m_cond.c | 10 ++++++++++ src/m_cond.h | 6 ++++-- src/p_user.c | 13 +++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index cde2c9b80..c01f82241 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -422,6 +422,8 @@ struct roundconditions_t targetdamaging_t targetdamaging; UINT8 gachabom_miser; + fixed_t maxspeed; + mobjeflag_t wet_player; // 32 triggers, one bit each, for map execution diff --git a/src/deh_soc.c b/src/deh_soc.c index 61752866b..909d7f7ba 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2949,6 +2949,18 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "SPEEDOMETER")) + { + PARAMCHECK(1); + ty = UCRP_SPEEDOMETER; + re = get_number(params[1]); + + if (re < 100 || re > 999) + { + deh_warning("Speed percent %d out of range (100 - 999) for condition ID %d", re, id+1); + return; + } + } else if (fastcmp(params[0], "TRIGGER")) { PARAMCHECK(1); diff --git a/src/m_cond.c b/src/m_cond.c index a7e8e3c2d..36baab845 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1599,6 +1599,9 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_RINGSEXACT: return (player->hudrings == cn->requirement); + case UCRP_SPEEDOMETER: + return (player->roundconditions.maxspeed >= cn->requirement); + case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); @@ -2369,6 +2372,13 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_RINGSEXACT: return va("with exactly %d Rings", cn->requirement); + case UCRP_SPEEDOMETER: + return va("reach %s%u%% on the speedometer", + (cn->requirement == 999) + ? "" : "at least", + cn->requirement + ); + case UCRP_TRIGGER: return "do something special"; diff --git a/src/m_cond.h b/src/m_cond.h index 5914e07f5..1db82c79d 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -108,8 +108,10 @@ typedef enum UCRP_FINISHTIMEEXACT, // Finish == [time, tics] UCRP_FINISHTIMELEFT, // Finish with at least [time, tics] to spare - UCRP_RINGS, // Finish >= [rings] - UCRP_RINGSEXACT, // Finish == [rings] + UCRP_RINGS, // >= [rings] + UCRP_RINGSEXACT, // == [rings] + + UCRP_SPEEDOMETER, // >= [percentage] UCRP_TRIGGER, // Map execution trigger [id] diff --git a/src/p_user.c b/src/p_user.c index 30266cb02..934614651 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -1920,6 +1920,19 @@ static void P_3dMovement(player_t *player) // Calculates player's speed based on distance-of-a-line formula player->speed = R_PointToDist2(0, 0, player->rmomx, player->rmomy); + const fixed_t topspeed = K_GetKartSpeed(player, false, true); + + if (player->speed > topspeed) + { + const fixed_t convSpeed = (player->speed * 100) / topspeed; + + if (convSpeed > player->roundconditions.maxspeed) + { + player->roundconditions.maxspeed = convSpeed; + //player->roundconditions.checkthisframe = true; -- no, safe to leave until lapchange at worst + } + } + // Monster Iestyn - 04-11-13 // Quadrants are stupid, excessive and broken, let's do this a much simpler way! // Get delta angle from rmom angle and player angle first From 90a65220b2f41dc77eb90359d4229da9de1b0e38 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 23:14:19 +0100 Subject: [PATCH 60/98] UCRP_FALLOFF, UCRP_TOUCHOFFROAD, UCRP_TOUCHSNEAKERPANEL, UCRP_RINGDEBT: Only evaluate the "false" case if the player is exiting or has PF_NOCONTEST --- src/d_player.h | 2 ++ src/m_cond.c | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index c01f82241..1416024be 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -410,6 +410,8 @@ struct roundconditions_t boolean touched_offroad; boolean touched_sneakerpanel; boolean debt_rings; + + // Basically the same, but it's a specific event where no is an easy default boolean tripwire_hyuu; boolean spb_neuter; boolean landmine_dunk; diff --git a/src/m_cond.c b/src/m_cond.c index 36baab845..5afc304c5 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1606,13 +1606,18 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); case UCRP_FALLOFF: - return (player->roundconditions.fell_off == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.fell_off == (cn->requirement == 1)); case UCRP_TOUCHOFFROAD: - return (player->roundconditions.touched_offroad == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.touched_offroad == (cn->requirement == 1)); case UCRP_TOUCHSNEAKERPANEL: - return (player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); case UCRP_RINGDEBT: - return (!(gametyperules & GTR_SPHERES) && (player->roundconditions.debt_rings == (cn->requirement == 1))); + return (!(gametyperules & GTR_SPHERES) + && (cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && (player->roundconditions.debt_rings == (cn->requirement == 1))); case UCRP_TRIPWIREHYUU: return (player->roundconditions.tripwire_hyuu); @@ -2383,13 +2388,13 @@ static const char *M_GetConditionString(condition_t *cn) return "do something special"; case UCRP_FALLOFF: - return (cn->requirement == 1) ? "fall off the course" : "without falling off"; + return (cn->requirement == 1) ? "fall off the course" : "don't fall off the course"; case UCRP_TOUCHOFFROAD: - return (cn->requirement == 1) ? "touch offroad" : "without touching any offroad"; + return (cn->requirement == 1) ? "touch offroad" : "don't touch any offroad"; case UCRP_TOUCHSNEAKERPANEL: - return (cn->requirement == 1) ? "touch a Sneaker Panel" : "without touching any Sneaker Panels"; + return (cn->requirement == 1) ? "touch a Sneaker Panel" : "don't touch any Sneaker Panels"; case UCRP_RINGDEBT: - return (cn->requirement == 1) ? "go into Ring debt" : "without going into Ring debt"; + return (cn->requirement == 1) ? "go into Ring debt" : "don't go into Ring debt"; case UCRP_TRIPWIREHYUU: return "go through Tripwire after getting snared by Hyudoro"; From fa2e54dd1333efeef2f54fcde1417088b0a06531 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 23:21:47 +0100 Subject: [PATCH 61/98] UCRP_FAULTED - `Condition1 = Faulted Yes` - `FAULT during POSITION` - for example, combine with "& finish in 1st" - `Condition1 = Faulted No` - `don't FAULT during POSITION` --- src/d_player.h | 1 + src/deh_soc.c | 3 ++- src/k_respawn.c | 2 ++ src/m_cond.c | 5 +++++ src/m_cond.h | 1 + src/p_user.c | 6 +++--- 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index 1416024be..84076df1b 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -410,6 +410,7 @@ struct roundconditions_t boolean touched_offroad; boolean touched_sneakerpanel; boolean debt_rings; + boolean faulted; // Basically the same, but it's a specific event where no is an easy default boolean tripwire_hyuu; diff --git a/src/deh_soc.c b/src/deh_soc.c index 909d7f7ba..1ff596b18 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2977,7 +2977,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) else if ((offset=0) || fastcmp(params[0], "FALLOFF") || (++offset && fastcmp(params[0], "TOUCHOFFROAD")) || (++offset && fastcmp(params[0], "TOUCHSNEAKERPANEL")) - || (++offset && fastcmp(params[0], "RINGDEBT"))) + || (++offset && fastcmp(params[0], "RINGDEBT")) + || (++offset && fastcmp(params[0], "FAULTED"))) { PARAMCHECK(1); ty = UCRP_FALLOFF + offset; diff --git a/src/k_respawn.c b/src/k_respawn.c index cec0f57df..128f0a198 100644 --- a/src/k_respawn.c +++ b/src/k_respawn.c @@ -111,6 +111,8 @@ void K_DoFault(player_t *player) player->pflags |= PF_FAULT; player->mo->renderflags |= RF_DONTDRAW; player->mo->flags |= MF_NOCLIPTHING; + + player->roundconditions.faulted = true; } } diff --git a/src/m_cond.c b/src/m_cond.c index 5afc304c5..e2be1e96b 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1618,6 +1618,9 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (!(gametyperules & GTR_SPHERES) && (cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && (player->roundconditions.debt_rings == (cn->requirement == 1))); + case UCRP_FAULTED: + return ((cn->requirement == 1 || player->laps >= 1) + && (player->roundconditions.faulted == (cn->requirement == 1))); case UCRP_TRIPWIREHYUU: return (player->roundconditions.tripwire_hyuu); @@ -2395,6 +2398,8 @@ static const char *M_GetConditionString(condition_t *cn) return (cn->requirement == 1) ? "touch a Sneaker Panel" : "don't touch any Sneaker Panels"; case UCRP_RINGDEBT: return (cn->requirement == 1) ? "go into Ring debt" : "don't go into Ring debt"; + case UCRP_FAULTED: + return (cn->requirement == 1) ? "FAULT during POSITION" : "don't FAULT during POSITION"; case UCRP_TRIPWIREHYUU: return "go through Tripwire after getting snared by Hyudoro"; diff --git a/src/m_cond.h b/src/m_cond.h index 1db82c79d..3fcda1761 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -119,6 +119,7 @@ typedef enum UCRP_TOUCHOFFROAD, // Touch offroad (or don't) UCRP_TOUCHSNEAKERPANEL, // Either touch sneaker panel (or don't) UCRP_RINGDEBT, // Go into debt (or don't) + UCRP_FAULTED, // FAULT UCRP_TRIPWIREHYUU, // Go through tripwire with Hyudoro UCRP_SPBNEUTER, // Kill an SPB with Lightning diff --git a/src/p_user.c b/src/p_user.c index 934614651..24631f520 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -1920,11 +1920,11 @@ static void P_3dMovement(player_t *player) // Calculates player's speed based on distance-of-a-line formula player->speed = R_PointToDist2(0, 0, player->rmomx, player->rmomy); - const fixed_t topspeed = K_GetKartSpeed(player, false, true); + const fixed_t topspeedometer = K_GetKartSpeed(player, false, true); - if (player->speed > topspeed) + if (player->speed > topspeedometer) { - const fixed_t convSpeed = (player->speed * 100) / topspeed; + const fixed_t convSpeed = (player->speed * 100) / topspeedometer; if (convSpeed > player->roundconditions.maxspeed) { From e9d4cc2f3a70f78f0497af87b6376835a2fe0140 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 17 Oct 2023 23:25:26 +0100 Subject: [PATCH 62/98] UCRP_TRACKHAZARD, UCRP_FAULTED: Use latestlap instead of current lap when determining elegibility --- src/m_cond.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index e2be1e96b..0e2d00696 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1619,7 +1619,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && (cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) && (player->roundconditions.debt_rings == (cn->requirement == 1))); case UCRP_FAULTED: - return ((cn->requirement == 1 || player->laps >= 1) + return ((cn->requirement == 1 || player->latestlap >= 1) && (player->roundconditions.faulted == (cn->requirement == 1))); case UCRP_TRIPWIREHYUU: @@ -1649,7 +1649,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) // condition can fire while that lap is active // but if you need to NOT get hit on lap X, // it only fires once the lap is complete - if (player->laps <= (requiredlap - cn->requirement)) + if (player->latestlap <= (requiredlap - cn->requirement)) return false; UINT8 requiredbit = 1<<(requiredlap & 7); From a746c4f33fca17396a0917be68c982792289d71e Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:07:30 +0100 Subject: [PATCH 63/98] SPB-related conditions: Make hyphenation consistent --- src/m_cond.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 0e2d00696..d5b12fddf 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2404,7 +2404,7 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_TRIPWIREHYUU: return "go through Tripwire after getting snared by Hyudoro"; case UCRP_SPBNEUTER: - return "shock a Self Propelled Bomb into submission"; + return "shock a Self-Propelled Bomb into submission"; case UCRP_LANDMINEDUNK: return "dunk a Landmine on another racer's head"; case UCRP_HITMIDAIR: @@ -2450,7 +2450,7 @@ static const char *M_GetConditionString(condition_t *cn) work = "Jawz"; break; case UFOD_SPB: - work = "Self Propelled Bombs"; + work = "Self-Propelled Bombs"; break; case UFOD_GACHABOM: work = "Gachabom"; From acdb7fd8690826e2fab139f5bdf49a6c48956f85 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:10:40 +0100 Subject: [PATCH 64/98] UCRP_CHASEDBYSPB True if the player has PF_RINGLOCK (SPB on the HUD). Probably the cleanest implementation. Stacks well with "finish in 1st". --- src/deh_soc.c | 3 ++- src/m_cond.c | 8 ++++++++ src/m_cond.h | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 2dd6c7fd5..068ff5e2a 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2900,7 +2900,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) || (++offset && fastcmp(params[0], "FINISHPERFECT")) || (++offset && fastcmp(params[0], "FINISHALLPRISONS")) || (++offset && fastcmp(params[0], "NOCONTEST")) - || (++offset && fastcmp(params[0], "SMASHUFO"))) + || (++offset && fastcmp(params[0], "SMASHUFO")) + || (++offset && fastcmp(params[0], "CHASEDBYSPB"))) { //PARAMCHECK(1); ty = UCRP_FINISHCOOL + offset; diff --git a/src/m_cond.c b/src/m_cond.c index d5b12fddf..ca73cefc0 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1513,6 +1513,11 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) || specialstageinfo.ufo->health <= 1 ) ); + case UCRP_CHASEDBYSPB: + // The PERFECT implementation would check spbplace, iterate over trackercap, etc. + // But the game already has this handy-dandy SPB signal for us... + // It's only MAYBE invalid in modded context. And mods can already cheat... + return ((player->pflags & PF_RINGLOCK) == PF_RINGLOCK); case UCRP_MAKERETIRE: { @@ -2387,6 +2392,9 @@ static const char *M_GetConditionString(condition_t *cn) cn->requirement ); + case UCRP_CHASEDBYSPB: + return "while chased by a Self-Propelled Bomb"; + case UCRP_TRIGGER: return "do something special"; diff --git a/src/m_cond.h b/src/m_cond.h index 3fcda1761..5ff51018b 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -97,7 +97,9 @@ typedef enum UCRP_FINISHPERFECT, // Finish a perfect race UCRP_FINISHALLPRISONS, // Break all prisons UCRP_NOCONTEST, // No Contest + UCRP_SMASHUFO, // Smash the UFO Catcher + UCRP_CHASEDBYSPB, // Chased by SPB UCRP_MAKERETIRE, // Make another player of [skin] No Contest From 417afd0ac9cbc1bf616f2ced0e5415b5773b15f3 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:27:44 +0100 Subject: [PATCH 65/98] UCRP_FALLOFF, UCRP_TOUCHOFFROAD, UCRP_TOUCHSNEAKERPANEL, UCRP_RINGDEBT: Don't flag if it happens to the player after they exit UCRP_FALLOFF, // Fall off (or don't) UCRP_TOUCHOFFROAD, // Touch offroad (or don't) UCRP_TOUCHSNEAKERPANEL, // Either touch sneaker panel (or don't) UCRP_RINGDEBT, // Go into debt (or don't) --- src/k_kart.c | 2 ++ src/p_inter.c | 5 ++++- src/p_user.c | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/k_kart.c b/src/k_kart.c index 56035c778..1304e2b58 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -1149,6 +1149,7 @@ static void K_UpdateOffroad(player_t *player) player->offroad = offroadstrength; if (player->roundconditions.touched_offroad == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) && player->offroad > (2*offroadstrength) / TICRATE) { player->roundconditions.touched_offroad = true; @@ -6089,6 +6090,7 @@ void K_DoSneaker(player_t *player, INT32 type) const fixed_t intendedboost = FRACUNIT/2; if (player->roundconditions.touched_sneakerpanel == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) && player->floorboost != 0) { player->roundconditions.touched_sneakerpanel = true; diff --git a/src/p_inter.c b/src/p_inter.c index 1d17fd172..3ed158c62 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2377,6 +2377,8 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 type) { + const boolean beforeexit = !(player->exiting || (player->pflags & PF_NOCONTEST)); + if (type == DMG_SPECTATOR && (G_GametypeHasTeams() || G_GametypeHasSpectators())) { P_SetPlayerSpectator(player-players); @@ -2424,7 +2426,8 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, { case DMG_DEATHPIT: // Fell off the stage - if (player->roundconditions.fell_off == false) + if (player->roundconditions.fell_off == false + && beforeexit == false) { player->roundconditions.fell_off = true; player->roundconditions.checkthisframe = true; diff --git a/src/p_user.c b/src/p_user.c index f57f6c976..7126afb9a 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -519,7 +519,9 @@ INT32 P_GivePlayerRings(player_t *player, INT32 num_rings) player->rings += num_rings; - if (player->roundconditions.debt_rings == false && player->rings < 0) + if (player->roundconditions.debt_rings == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) + && player->rings < 0) { player->roundconditions.debt_rings = true; player->roundconditions.checkthisframe = true; From 6dfef245346844dfe18fc43a9f3df9cbe7137a38 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:35:16 +0100 Subject: [PATCH 66/98] P_ArchiveMisc: Guarantee that loading a Grand Prix Backup counts as using a continue --- src/p_saveg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/p_saveg.c b/src/p_saveg.c index 57e55267f..1e6d3469a 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -5738,7 +5738,7 @@ static inline void P_ArchiveMisc(savebuffer_t *save) WRITEUINT32(save->p, grandprixinfo.rank.laps); WRITEUINT32(save->p, grandprixinfo.rank.totalLaps); - WRITEUINT32(save->p, grandprixinfo.rank.continuesUsed); + WRITEUINT32(save->p, (grandprixinfo.rank.continuesUsed + 1)); WRITEUINT32(save->p, grandprixinfo.rank.prisons); WRITEUINT32(save->p, grandprixinfo.rank.totalPrisons); From 813ad52b157fd7da0923487f66613f9f6a711fe3 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:42:02 +0100 Subject: [PATCH 67/98] Guarantee UCRP_PODIUMxx events only occour ig grandprixinfo.gp is true Currently podium is GP only, but that won't be forever --- src/m_cond.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index ca73cefc0..08cb01813 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1468,7 +1468,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (grandprixinfo.gamespeed >= cn->requirement); case UCRP_PODIUMCUP: - if (K_PodiumRanking() == false) + if (grandprixinfo.gp == false || K_PodiumRanking() == false) return false; if (grandprixinfo.cup == NULL || grandprixinfo.cup->id != cn->requirement) @@ -1481,7 +1481,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return true; case UCRP_PODIUMEMERALD: case UCRP_PODIUMPRIZE: - return (K_PodiumRanking() == true + return (grandprixinfo.gp == true + && K_PodiumRanking() == true && grandprixinfo.rank.specialWon == true); case UCRP_FINISHCOOL: From edd3e8bbea03970890aad7451560842eda095934 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:42:59 +0100 Subject: [PATCH 68/98] UCRP_PODIUMNOCONTINUES Fires if no continues have been used (as tracked on the Ceremony Ranking screen) --- src/deh_soc.c | 3 ++- src/m_cond.c | 6 ++++++ src/m_cond.h | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 068ff5e2a..468f1d6c0 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2891,7 +2891,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) } } else if ((offset=0) || fastcmp(params[0], "PODIUMEMERALD") - || (++offset && fastcmp(params[0], "PODIUMPRIZE"))) + || (++offset && fastcmp(params[0], "PODIUMPRIZE")) + || (++offset && fastcmp(params[0], "PODIUMNOCONTINUES"))) { //PARAMCHECK(1); ty = UCRP_PODIUMEMERALD + offset; diff --git a/src/m_cond.c b/src/m_cond.c index 08cb01813..ed0d423be 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1484,6 +1484,10 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (grandprixinfo.gp == true && K_PodiumRanking() == true && grandprixinfo.rank.specialWon == true); + case UCRP_PODIUMNOCONTINUES: + return (grandprixinfo.gp == true + && K_PodiumRanking() == true + && grandprixinfo.rank.continuesUsed == 0); case UCRP_FINISHCOOL: return (player->exiting @@ -2335,6 +2339,8 @@ static const char *M_GetConditionString(condition_t *cn) if (!gamedata->everseenspecial) return "???"; return "collect the prize"; + case UCRP_PODIUMNOCONTINUES: + return "without using any continues"; case UCRP_FINISHCOOL: return "finish in good standing"; diff --git a/src/m_cond.h b/src/m_cond.h index 5ff51018b..6f23f7953 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -92,6 +92,7 @@ typedef enum UCRP_PODIUMCUP, // cup == [cup] [optional: >= grade OR place] UCRP_PODIUMEMERALD, // Get to podium sequence with that cup's emerald UCRP_PODIUMPRIZE, // Get to podium sequence with that cup's bonus (alternate string version of UCRP_PODIUMEMERALD + UCRP_PODIUMNOCONTINUES, // Get to podium sequence without any continues UCRP_FINISHCOOL, // Finish in good standing UCRP_FINISHPERFECT, // Finish a perfect race From 1c887a70418316b4613b0d2cb557262516aa7b50 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 00:45:05 +0100 Subject: [PATCH 69/98] UCRP_LANDMINEDUNK: The official parsing is "Land Mine" --- src/m_cond.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m_cond.c b/src/m_cond.c index ed0d423be..87f97d298 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2421,7 +2421,7 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_SPBNEUTER: return "shock a Self-Propelled Bomb into submission"; case UCRP_LANDMINEDUNK: - return "dunk a Landmine on another racer's head"; + return "dunk a Land Mine on another racer's head"; case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; case UCRP_RETURNMARKTOSENDER: From a7940684f63a405672aa231c60a67498098a251d Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 01:18:34 +0100 Subject: [PATCH 70/98] UCRP_WHIPHYUU Insta-Whip another racer while you yourself are invisible from Hyuudoro, it's shrimple Also updates the phrasing used for UCRP_TRIPWIREHYUU to match ("afflicted by Hyudoro") --- src/d_player.h | 1 + src/deh_soc.c | 1 + src/k_collide.cpp | 8 ++++++++ src/m_cond.c | 6 +++++- src/m_cond.h | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/d_player.h b/src/d_player.h index ad9adf9b7..afe193528 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -414,6 +414,7 @@ struct roundconditions_t // Basically the same, but it's a specific event where no is an easy default boolean tripwire_hyuu; + boolean whip_hyuu; boolean spb_neuter; boolean landmine_dunk; boolean hit_midair; diff --git a/src/deh_soc.c b/src/deh_soc.c index 468f1d6c0..4f47e1480 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2993,6 +2993,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) re = 0; } else if ((offset=0) || fastcmp(params[0], "TRIPWIREHYUU") + || (++offset && fastcmp(params[0], "WHIPHYUU")) || (++offset && fastcmp(params[0], "SPBNEUTER")) || (++offset && fastcmp(params[0], "LANDMINEDUNK")) || (++offset && fastcmp(params[0], "HITMIDAIR")) diff --git a/src/k_collide.cpp b/src/k_collide.cpp index 5afd75d18..503481bb3 100644 --- a/src/k_collide.cpp +++ b/src/k_collide.cpp @@ -910,6 +910,14 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim) K_AddHitLag(victim, victimHitlag, true); K_AddHitLag(attacker, attackerHitlag, false); shield->hitlag = attacker->hitlag; + + if (attackerPlayer->roundconditions.whip_hyuu == false + && attackerPlayer->hyudorotimer > 0) + { + attackerPlayer->roundconditions.whip_hyuu = true; + attackerPlayer->roundconditions.checkthisframe = true; + } + return true; } return false; diff --git a/src/m_cond.c b/src/m_cond.c index 87f97d298..a0ac581be 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1634,6 +1634,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_TRIPWIREHYUU: return (player->roundconditions.tripwire_hyuu); + case UCRP_WHIPHYUU: + return (player->roundconditions.whip_hyuu); case UCRP_SPBNEUTER: return (player->roundconditions.spb_neuter); case UCRP_LANDMINEDUNK: @@ -2417,7 +2419,9 @@ static const char *M_GetConditionString(condition_t *cn) return (cn->requirement == 1) ? "FAULT during POSITION" : "don't FAULT during POSITION"; case UCRP_TRIPWIREHYUU: - return "go through Tripwire after getting snared by Hyudoro"; + return "go through Tripwire while afflicted by Hyudoro"; + case UCRP_WHIPHYUU: + return "Insta-Whip a racer while afflicted by Hyudoro"; case UCRP_SPBNEUTER: return "shock a Self-Propelled Bomb into submission"; case UCRP_LANDMINEDUNK: diff --git a/src/m_cond.h b/src/m_cond.h index 6f23f7953..d78ea4791 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -125,6 +125,7 @@ typedef enum UCRP_FAULTED, // FAULT UCRP_TRIPWIREHYUU, // Go through tripwire with Hyudoro + UCRP_WHIPHYUU, // Use Insta-Whip with Hyudoro UCRP_SPBNEUTER, // Kill an SPB with Lightning UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem From 065fc0b89c15ef3713e0084942202c3b7673b7df Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 01:57:57 +0100 Subject: [PATCH 71/98] UCRP_RETURNMARKTOSENDER: Don't fire checkthisframe if it's already true --- src/p_inter.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/p_inter.c b/src/p_inter.c index 3ed158c62..ffcb5c647 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2705,7 +2705,8 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da && inflictor->threshold == KITEM_EGGMAN && !P_MobjWasRemoved(inflictor->tracer) && inflictor->tracer != source - && inflictor->tracer->player) + && inflictor->tracer->player + && inflictor->tracer->player->roundconditions.returntosender_mark == false) { inflictor->tracer->player->roundconditions.returntosender_mark = true; inflictor->tracer->player->roundconditions.checkthisframe = true; From 83acdaa0cfd8b07ae3448c7884f23edfdb03e5b8 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 02:05:12 +0100 Subject: [PATCH 72/98] UCRP_HITDRAFTERLOOKBACK "hit a racer drafting off you while looking back at them" Self-explanatory set of conditions Wrote a commented out more compicated check for valid angles between player and target, but lastdraft probably is good enough for these purpses --- src/d_player.h | 1 + src/deh_soc.c | 1 + src/m_cond.c | 4 ++++ src/m_cond.h | 1 + src/p_inter.c | 10 ++++++++++ 5 files changed, 17 insertions(+) diff --git a/src/d_player.h b/src/d_player.h index afe193528..d17d84868 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -418,6 +418,7 @@ struct roundconditions_t boolean spb_neuter; boolean landmine_dunk; boolean hit_midair; + boolean hit_drafter_lookback; boolean returntosender_mark; UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; diff --git a/src/deh_soc.c b/src/deh_soc.c index 4f47e1480..e1d147f8c 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2997,6 +2997,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) || (++offset && fastcmp(params[0], "SPBNEUTER")) || (++offset && fastcmp(params[0], "LANDMINEDUNK")) || (++offset && fastcmp(params[0], "HITMIDAIR")) + || (++offset && fastcmp(params[0], "HITDRAFTERLOOKBACK")) || (++offset && fastcmp(params[0], "RETURNMARKTOSENDER"))) { //PARAMCHECK(1); diff --git a/src/m_cond.c b/src/m_cond.c index a0ac581be..a621c6704 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1642,6 +1642,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (player->roundconditions.landmine_dunk); case UCRP_HITMIDAIR: return (player->roundconditions.hit_midair); + case UCRP_HITDRAFTERLOOKBACK: + return (player->roundconditions.hit_drafter_lookback); case UCRP_RETURNMARKTOSENDER: return (player->roundconditions.returntosender_mark); @@ -2428,6 +2430,8 @@ static const char *M_GetConditionString(condition_t *cn) return "dunk a Land Mine on another racer's head"; case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; + case UCRP_HITDRAFTERLOOKBACK: + return "hit a racer drafting off you while looking back at them"; case UCRP_RETURNMARKTOSENDER: return "when cursed with Eggmark, blow up the racer responsible"; diff --git a/src/m_cond.h b/src/m_cond.h index d78ea4791..989a7a3ff 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -129,6 +129,7 @@ typedef enum UCRP_SPBNEUTER, // Kill an SPB with Lightning UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem + UCRP_HITDRAFTERLOOKBACK, // Hit a player that's behind you, while looking back at them, and they're drafting off you UCRP_RETURNMARKTOSENDER, // Hit the player responsible for Eggman Marking you with that explosion UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) diff --git a/src/p_inter.c b/src/p_inter.c index ffcb5c647..6e965b542 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2699,6 +2699,16 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da source->player->roundconditions.checkthisframe = true; } + if (source->player->roundconditions.hit_drafter_lookback == false + && source != target + && target->player->lastdraft == (source->player - players) + && (K_GetKartButtons(source->player) & BT_LOOKBACK) == BT_LOOKBACK + /*&& (AngleDelta(K_MomentumAngle(source), R_PointToAngle2(source->x, source->y, target->x, target->y)) > ANGLE_90)*/) + { + source->player->roundconditions.hit_drafter_lookback = true; + source->player->roundconditions.checkthisframe = true; + } + if (source == target && !P_MobjWasRemoved(inflictor) && inflictor->type == MT_SPBEXPLOSION From 0f06807c65f7d4983c2d96d422024677d856d99e Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 02:05:33 +0100 Subject: [PATCH 73/98] UCRP_HITMIDAIR: Don't count hitting yourself --- src/p_inter.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/p_inter.c b/src/p_inter.c index 6e965b542..bcd88c452 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2690,6 +2690,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da if (source && source->player) { if (source->player->roundconditions.hit_midair == false + && source != target && inflictor && K_IsMissileOrKartItem(inflictor) && target->player->airtime > TICRATE/2 From a6ef8d357c69651a0f7ff2776006bf07bb0690f5 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 18 Oct 2023 02:32:46 +0100 Subject: [PATCH 74/98] UCRP_DRAFTDURATION "consistently draft off other racers for [parameter] seconds" Actually counts leniency tics too, but if the leniency hits 0 it gets reset. --- src/d_player.h | 3 +++ src/deh_soc.c | 12 ++++++++++++ src/k_kart.c | 10 +++++++++- src/m_cond.c | 9 ++++++--- src/m_cond.h | 1 + 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index d17d84868..1a82272e3 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -429,6 +429,9 @@ struct roundconditions_t fixed_t maxspeed; + tic_t continuousdraft; + tic_t continuousdraft_best; + mobjeflag_t wet_player; // 32 triggers, one bit each, for map execution diff --git a/src/deh_soc.c b/src/deh_soc.c index e1d147f8c..377eccb7f 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2966,6 +2966,18 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "DRAFTDURATION")) + { + PARAMCHECK(1); + ty = UCRP_DRAFTDURATION; + re = get_number(params[1]); + + if (re < 5) + { + deh_warning("Duration %d seconds too low for condition ID %d", re, id+1); + return; + } + } else if (fastcmp(params[0], "TRIGGER")) { PARAMCHECK(1); diff --git a/src/k_kart.c b/src/k_kart.c index 1304e2b58..00bc70862 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -1466,7 +1466,8 @@ static void K_UpdateDraft(player_t *player) if (K_TryDraft(player, otherPlayer->mo, minDist, draftdistance, leniency) == true) { - return; // Finished doing our draft. + //return; + goto draftdurationhandling; // Finished doing our draft. } } } @@ -1501,7 +1502,14 @@ static void K_UpdateDraft(player_t *player) { player->draftpower = 0; player->lastdraft = -1; + player->roundconditions.continuousdraft = 0; + return; } + +draftdurationhandling: + player->roundconditions.continuousdraft++; + if (player->roundconditions.continuousdraft > player->roundconditions.continuousdraft_best) + player->roundconditions.continuousdraft_best = player->roundconditions.continuousdraft; } void K_KartPainEnergyFling(player_t *player) diff --git a/src/m_cond.c b/src/m_cond.c index a621c6704..02a935d6a 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1611,6 +1611,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UCRP_SPEEDOMETER: return (player->roundconditions.maxspeed >= cn->requirement); + case UCRP_DRAFTDURATION: + return (player->roundconditions.continuousdraft_best >= ((tic_t)cn->requirement)*TICRATE); case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); @@ -2359,6 +2361,8 @@ static const char *M_GetConditionString(condition_t *cn) if (!gamedata->everseenspecial) return NULL; return "smash the UFO Catcher"; + case UCRP_CHASEDBYSPB: + return "while chased by a Self-Propelled Bomb"; case UCRP_MAKERETIRE: { @@ -2402,9 +2406,8 @@ static const char *M_GetConditionString(condition_t *cn) ? "" : "at least", cn->requirement ); - - case UCRP_CHASEDBYSPB: - return "while chased by a Self-Propelled Bomb"; + case UCRP_DRAFTDURATION: + return va("consistently draft off other racers for %u seconds", cn->requirement); case UCRP_TRIGGER: return "do something special"; diff --git a/src/m_cond.h b/src/m_cond.h index 989a7a3ff..bcf77fe21 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -115,6 +115,7 @@ typedef enum UCRP_RINGSEXACT, // == [rings] UCRP_SPEEDOMETER, // >= [percentage] + UCRP_DRAFTDURATION, // >= [time, seconds] UCRP_TRIGGER, // Map execution trigger [id] From 5680e00deb624c49def3d810e9264b22b36b068e Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 16:13:01 +0100 Subject: [PATCH 75/98] P_TrackRoundConditionTargetDamage, Obj_SpecialUFODamage: Fix incorrect non-g_localplayers indexing into player table Destroying the UFO has now been promoted to a full deferred condition check --- src/objects/ufo.c | 13 ++----------- src/p_inter.c | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/objects/ufo.c b/src/objects/ufo.c index bff265b66..a52b112ed 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -18,6 +18,7 @@ #include "../k_objects.h" #include "../m_random.h" #include "../p_local.h" +#include "../m_cond.h" #include "../r_main.h" #include "../s_sound.h" #include "../g_game.h" @@ -923,17 +924,7 @@ boolean Obj_SpecialUFODamage(mobj_t *ufo, mobj_t *inflictor, mobj_t *source, UIN // Destroy the UFO parts, and make the emerald collectible! UFOKillPieces(ufo); - { - UINT8 i; - for (i = 0; i <= splitscreen; i++) - { - if (!playeringame[g_localplayers[i]]) - continue; - if (players[g_localplayers[i]].spectator) - continue; - players[i].roundconditions.checkthisframe = true; - } - } + gamedata->deferredconditioncheck = true; // Check Challenges! ufo->flags = (ufo->flags & ~MF_SHOOTABLE) | (MF_SPECIAL|MF_PICKUPFROMBELOW); ufo->shadowscale = FRACUNIT/3; diff --git a/src/p_inter.c b/src/p_inter.c index bcd88c452..c6a2eea6c 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -997,10 +997,10 @@ void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging) continue; if (players[g_localplayers[i]].spectator) continue; - players[i].roundconditions.targetdamaging |= targetdamaging; + players[g_localplayers[i]].roundconditions.targetdamaging |= targetdamaging; /* -- the following isn't needed because we can just check for targetdamaging == UFOD_GACHABOM if (targetdamaging != UFOD_GACHABOM) - players[i].roundconditions.gachabom_miser = 0xFF; + players[g_localplayers[i]].roundconditions.gachabom_miser = 0xFF; */ } } From de79dfe9834722b8f2fcfa7a1fa2b0ce95416262 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 17:27:33 +0100 Subject: [PATCH 76/98] UCRP_MAPDESTROYOBJECTS You have to destroy all objects of a certain list of types. UNLIKE OTHER CHALLENGES, this is dependent on a mapheader parameter!! The author of this commit just didn't want to create too laggy a condition check. - `Condition1 = MapDestroyObjects RR_ESPRESSOLANE tables & chairs - Map RR_ESPRESSOLANE - DestroyObjectsForChallenges = MT_ESPTABLE,MT_ESPCHAIR --- src/deh_soc.c | 32 +++++++++++++++++++++++++- src/doomstat.h | 5 +++++ src/g_game.c | 1 + src/m_cond.c | 15 +++++++++++++ src/m_cond.h | 1 + src/p_mobj.c | 61 +++++++++++++++++++++++++++++++++++++++----------- src/p_saveg.c | 4 ++++ src/p_setup.c | 3 +++ 8 files changed, 108 insertions(+), 14 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 377eccb7f..bf9d4a9a0 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -1383,6 +1383,28 @@ void readlevelheader(MYFILE *f, char * name) } else if (fastcmp(word, "GRAVITY")) mapheaderinfo[num]->gravity = FLOAT_TO_FIXED(atof(word2)); + else if (fastcmp(word, "DESTROYOBJECTSFORCHALLENGES")) + { + if (fastcmp(word2, "NONE")) + { + mapheaderinfo[num]->destroyforchallenge_size = 0; + } + else + { + UINT8 j = 0; // i was declared elsewhere + tmp = strtok(word2, ","); + do { + if (j >= MAXDESTRUCTIBLES) + break; + mapheaderinfo[num]->destroyforchallenge[j] = get_mobjtype(word2); + j++; + } while ((tmp = strtok(NULL,",")) != NULL); + + if (tmp != NULL) + deh_warning("Level header %d: additional destructibles past %d discarded", num, MAXDESTRUCTIBLES); + mapheaderinfo[num]->destroyforchallenge_size = j; + } + } else deh_warning("Level header %d: unknown word '%s'", num, word); } @@ -2500,7 +2522,6 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) ty = UC_PASSWORD; stringvar = Z_StrDup(spos); - re = -1; } if (ty != UC_NONE) @@ -2531,6 +2552,15 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) if (spos && *spos) stringvar = Z_StrDup(spos); } + else if (fastcmp(params[0], "MAPDESTROYOBJECTS")) + { + PARAMCHECK(1); + EXTENDEDPARAMCHECK(spos, 2); + ty = UCRP_MAPDESTROYOBJECTS; + re = G_MapNumber(params[1]); + + stringvar = Z_StrDup(spos); + } if (ty != UC_NONE) goto setcondition; diff --git a/src/doomstat.h b/src/doomstat.h index b4a2c2f11..edf5fe334 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -440,6 +440,7 @@ struct staffbrief_t }; #define MAXMUSNAMES 3 // maximum definable music tracks per level +#define MAXDESTRUCTIBLES 3 #define MAXHEADERFOLLOWERS 32 struct mapheader_lighting_t @@ -537,6 +538,9 @@ struct mapheader_t UINT8 precutscenenum; ///< Cutscene number to play BEFORE a level starts. UINT8 cutscenenum; ///< Cutscene number to use, 0 for none. + mobjtype_t destroyforchallenge[MAXDESTRUCTIBLES]; ///< Assistive for UCRP_MAPDESTROYOBJECTS + UINT8 destroyforchallenge_size; ///< Number for above + UINT32 _saveid; ///< Purely assistive in gamedata save processes UINT16 cache_spraycan; ///< Cached Spraycan ID UINT16 cache_maplock; ///< Cached Unlockable ID @@ -722,6 +726,7 @@ extern INT32 luabanks[NUM_LUABANKS]; extern INT32 nummaprings; //keep track of spawned rings/coins extern UINT8 nummapspraycans; +extern UINT16 numchallengedestructibles; extern UINT32 bluescore; ///< Blue Team Scores extern UINT32 redscore; ///< Red Team Scores diff --git a/src/g_game.c b/src/g_game.c index 61cf029f4..4193ffe86 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -222,6 +222,7 @@ UINT32 bluescore, redscore; // CTF and Team Match team scores INT32 nummaprings = 0; UINT8 nummapspraycans = 0; +UINT16 numchallengedestructibles = 0; // Elminates unnecessary searching. boolean CheckForBustableBlocks; diff --git a/src/m_cond.c b/src/m_cond.c index 02a935d6a..ccdefe978 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1523,6 +1523,11 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) // But the game already has this handy-dandy SPB signal for us... // It's only MAYBE invalid in modded context. And mods can already cheat... return ((player->pflags & PF_RINGLOCK) == PF_RINGLOCK); + case UCRP_MAPDESTROYOBJECTS: + return ( + gamemap == cn->requirement+1 + && numchallengedestructibles == UINT16_MAX + ); case UCRP_MAKERETIRE: { @@ -2363,6 +2368,16 @@ static const char *M_GetConditionString(condition_t *cn) return "smash the UFO Catcher"; case UCRP_CHASEDBYSPB: return "while chased by a Self-Propelled Bomb"; + case UCRP_MAPDESTROYOBJECTS: + { + if (cn->stringvar == NULL) + return va("INVALID DESTROY CONDITION \"%d\"", cn->type); + + title = M_BuildConditionTitle(cn->requirement); + work = va("%s: destroy all the %s", title, cn->stringvar); + Z_Free(title); + return work; + } case UCRP_MAKERETIRE: { diff --git a/src/m_cond.h b/src/m_cond.h index bcf77fe21..24e26895c 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -101,6 +101,7 @@ typedef enum UCRP_SMASHUFO, // Smash the UFO Catcher UCRP_CHASEDBYSPB, // Chased by SPB + UCRP_MAPDESTROYOBJECTS, // LEVELNAME: Destroy all [object names] -- CAUTION: You have to add to the level's header too to get them successfully tracked! UCRP_MAKERETIRE, // Make another player of [skin] No Contest diff --git a/src/p_mobj.c b/src/p_mobj.c index 0af0a484a..524c0f8a8 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -11606,18 +11606,40 @@ void P_RemoveMobj(mobj_t *mobj) mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker; // needed for P_UnsetThingPosition, etc. to work. // Rings only, please! - if (mobj->spawnpoint && - (mobj->type == MT_RING - || mobj->type == MT_BLUESPHERE) - && !(mobj->flags2 & MF2_DONTRESPAWN)) + if (mobj->spawnpoint == NULL) + ; + else { - //CONS_Printf("added to queue at tic %d\n", leveltime); - itemrespawnque[iquehead] = mobj->spawnpoint; - itemrespawntime[iquehead] = leveltime; - iquehead = (iquehead+1)&(ITEMQUESIZE-1); - // lose one off the end? - if (iquehead == iquetail) - iquetail = (iquetail+1)&(ITEMQUESIZE-1); + if ((mobj->type == MT_RING + || mobj->type == MT_BLUESPHERE) + && !(mobj->flags2 & MF2_DONTRESPAWN)) + { + //CONS_Printf("added to queue at tic %d\n", leveltime); + itemrespawnque[iquehead] = mobj->spawnpoint; + itemrespawntime[iquehead] = leveltime; + iquehead = (iquehead+1)&(ITEMQUESIZE-1); + // lose one off the end? + if (iquehead == iquetail) + iquetail = (iquetail+1)&(ITEMQUESIZE-1); + } + + if (numchallengedestructibles && numchallengedestructibles != UINT16_MAX) + { + UINT8 i; + for (i = 0; i < mapheaderinfo[gamemap-1]->destroyforchallenge_size; i++) + { + if (mobj->type != mapheaderinfo[gamemap-1]->destroyforchallenge[i]) + continue; + + if ((--numchallengedestructibles) == 0) + { + numchallengedestructibles = UINT16_MAX; + gamedata->deferredconditioncheck = true; + } + + break; + } + } } if (P_IsTrackerType(mobj->type)) @@ -14148,12 +14170,12 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) return true; } -static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, fixed_t z, mobjtype_t i) +static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, fixed_t z, mobjtype_t type) { mobj_t *mobj = NULL; size_t arg = SIZE_MAX; - mobj = P_SpawnMobj(x, y, z, i); + mobj = P_SpawnMobj(x, y, z, type); mobj->spawnpoint = mthing; mobj->angle = FixedAngle(mthing->angle << FRACBITS); @@ -14237,6 +14259,19 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, mobj->flags2 |= MF2_OBJECTFLIP; } + if (mapheaderinfo[gamemap-1]->destroyforchallenge_size && numchallengedestructibles != UINT16_MAX) + { + UINT8 i; + for (i = 0; i < mapheaderinfo[gamemap-1]->destroyforchallenge_size; i++) + { + if (type != mapheaderinfo[gamemap-1]->destroyforchallenge[i]) + continue; + + numchallengedestructibles++; + break; + } + } + return mobj; } diff --git a/src/p_saveg.c b/src/p_saveg.c index 1e6d3469a..29229935f 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -6064,6 +6064,8 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) WRITEUINT32(save->p, darktimer); WRITEFIXED(save->p, darkness); + WRITEUINT16(save->p, numchallengedestructibles); + // Is it paused? if (paused) WRITEUINT8(save->p, 0x2f); @@ -6246,6 +6248,8 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) darktimer = READUINT32(save->p); darkness = READFIXED(save->p); + numchallengedestructibles = READUINT16(save->p); + // Is it paused? if (READUINT8(save->p) == 0x2f) paused = true; diff --git a/src/p_setup.c b/src/p_setup.c index 049857c93..1ebebb383 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -463,6 +463,8 @@ static void P_ClearSingleMapHeaderInfo(INT16 num) mapheaderinfo[num]->justPlayed = 0; mapheaderinfo[num]->anger = 0; + mapheaderinfo[num]->destroyforchallenge_size = 0; + mapheaderinfo[num]->cache_spraycan = UINT16_MAX; mapheaderinfo[num]->cache_maplock = MAXUNLOCKABLES; @@ -7553,6 +7555,7 @@ static void P_InitLevelSettings(void) battleprisons = false; nummapspraycans = 0; + numchallengedestructibles = 0; // circuit, race and competition stuff numcheatchecks = 0; From d2d7830afc388191d60787d6ee91875d974807e1 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 18:06:27 +0100 Subject: [PATCH 77/98] UCRP_HASFOLLOWER Self-explanatory. - `Condition1 = HasFollower Buzz_Bomber` - "with Buzz Bomber in tow" - Combine with Podium checks or other completion-based challenges. --- src/deh_soc.c | 7 +++++++ src/m_cond.c | 34 +++++++++++++++++++++++++++++++++- src/m_cond.h | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index c3c67bcac..0d329a05b 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2844,6 +2844,13 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "HASFOLLOWER")) + { + PARAMCHECK(1); + ty = UCRP_HASFOLLOWER; + stringvar = Z_StrDup(params[1]); + re = -1; + } else if (fastcmp(params[0], "ISDIFFICULTY")) { //PARAMCHECK(1); diff --git a/src/m_cond.c b/src/m_cond.c index ccdefe978..e2dadb6d2 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1103,7 +1103,7 @@ void M_FinaliseGameData(void) void M_UpdateConditionSetsPending(void) { - UINT32 i, j; + UINT32 i, j, k; conditionset_t *c; condition_t *cn; @@ -1139,6 +1139,29 @@ void M_UpdateConditionSetsPending(void) break; } + case UCRP_HASFOLLOWER: + { + // match deh_soc readfollower() + for (k = 0; cn->stringvar[k]; k++) + { + if (cn->stringvar[k] == '_') + cn->stringvar[k] = ' '; + } + + cn->requirement = K_FollowerAvailable(cn->stringvar); + + if (cn->requirement < 0) + { + CONS_Alert(CONS_WARNING, "UC TYPE %u: Invalid character %s for condition ID %d", cn->type, cn->stringvar, cn->id+1); + continue; + } + + Z_Free(cn->stringvar); + cn->stringvar = NULL; + + break; + } + case UCRP_WETPLAYER: { if (cn->extrainfo1) @@ -1460,6 +1483,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) skins[player->skin].kartweight, skins[player->skin].flags ) == (unsigned)cn->requirement); + case UCRP_HASFOLLOWER: + return (cn->requirement != -1 && player->followerskin == cn->requirement); case UCRP_ISDIFFICULTY: if (grandprixinfo.gp == false) return (gamespeed >= cn->requirement); @@ -2272,6 +2297,13 @@ static const char *M_GetConditionString(condition_t *cn) return va("as %s", work); case UCRP_ISENGINECLASS: return va("with engine class %c", 'A' + cn->requirement); + case UCRP_HASFOLLOWER: + if (cn->requirement < 0 || !followers[cn->requirement].name[0]) + return va("INVALID FOLLOWER CONDITION \"%d:%d\"", cn->type, cn->requirement); + work = (K_FollowerUsable(cn->requirement)) + ? followers[cn->requirement].name + : "???"; + return va("with %s in tow", work); case UCRP_ISDIFFICULTY: { const char *speedtext = ""; diff --git a/src/m_cond.h b/src/m_cond.h index 24e26895c..a13bd014c 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -87,6 +87,7 @@ typedef enum UCRP_ISCHARACTER, // character == [skin] UCRP_ISENGINECLASS, // engine class [class] + UCRP_HASFOLLOWER, // follower == [followerskin] UCRP_ISDIFFICULTY, // difficulty >= [difficulty] UCRP_PODIUMCUP, // cup == [cup] [optional: >= grade OR place] From 6211c0b22222e23febe83d8e358f6ff0f05cacd2 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 18:43:40 +0100 Subject: [PATCH 78/98] UCRP_GIANTRACERSHRUNKENORBI "hit a giant racer with a shrunken Orbinaut" Does what it says on the tin --- src/d_player.h | 1 + src/deh_soc.c | 1 + src/m_cond.c | 4 ++++ src/m_cond.h | 1 + src/p_inter.c | 11 +++++++++++ 5 files changed, 18 insertions(+) diff --git a/src/d_player.h b/src/d_player.h index 593b3fee3..65bfdd2b9 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -419,6 +419,7 @@ struct roundconditions_t boolean landmine_dunk; boolean hit_midair; boolean hit_drafter_lookback; + boolean giant_foe_shrunken_orbi; boolean returntosender_mark; UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; diff --git a/src/deh_soc.c b/src/deh_soc.c index 0d329a05b..2e88c8107 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3047,6 +3047,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) || (++offset && fastcmp(params[0], "LANDMINEDUNK")) || (++offset && fastcmp(params[0], "HITMIDAIR")) || (++offset && fastcmp(params[0], "HITDRAFTERLOOKBACK")) + || (++offset && fastcmp(params[0], "GIANTRACERSHRUNKENORBI")) || (++offset && fastcmp(params[0], "RETURNMARKTOSENDER"))) { //PARAMCHECK(1); diff --git a/src/m_cond.c b/src/m_cond.c index e2dadb6d2..aacb6b609 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1676,6 +1676,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (player->roundconditions.hit_midair); case UCRP_HITDRAFTERLOOKBACK: return (player->roundconditions.hit_drafter_lookback); + case UCRP_GIANTRACERSHRUNKENORBI: + return (player->roundconditions.giant_foe_shrunken_orbi); case UCRP_RETURNMARKTOSENDER: return (player->roundconditions.returntosender_mark); @@ -2482,6 +2484,8 @@ static const char *M_GetConditionString(condition_t *cn) return "hit another racer with a projectile while you're both in the air"; case UCRP_HITDRAFTERLOOKBACK: return "hit a racer drafting off you while looking back at them"; + case UCRP_GIANTRACERSHRUNKENORBI: + return "hit a giant racer with a shrunken Orbinaut"; case UCRP_RETURNMARKTOSENDER: return "when cursed with Eggmark, blow up the racer responsible"; diff --git a/src/m_cond.h b/src/m_cond.h index a13bd014c..7cfc0685a 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -133,6 +133,7 @@ typedef enum UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem UCRP_HITDRAFTERLOOKBACK, // Hit a player that's behind you, while looking back at them, and they're drafting off you + UCRP_GIANTRACERSHRUNKENORBI, // Hit a giant racer with a shrunken Orbinaut UCRP_RETURNMARKTOSENDER, // Hit the player responsible for Eggman Marking you with that explosion UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) diff --git a/src/p_inter.c b/src/p_inter.c index d487bd20e..52695a598 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2713,6 +2713,17 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da source->player->roundconditions.checkthisframe = true; } + if (source->player->roundconditions.giant_foe_shrunken_orbi == false + && source != target + && player->growshrinktimer > 0 + && !P_MobjWasRemoved(inflictor) + && inflictor->type == MT_ORBINAUT + && inflictor->scale < FixedMul((FRACUNIT + SHRINK_SCALE), mapobjectscale * 2)) // halfway between base scale and shrink scale, a little bit of leeway + { + source->player->roundconditions.giant_foe_shrunken_orbi = true; + source->player->roundconditions.checkthisframe = true; + } + if (source == target && !P_MobjWasRemoved(inflictor) && inflictor->type == MT_SPBEXPLOSION From 42233cfb9ed17a42e18575f783d16a1d27735694 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 19:44:37 +0100 Subject: [PATCH 79/98] UCRP_GROWCONSECUTIVEBEAMS - Condition1 = GrowConsecutiveBeams 4 - "touch the blue beams from your own Shrink at least 4 times before returning to normal size --- src/d_player.h | 3 +++ src/deh_soc.c | 12 ++++++++++++ src/k_kart.c | 1 + src/m_cond.c | 4 ++++ src/m_cond.h | 1 + src/objects/shrink.c | 10 ++++++++++ 6 files changed, 31 insertions(+) diff --git a/src/d_player.h b/src/d_player.h index 65bfdd2b9..4676f1076 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -433,6 +433,9 @@ struct roundconditions_t tic_t continuousdraft; tic_t continuousdraft_best; + UINT8 consecutive_grow_lasers; + UINT8 best_consecutive_grow_lasers; + mobjeflag_t wet_player; // 32 triggers, one bit each, for map execution diff --git a/src/deh_soc.c b/src/deh_soc.c index 2e88c8107..0d21c7a76 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3015,6 +3015,18 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "GROWCONSECUTIVEBEAMS")) + { + PARAMCHECK(1); + ty = UCRP_GROWCONSECUTIVEBEAMS; + re = get_number(params[1]); + + if (re < 2 || re > UINT8_MAX) + { + deh_warning("Touch count %d out of range (2 - %u) for condition ID %d", re, UINT8_MAX, id+1); + return; + } + } else if (fastcmp(params[0], "TRIGGER")) { PARAMCHECK(1); diff --git a/src/k_kart.c b/src/k_kart.c index 405e9ff70..688f96471 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -3842,6 +3842,7 @@ void K_RemoveGrowShrink(player_t *player) } player->growshrinktimer = 0; + player->roundconditions.consecutive_grow_lasers = 0; } boolean K_IsBigger(mobj_t *compare, mobj_t *other) diff --git a/src/m_cond.c b/src/m_cond.c index aacb6b609..095b44598 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1643,6 +1643,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (player->roundconditions.maxspeed >= cn->requirement); case UCRP_DRAFTDURATION: return (player->roundconditions.continuousdraft_best >= ((tic_t)cn->requirement)*TICRATE); + case UCRP_GROWCONSECUTIVEBEAMS: + return (player->roundconditions.best_consecutive_grow_lasers >= cn->requirement); case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); @@ -2457,6 +2459,8 @@ static const char *M_GetConditionString(condition_t *cn) ); case UCRP_DRAFTDURATION: return va("consistently draft off other racers for %u seconds", cn->requirement); + case UCRP_GROWCONSECUTIVEBEAMS: + return va("touch the blue beams from your own Shrink at least %u times before returning to normal size", cn->requirement); case UCRP_TRIGGER: return "do something special"; diff --git a/src/m_cond.h b/src/m_cond.h index 7cfc0685a..7b9a9e3ec 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -118,6 +118,7 @@ typedef enum UCRP_SPEEDOMETER, // >= [percentage] UCRP_DRAFTDURATION, // >= [time, seconds] + UCRP_GROWCONSECUTIVEBEAMS, // touch more than n times consecutively UCRP_TRIGGER, // Map execution trigger [id] diff --git a/src/objects/shrink.c b/src/objects/shrink.c index 23613ff52..04b703328 100644 --- a/src/objects/shrink.c +++ b/src/objects/shrink.c @@ -547,6 +547,16 @@ boolean Obj_ShrinkLaserCollide(mobj_t *gun, mobj_t *victim) victim->player->growshrinktimer += 6*TICRATE; S_StartSound(victim, sfx_kc5a); + if (victim->player->roundconditions.consecutive_grow_lasers < UINT8_MAX) + { + victim->player->roundconditions.consecutive_grow_lasers++; + if (victim->player->roundconditions.consecutive_grow_lasers > victim->player->roundconditions.best_consecutive_grow_lasers) + { + victim->player->roundconditions.best_consecutive_grow_lasers + = victim->player->roundconditions.consecutive_grow_lasers; + } + } + if (prevTimer <= 0) { victim->scalespeed = mapobjectscale/TICRATE; From d8e6e1d1a49ea1a807403640a712b359ab5782ed Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 20:22:17 +0100 Subject: [PATCH 80/98] UC_TOTALTUMBLETIME `Condition1 = TotalTumbleTime 30*TICRATE "tumble through the air for 30:00" Also makes all the time-based non-playing Conditions use get_number so TICRATE can be provided --- src/deh_soc.c | 14 ++++++++++---- src/k_kart.c | 8 ++++++++ src/m_cond.c | 10 ++++++++-- src/m_cond.h | 2 ++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 0d21c7a76..49a8607c8 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2577,8 +2577,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) else if (fastcmp(params[0], "PLAYTIME")) { PARAMCHECK(1); - ty = UC_PLAYTIME + offset; - re = atoi(params[1]); + ty = UC_PLAYTIME; + re = get_number(params[1]); } else if (fastcmp(params[0], "ROUNDSPLAYED")) { @@ -2624,6 +2624,12 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "TOTALTUMBLETIME")) + { + PARAMCHECK(1); + ty = UC_TOTALTUMBLETIME; + re = get_number(params[1]); + } else if (fastcmp(params[0], "GAMECLEAR")) { ty = UC_GAMECLEAR; @@ -2633,7 +2639,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(1); ty = UC_OVERALLTIME; - re = atoi(params[1]); + re = get_number(params[1]); } else if ((offset=0) || fastcmp(params[0], "MAPVISITED") || (++offset && fastcmp(params[0], "MAPBEATEN")) @@ -2655,7 +2661,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(2); ty = UC_MAPTIME; - re = atoi(params[2]); + re = get_number(params[2]); x1 = G_MapNumber(params[1]); if (x1 >= nummapheaders) diff --git a/src/k_kart.c b/src/k_kart.c index 688f96471..182e0a7cb 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -7658,6 +7658,14 @@ void K_KartPlayerHUDUpdate(player_t *player) } else player->karthud[khud_finish] = 0; + + if (demo.playback == false && P_IsLocalPlayer(player) == true) + { + if (player->tumbleBounces != 0 && gamedata->totaltumbletime != UINT32_MAX) + { + gamedata->totaltumbletime++; + } + } } #undef RINGANIM_DELAYMAX diff --git a/src/m_cond.c b/src/m_cond.c index 095b44598..c69e1a460 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -649,6 +649,7 @@ void M_ClearStats(void) UINT8 i; gamedata->totalplaytime = 0; gamedata->totalrings = 0; + gamedata->totaltumbletime = 0; for (i = 0; i < GDGT_MAX; ++i) gamedata->roundsplayed[i] = 0; gamedata->timesBeaten = 0; @@ -1286,6 +1287,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) } case UC_TOTALRINGS: // Requires grabbing >= x rings return (gamedata->totalrings >= (unsigned)cn->requirement); + case UC_TOTALTUMBLETIME: // Requires total tumbling time >= x + return (gamedata->totaltumbletime >= (unsigned)cn->requirement); case UC_GAMECLEAR: // Requires game beaten >= x times return (gamedata->timesBeaten >= (unsigned)cn->requirement); case UC_OVERALLTIME: // Requires overall time <= x @@ -1918,14 +1921,12 @@ static const char *M_GetConditionString(condition_t *cn) switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x - return va("play for %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(cn->requirement)); case UC_ROUNDSPLAYED: // Requires any level completed >= x times - if (cn->extrainfo1 == GDGT_MAX) work = ""; else if (cn->extrainfo1 != GDGT_RACE && cn->extrainfo1 != GDGT_BATTLE // Base gametypes @@ -1963,6 +1964,11 @@ static const char *M_GetConditionString(condition_t *cn) return va("collect %u,%03u Rings", (cn->requirement/1000), (cn->requirement%1000)); return va("collect %u Rings", cn->requirement); + case UC_TOTALTUMBLETIME: + return va("tumble through the air for %i:%02i", + G_TicsToMinutes(cn->requirement, true), + G_TicsToSeconds(cn->requirement)); + case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) return va("beat game %d times", cn->requirement); diff --git a/src/m_cond.h b/src/m_cond.h index 7b9a9e3ec..1f747f263 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -33,6 +33,7 @@ typedef enum UC_PLAYTIME, // PLAYTIME [tics] UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played] UC_TOTALRINGS, // TOTALRINGS [x collected] + UC_TOTALTUMBLETIME, // TOTALTUMBLETIME [tics] UC_GAMECLEAR, // GAMECLEAR UC_OVERALLTIME, // OVERALLTIME [time to beat, tics] @@ -344,6 +345,7 @@ struct gamedata_t UINT32 totalplaytime; UINT32 roundsplayed[GDGT_MAX]; UINT32 totalrings; + UINT32 totaltumbletime; // Chao Key condition bypass UINT32 pendingkeyrounds; From d3b35099fcb5606d51c39a1059a9b118c9fe65b8 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 22:13:36 +0100 Subject: [PATCH 81/98] UCRP_TARGETATTACKMETHOD: Don't care about everseenspecial since this can be used with Prison Break now too --- src/m_cond.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index c69e1a460..9690a23e0 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2513,9 +2513,6 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_TARGETATTACKMETHOD: { - if (!gamedata->everseenspecial) - return NULL; - work = NULL; switch (cn->requirement) From a0df25b5cde990b8e2f60eaf57f2c99349268e83 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 19 Oct 2023 23:53:40 +0100 Subject: [PATCH 82/98] UC_TOTALTUMBLETIME: Forgot to update the gamedata --- src/g_game.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index 94f1b6a77..f036e61a7 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4426,7 +4426,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 8 // Change every format update +#define GD_VERSIONMINOR 9 // Change every format update // You can't rearrange these without a special format update typedef enum @@ -4542,6 +4542,11 @@ void G_LoadGameData(void) { gamedata->totalrings = READUINT32(save.p); + if (versionMinor >= 9) + { + gamedata->totaltumbletime = READUINT32(save.p); + } + for (i = 0; i < GDGT_MAX; i++) { gamedata->roundsplayed[i] = READUINT32(save.p); @@ -5094,7 +5099,7 @@ void G_SaveGameData(void) } length = (4+1+1+ - 4+4+ + 4+4+4+ (4*GDGT_MAX)+ 4+1+2+2+ 4+ @@ -5235,6 +5240,7 @@ void G_SaveGameData(void) WRITEUINT32(save.p, gamedata->totalplaytime); // 4 WRITEUINT32(save.p, gamedata->totalrings); // 4 + WRITEUINT32(save.p, gamedata->totaltumbletime); // 4 for (i = 0; i < GDGT_MAX; i++) // 4 * GDGT_MAX { From f68d58fb856d7c1c30394b2e878c6c2313ff8ece Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 12:36:22 +0100 Subject: [PATCH 83/98] UCRP_PODIUMCUP: Make %s Cup mixed case --- src/m_cond.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m_cond.c b/src/m_cond.c index 9690a23e0..199b650b6 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2377,7 +2377,7 @@ static const char *M_GetConditionString(condition_t *cn) { if (cup->id != cn->requirement) continue; - return va("%s%s %s CUP", + return va("%s%s %s Cup", completetype, orbetter, (M_CupLocked(cup) ? "???" : cup->realname) ); From 48bb2e1b3ce250b82616891d1500aaa9c54873d1 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 12:41:07 +0100 Subject: [PATCH 84/98] Add support for `PodiumCup Any` --- src/deh_soc.c | 3 +++ src/m_cond.c | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 49a8607c8..a38bfd4fa 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2881,6 +2881,9 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(1); ty = UCRP_PODIUMCUP; + + re = -1; + if (!fastcmp(params[1], "ANY")) { cupheader_t *cup = kartcupheaders; UINT32 hash = quickncasehash(params[1], MAXCUPNAME); diff --git a/src/m_cond.c b/src/m_cond.c index 199b650b6..8c8dfb6fc 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1499,9 +1499,13 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) if (grandprixinfo.gp == false || K_PodiumRanking() == false) return false; if (grandprixinfo.cup == NULL - || grandprixinfo.cup->id != cn->requirement) + || ( + cn->requirement != -1 // Any + && grandprixinfo.cup->id != cn->requirement + ) + ) return false; - if (cn->extrainfo2) + if (cn->extrainfo2 != 0) return (K_PodiumGrade() >= (unsigned)cn->requirement); if (cn->extrainfo1 != 0) return (player->position != 0 @@ -2373,6 +2377,13 @@ static const char *M_GetConditionString(condition_t *cn) orbetter = " or better in"; } + if (cn->requirement == -1) + { + return va("%s%s any Cup", + completetype, orbetter + ); + } + for (cup = kartcupheaders; cup; cup = cup->next) { if (cup->id != cn->requirement) From a1f93b4c60229ac1fb0d42bec5e748a3f234e0a2 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 13:03:45 +0100 Subject: [PATCH 85/98] UCRP_FINISHGRADE A "get grade A or better" Not for Podium, use PodiumCup instead for that --- src/deh_soc.c | 26 ++++++++++++++++++++++++++ src/k_tally.cpp | 3 +++ src/m_cond.c | 31 +++++++++++++++++++++++++++++++ src/m_cond.h | 2 ++ 4 files changed, 62 insertions(+) diff --git a/src/deh_soc.c b/src/deh_soc.c index a38bfd4fa..fd66fb246 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2973,6 +2973,32 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "FINISHGRADE")) + { + PARAMCHECK(1); + ty = UCRP_FINISHGRADE; + + re = -1; + if (!params[1][1]) + { + switch (params[1][0]) + { + case 'E': { re = GRADE_E; break; } + case 'D': { re = GRADE_D; break; } + case 'C': { re = GRADE_C; break; } + case 'B': { re = GRADE_B; break; } + case 'A': { re = GRADE_A; break; } + case 'S': { re = GRADE_S; break; } + default: { break; } + } + } + + if (re == -1) + { + deh_warning("Invalid grade %s for condition ID %d", params[1], id+1); + return; + } + } else if ((offset=0) || fastcmp(params[0], "FINISHTIME") || (++offset && fastcmp(params[0], "FINISHTIMEEXACT")) || (++offset && fastcmp(params[0], "FINISHTIMELEFT"))) diff --git a/src/k_tally.cpp b/src/k_tally.cpp index eba1452c8..15e11a2c7 100644 --- a/src/k_tally.cpp +++ b/src/k_tally.cpp @@ -800,6 +800,9 @@ void level_tally_t::Tick(void) transition = 0; transitionTime = TICRATE/7; delay = TICRATE/2; + + // for UCRP_FINISHGRADE + owner->roundconditions.checkthisframe = true; } else { diff --git a/src/m_cond.c b/src/m_cond.c index 8c8dfb6fc..8eb78c96c 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1623,6 +1623,14 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && player->position == cn->requirement); + case UCRP_FINISHGRADE: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay() + && (player->tally.active == true) + && (player->tally.state >= TALLY_ST_GRADE_APPEAR) + && (player->tally.state < TALLY_ST_DONE) + && (player->tally.rank >= cn->requirement)); case UCRP_FINISHTIME: return (player->exiting && !(player->pflags & PF_NOCONTEST) @@ -2446,6 +2454,29 @@ static const char *M_GetConditionString(condition_t *cn) return va("finish in %d%s%s", cn->requirement, M_GetNthType(cn->requirement), ((cn->type == UCRP_FINISHPLACE && cn->requirement > 1) ? " or better" : "")); + case UCRP_FINISHGRADE: + { + char gradeletter = '?'; + const char *orbetter = ""; + + switch (cn->requirement) + { + case GRADE_E: { gradeletter = 'E'; break; } + case GRADE_D: { gradeletter = 'D'; break; } + case GRADE_C: { gradeletter = 'C'; break; } + case GRADE_B: { gradeletter = 'B'; break; } + case GRADE_A: { gradeletter = 'A'; break; } + case GRADE_S: { gradeletter = 'S'; break; } + default: { break; } + } + + if (cn->requirement < GRADE_S) + orbetter = " or better"; + + return va("get grade %c%s", + gradeletter, orbetter + ); + } case UCRP_FINISHTIME: return va("finish in %i:%02i.%02i", G_TicsToMinutes(cn->requirement, true), diff --git a/src/m_cond.h b/src/m_cond.h index 1f747f263..832200d08 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -110,6 +110,8 @@ typedef enum UCRP_FINISHPLACE, // Finish at least [place] UCRP_FINISHPLACEEXACT, // Finish at [place] exactly + UCRP_FINISHGRADE, // Finish with at least grade [grade] + UCRP_FINISHTIME, // Finish <= [time, tics] UCRP_FINISHTIMEEXACT, // Finish == [time, tics] UCRP_FINISHTIMELEFT, // Finish with at least [time, tics] to spare From d122ddf1c7b6b2afbf244bd82d558789d3811c09 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 13:31:58 +0100 Subject: [PATCH 86/98] UC_TOTALTUMBLETIME: Show centiseconds too --- src/m_cond.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 8eb78c96c..6f488d634 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1977,9 +1977,10 @@ static const char *M_GetConditionString(condition_t *cn) return va("collect %u Rings", cn->requirement); case UC_TOTALTUMBLETIME: - return va("tumble through the air for %i:%02i", + return va("tumble through the air for %i:%02i.%02i", G_TicsToMinutes(cn->requirement, true), - G_TicsToSeconds(cn->requirement)); + G_TicsToSeconds(cn->requirement), + G_TicsToCentiseconds(cn->requirement)); case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) From dc72602b0005b0bddbd08ca662f135068aef7f00 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 14:27:19 +0100 Subject: [PATCH 87/98] UnlockPercent: Add fudge value If you're hypothetically gating a Character behind 100% of Characters unlocked... that'll never come true! To that end, allow a fudge value to be specified. It's added to the unlock count before comparing it against the percentage. --- src/deh_soc.c | 9 +++++++++ src/m_cond.c | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index fd66fb246..21057128d 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2765,7 +2765,16 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) deh_warning("Condition challenge type \"%s\" invalid for condition ID %d", params[2], id+1); return; } + + x2 = 0; + if (params[3]) + { + // fudge value + x2 = atoi(params[3]); + } } + else + x2 = 1; // guaranteed fudge for raw Unlockables count } else if ((offset=0) || fastcmp(params[0], "ADDON") || (++offset && fastcmp(params[0], "CREDITS")) diff --git a/src/m_cond.c b/src/m_cond.c index 6f488d634..96980e5b9 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1351,7 +1351,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UC_UNLOCKPERCENT: { - UINT16 i, unlocked = 0, total = 0; + UINT16 i, unlocked = cn->extrainfo2, total = 0; // Special case for maps if (cn->extrainfo1 == SECRET_MAP) @@ -1396,8 +1396,6 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) unlocked++; } - - unlocked++; // Try to account for this one too } else { From 986fbd06b9fecbb786632b24a61354fc8b60f0a8 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 14:47:28 +0100 Subject: [PATCH 88/98] Self-review: G_SetPlayerGamepadIndicator - use skincolor's ramp directly instead of cacheing and then indexing an entire colormap --- src/g_input.c | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/g_input.c b/src/g_input.c index e5f7a7c0a..86ceb1d61 100644 --- a/src/g_input.c +++ b/src/g_input.c @@ -208,7 +208,6 @@ void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) { INT32 device; UINT16 skincolor; - UINT8 *colormap; byteColor_t byte_color; I_Assert(player >= 0 && player < MAXSPLITSCREENPLAYERS); @@ -229,16 +228,7 @@ void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) skincolor = skins[skin].prefcolor; } - // We use TC_DEFAULT here rather than player skin because... - colormap = R_GetTranslationColormap(TC_DEFAULT, skincolor, GTC_MENUCACHE); - - if (colormap == NULL) - { - return; - } - - // ...we're grabbing the same index as a reference point across remaps! - byte_color = V_GetColor(colormap[104]).s; + byte_color = V_GetColor(skincolors[skincolor].ramp[8]).s; I_SetGamepadIndicatorColor(device, byte_color.red, byte_color.green, byte_color.blue); } From 340ac0de0f4d2c78330911dc3b4e0cca37b9c970 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 14:59:49 +0100 Subject: [PATCH 89/98] Self-review: K_DoFault - add challenge check deferral --- src/k_respawn.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/k_respawn.c b/src/k_respawn.c index c3804ffae..21af8253d 100644 --- a/src/k_respawn.c +++ b/src/k_respawn.c @@ -112,7 +112,11 @@ void K_DoFault(player_t *player) player->mo->renderflags |= RF_DONTDRAW; player->mo->flags |= MF_NOCLIPTHING; - player->roundconditions.faulted = true; + if (player->roundconditions.faulted == false) + { + player->roundconditions.faulted = true; + player->roundconditions.checkthisframe = true; + } } } From 94cfda6832eb6bb7f75f7faff259a44d3ac364e2 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 15:19:21 +0100 Subject: [PATCH 90/98] Self-review: TrackHazard - Fix the highest bit handling - Cleaner code for specific lap handling --- src/m_cond.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 96980e5b9..9da7e9dce 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1737,7 +1737,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) // Check the highest relevant byte for all necessary bits. // We only do this if an == 0xFF/0xFE check wouldn't satisfy. - if (requiredbit != 7) + if (requiredbit != (1<<7)) { // Last bit MAYBE not needed, POSITION doesn't count. const UINT8 finalbit = (requiredlap == 0) ? 1 : 0; @@ -1765,7 +1765,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (player->roundconditions.hittrackhazard[0] == 0xFE); } - return (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit) != (cn->requirement == 1)); + return (((player->roundconditions.hittrackhazard[requiredlap] & requiredbit) == requiredbit) == (cn->requirement == 1)); } case UCRP_TARGETATTACKMETHOD: From d1ad45236f177e9cda8a1395d177d4933ff5bb16 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 15:34:44 +0100 Subject: [PATCH 91/98] Self-review: Guarantee the first character of the Challenge Hint is also uppercase, thanks to a possible prefix being "The Controls Tutorial:" --- src/m_cond.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 9da7e9dce..f4822e1f0 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2695,12 +2695,9 @@ char *M_BuildConditionSetString(UINT16 unlockid) break; } - // If we didn't find a prefix, just start from the first character again. - if (!message[i]) - i = 0; - - // Okay, now make the first non-whitespace character after the prefix a capital. + // Okay, now make the first non-whitespace character after this a capital. // Doesn't matter if !isalpha() - toupper is a no-op. + // (If the first loop hit the string's end, the message[i] check keeps us safe) for (; message[i]; i++) { if ((message[i] & 0x80) || isspace(message[i])) @@ -2708,6 +2705,16 @@ char *M_BuildConditionSetString(UINT16 unlockid) message[i] = toupper(message[i]); break; } + + // Also do this for the prefix. + // This might seem redundant, but "the Controls Tutorial:" is a possible prefix! + for (i = 0; message[i]; i++) + { + if ((message[i] & 0x80) || isspace(message[i])) + continue; + message[i] = toupper(message[i]); + break; + } } // Finally, do a clean wordwrap! From 82afb5da5ab6912e9ef3b71b843af3f66964def5 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 16:14:58 +0100 Subject: [PATCH 92/98] Self-review: Grade S is not an available rank for the Tally --- src/deh_soc.c | 1 - src/m_cond.c | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 21057128d..5fc282e85 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2997,7 +2997,6 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) case 'C': { re = GRADE_C; break; } case 'B': { re = GRADE_B; break; } case 'A': { re = GRADE_A; break; } - case 'S': { re = GRADE_S; break; } default: { break; } } } diff --git a/src/m_cond.c b/src/m_cond.c index f4822e1f0..e3c08e7d5 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2465,11 +2465,10 @@ static const char *M_GetConditionString(condition_t *cn) case GRADE_C: { gradeletter = 'C'; break; } case GRADE_B: { gradeletter = 'B'; break; } case GRADE_A: { gradeletter = 'A'; break; } - case GRADE_S: { gradeletter = 'S'; break; } default: { break; } } - if (cn->requirement < GRADE_S) + if (cn->requirement < GRADE_A) orbetter = " or better"; return va("get grade %c%s", From 22bef38562c93fb52a898fa61fbe9f9f0705062f Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 16:18:10 +0100 Subject: [PATCH 93/98] Self-review: Fix missing space in "reach at least400% on the speedometer" --- src/m_cond.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m_cond.c b/src/m_cond.c index e3c08e7d5..2b588ce02 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -2500,7 +2500,7 @@ static const char *M_GetConditionString(condition_t *cn) case UCRP_SPEEDOMETER: return va("reach %s%u%% on the speedometer", (cn->requirement == 999) - ? "" : "at least", + ? "" : "at least ", cn->requirement ); case UCRP_DRAFTDURATION: From 7604235c002cd42f3143fb37009e42eef5c9b79c Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 16:22:45 +0100 Subject: [PATCH 94/98] Self-review: Fix inverted before-exiting check for FellOff --- src/p_inter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/p_inter.c b/src/p_inter.c index 52695a598..bf70ce28e 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -2430,7 +2430,7 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, case DMG_DEATHPIT: // Fell off the stage if (player->roundconditions.fell_off == false - && beforeexit == false) + && beforeexit == true) { player->roundconditions.fell_off = true; player->roundconditions.checkthisframe = true; From 909eb7b6ef0a21836c88fbd1b1a5980774323418 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 16:56:15 +0100 Subject: [PATCH 95/98] The Tournament Mode cheat is now "chaos zero 64". Thanks to the OG! In addition, make it case insensitive --- src/m_cheat.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m_cheat.c b/src/m_cheat.c index e44735eb9..e91e445fd 100644 --- a/src/m_cheat.c +++ b/src/m_cheat.c @@ -167,7 +167,7 @@ static UINT8 cheatf_devmode(void) static cheatseq_t cheat_warp = { NULL, cheatf_warp, - (UINT8[]){ SCRAMBLE('p'), SCRAMBLE('l'), SCRAMBLE('a'), SCRAMBLE('c'), SCRAMBLE('e'), SCRAMBLE('h'), SCRAMBLE('o'), SCRAMBLE('l'), SCRAMBLE('d'), SCRAMBLE('e'), SCRAMBLE('r'), 0xff } + (UINT8[]){ SCRAMBLE('c'), SCRAMBLE('h'), SCRAMBLE('a'), SCRAMBLE('o'), SCRAMBLE('s'), SCRAMBLE(' '), SCRAMBLE('z'), SCRAMBLE('e'), SCRAMBLE('r'), SCRAMBLE('o'), SCRAMBLE(' '), SCRAMBLE('6'), SCRAMBLE('4'), 0xff } }; static cheatseq_t cheat_wrongwarp = { @@ -263,7 +263,7 @@ boolean cht_Interpret(const char *password) cheatseqid = 0; while (cheatseqlist[cheatseqid]) { - ret += cht_CheckCheat(cheatseqlist[cheatseqid], *password, (password == endofpassword)); + ret += cht_CheckCheat(cheatseqlist[cheatseqid], tolower(*password), (password == endofpassword)); cheatseqid++; } From 5aa71fb43e34f38e1f64fb10961a658f0fb6bf96 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 18:24:07 +0100 Subject: [PATCH 96/98] PrecacheLevelLocks: Automate naming SECRET_ALTMUSIC Untested, might need to patch iF Oni runs into bugs --- src/m_cond.c | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/m_cond.c b/src/m_cond.c index 2b588ce02..6055586c4 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1047,6 +1047,68 @@ static void M_PrecacheLevelLocks(void) } mapheaderinfo[map]->cache_muslock[j - 1] = i; + + const char *tempstr = NULL; + UINT8 positionid = 0; + + if (mapheaderinfo[map]->cup) + { + for (positionid = 0; positionid < CUPCACHE_PODIUM; positionid++) + { + if (mapheaderinfo[map]->cup->cachedlevels[positionid] != map) + continue; + break; + } + + if (positionid < CUPCACHE_PODIUM) + { + char prefix = 'R'; + if (positionid >= CUPCACHE_BONUS) + { + positionid -= (CUPCACHE_BONUS); + prefix = 'B'; + } + + tempstr = va( + "Music: %s Cup %c%u #%u", + mapheaderinfo[map]->cup->realname, + prefix, + positionid + 1, + j + ); + } + } + + if (tempstr == NULL) + { + UINT16 mapcheck; + for (mapcheck = 0; mapcheck < map; mapcheck++) + { + if (!mapheaderinfo[mapcheck] || mapheaderinfo[mapcheck]->cup != NULL) + continue; + if (mapheaderinfo[mapcheck]->menuflags & (LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + if (((mapheaderinfo[mapcheck]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL) + != ((mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL)) + continue; + + // We don't check for locked, because the levels exist + positionid++; + } + + tempstr = va( + "Music: %s #%u #%u", + (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) ? "Tutorial" : "Lost and Found", + positionid + 1, + j + ); + } + + if (tempstr != NULL) + { + strlcpy(unlockables[i].name, tempstr, sizeof (unlockables[i].name)); + } + break; } if (j == mapheaderinfo[map]->musname_size) From 91f411d52bff5adc2cc3d6f6ae6358ba43d12f8b Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 20 Oct 2023 20:39:16 +0100 Subject: [PATCH 97/98] VC requests for auto-naming AltMusic Challenge - Replace the track #1/#2 with side B/C - Add invalid map-set text --- src/m_cond.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 6055586c4..39383aafc 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1036,6 +1036,8 @@ static void M_PrecacheLevelLocks(void) case SECRET_ALTMUSIC: { UINT16 map = M_UnlockableMapNum(&unlockables[i]); + const char *tempstr = NULL; + if (map < nummapheaders && mapheaderinfo[map]) { @@ -1048,7 +1050,6 @@ static void M_PrecacheLevelLocks(void) mapheaderinfo[map]->cache_muslock[j - 1] = i; - const char *tempstr = NULL; UINT8 positionid = 0; if (mapheaderinfo[map]->cup) @@ -1070,11 +1071,11 @@ static void M_PrecacheLevelLocks(void) } tempstr = va( - "Music: %s Cup %c%u #%u", + "Music: %s Cup %c%u %c", mapheaderinfo[map]->cup->realname, prefix, positionid + 1, - j + 'A' + j // :D ? ); } } @@ -1097,23 +1098,24 @@ static void M_PrecacheLevelLocks(void) } tempstr = va( - "Music: %s #%u #%u", + "Music: %s #%u %c", (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) ? "Tutorial" : "Lost and Found", positionid + 1, - j + 'A' + j // :D ? ); } - if (tempstr != NULL) - { - strlcpy(unlockables[i].name, tempstr, sizeof (unlockables[i].name)); - } - break; } if (j == mapheaderinfo[map]->musname_size) CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_ALTMUSICs associated with Level %s\n", i, mapheaderinfo[map]->lumpname); } + + if (tempstr == NULL) + tempstr = va("INVALID MUSIC UNLOCK %u", i); + + strlcpy(unlockables[i].name, tempstr, sizeof (unlockables[i].name)); + break; } From e98a24363da8b5ed07db248a17d0602e074621e8 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 21 Oct 2023 11:27:31 +0100 Subject: [PATCH 98/98] IsDifficulty: Fix setting Hard/Master --- src/deh_soc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 5fc282e85..394e2d12d 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2876,9 +2876,9 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) if (fastcmp(params[1], "NORMAL")) ; else if (fastcmp(params[1], "HARD")) - x1 = KARTSPEED_HARD; + re = KARTSPEED_HARD; else if (fastcmp(params[1], "MASTER")) - x1 = KARTGP_MASTER; + re = KARTGP_MASTER; else { deh_warning("gamespeed requirement \"%s\" invalid for condition ID %d", params[1], id+1);