From c3131f697ee98b1241efb2088c586a2dd41103e6 Mon Sep 17 00:00:00 2001 From: toaster Date: Wed, 27 Aug 2025 20:43:46 +0100 Subject: [PATCH] Tutorial Recommendation hints for GP cup select One new parameter on cupheader_t that can only be set in mainwads If specified hint condition has been achieved, identifies from the actual unlock condition what Tutorial it should surface in a popup. Funny question mark --- src/deh_soc.c | 12 ++++ src/dehacked.c | 1 + src/doomstat.h | 4 +- src/k_menudraw.c | 117 +++++++++++++++---------------- src/menus/transient/cup-select.c | 112 ++++++++++++++++++++++++++++- 5 files changed, 184 insertions(+), 62 deletions(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 6309e7cf8..4a30756b5 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -4114,6 +4114,18 @@ void readcupheader(MYFILE *f, cupheader_t *cup) deh_warning("You must define a custom gamedata to use \"%s\"", word); } } + else if (fastcmp(word, "HINTCONDITION")) + { + if (!mainwads || (refreshdirmenu & REFRESHDIR_GAMEDATA)) + { + if (i > 0) + cup->hintcondition = i-1; + } + else + { + deh_warning("You must define a custom gamedata to use \"%s\"", word); + } + } else deh_warning("%s Cup: unknown word '%s'", cup->name, word); } diff --git a/src/dehacked.c b/src/dehacked.c index 92d59fe85..45081d58a 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -495,6 +495,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) cup->monitor = 1; cup->id = numkartcupheaders; cup->cache_cuplock = MAXUNLOCKABLES; + cup->hintcondition = MAXCONDITIONSETS; for (i = 0; i < CUPCACHE_MAX; i++) cup->cachedlevels[i] = NEXTMAP_INVALID; diff --git a/src/doomstat.h b/src/doomstat.h index 0d567c053..63c976438 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -449,10 +449,12 @@ struct cupheader_t UINT8 numbonus; ///< Number of bonus stages defined UINT8 emeraldnum; ///< ID of Emerald to use for special stage (1-7 for Chaos Emeralds, 8-14 for Super Emeralds, 0 for no emerald) + // Modifiable in mainwads only boolean playcredits; ///< Play the credits? + UINT16 hintcondition; ///< Hint condition for 2.4 Super Cup + // Truly internal data UINT16 cache_cuplock; ///< Cached Unlockable ID - cupwindata_t windata[4]; ///< Data for cup visitation cupheader_t *next; ///< Next cup in linked list }; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index bf85955a9..9179e5814 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -3217,111 +3217,108 @@ void M_DrawCupSelect(void) INT16 cy = M_EaseWithTransition(Easing_Linear, 5 * 30); cupwindata_t *windata = NULL; levelsearch_t templevelsearch = levellist.levelsearch; // full copy + boolean isLocked; + const boolean isGP = (templevelsearch.grandprix && (cv_dummygpdifficulty.value >= 0 && cv_dummygpdifficulty.value < KARTGP_MAX)); + const UINT8 numrows = (cupgrid.cache_secondrowlocked ? 1 : CUPMENU_ROWS); for (i = 0; i < CUPMENU_COLUMNS; i++) { x = 14 + (i*42); + y = 20 - cy; + if (cupgrid.cache_secondrowlocked == true) + y += 28; - for (j = 0; j < (cupgrid.cache_secondrowlocked ? 1 : CUPMENU_ROWS); j++) + for (j = 0; j < numrows; j++) { - size_t id = (i + (j * CUPMENU_COLUMNS)) + (cupgrid.pageno * (CUPMENU_COLUMNS * CUPMENU_ROWS)); + const size_t id = (i + (j * CUPMENU_COLUMNS)) + (cupgrid.pageno * (CUPMENU_COLUMNS * CUPMENU_ROWS)); if (!cupgrid.builtgrid[id]) break; templevelsearch.cup = cupgrid.builtgrid[id]; - y = 20 + (j*44) - cy; - if (cupgrid.cache_secondrowlocked == true) - y += 28; - - const boolean isGP = (templevelsearch.grandprix && (cv_dummygpdifficulty.value >= 0 && cv_dummygpdifficulty.value < KARTGP_MAX)); if (isGP) - { windata = &templevelsearch.cup->windata[cv_dummygpdifficulty.value]; - } + + isLocked = (M_GetFirstLevelInList(&temp, &templevelsearch) == NEXTMAP_INVALID); M_DrawCup( templevelsearch.cup, x * FRACUNIT, y * FRACUNIT, - (M_GetFirstLevelInList(&temp, &templevelsearch) == NEXTMAP_INVALID) ? ((cupgrid.previewanim % 4) + 1) : 0, + isLocked ? ((cupgrid.previewanim % 4) + 1) : 0, isGP, windata ? windata->best_placement : 0 ); - if (templevelsearch.grandprix == true - && templevelsearch.cup == cupsavedata.cup + if (!isGP || id == CUPMENU_CURSORID) + ; + else if (isLocked) + { + if (templevelsearch.cup->hintcondition != MAXCONDITIONSETS + && M_Achieved(templevelsearch.cup->hintcondition)) + { + // Super Cup tutorial hint. + V_DrawScaledPatch(x + (32-10), y + (32-9), 0, W_CachePatchName("UN_HNT2A", PU_CACHE)); + } + } + else if (templevelsearch.cup == cupsavedata.cup && id != CUPMENU_CURSORID) { + // GP backup notif. V_DrawScaledPatch(x + 32, y + 32, 0, W_CachePatchName("CUPBKUP1", PU_CACHE)); } // used to be 8 + (j*100) - (30*menutransition.tics) // but one-row mode means y has to be changed // this is the difference between y and that - if (j == 0) - { - y -= 12; // (8) - (20) - } - else - { - y += 44; //(8 + 100) - (20 + 44) - } if (windata) { - if (cv_reducevfx.value) - { - M_DrawCupWinData( - x, - y, - templevelsearch.cup, - cv_dummygpdifficulty.value, - false, - false - ); - } - else - { - M_DrawCupWinData( - x, - y, - templevelsearch.cup, - cv_dummygpdifficulty.value, - (cupgrid.previewanim & 1), - false - ); - } + M_DrawCupWinData( + x, + y + (j ? 44 : -12), + templevelsearch.cup, + cv_dummygpdifficulty.value, + (!cv_reducevfx.value && (cupgrid.previewanim & 1)), + false + ); } + + y += 44; } } - { - fixed_t tx = Easing_Linear(M_DueFrac(cupgrid.xslide.start, CUPMENU_SLIDETIME), cupgrid.xslide.dist * FRACUNIT, 0); - fixed_t ty = Easing_Linear(M_DueFrac(cupgrid.yslide.start, CUPMENU_SLIDETIME), cupgrid.yslide.dist * FRACUNIT, 0); - - x = 14 + (cupgrid.x*42*FRACUNIT - tx) / FRACUNIT; - y = 20 + (cupgrid.y*44*FRACUNIT - ty) / FRACUNIT - cy; - } - + x = 14 + (cupgrid.x*42); + y = 20 + (cupgrid.y*44) - cy; if (cupgrid.cache_secondrowlocked == true) y += 28; - V_DrawScaledPatch(x - 4, y - 1, 0, W_CachePatchName("CUPCURS", PU_CACHE)); + // Interpolated cursor + { + fixed_t tx = Easing_Linear(M_DueFrac(cupgrid.xslide.start, CUPMENU_SLIDETIME), cupgrid.xslide.dist * FRACUNIT, 0)/FRACUNIT; + fixed_t ty = Easing_Linear(M_DueFrac(cupgrid.yslide.start, CUPMENU_SLIDETIME), cupgrid.yslide.dist * FRACUNIT, 0)/FRACUNIT; + + V_DrawScaledPatch((x - 4) - tx, (y - 1) - ty, 0, W_CachePatchName("CUPCURS", PU_CACHE)); + } templevelsearch.cup = cupgrid.builtgrid[CUPMENU_CURSORID]; - if (templevelsearch.grandprix == true - && templevelsearch.cup != NULL + if (!isGP) + ; + else if (M_GetFirstLevelInList(&temp, &templevelsearch) == NEXTMAP_INVALID) + { + if (templevelsearch.cup->hintcondition != MAXCONDITIONSETS + && M_Achieved(templevelsearch.cup->hintcondition)) + { + // Super Cup tutorial hint. + V_DrawScaledPatch(x + (32-10), y + (32-9), 0, W_CachePatchName("UN_HNT1A", PU_CACHE)); + } + } + else if (templevelsearch.cup != NULL && templevelsearch.cup == cupsavedata.cup) { - V_DrawScaledPatch( - 14 + (cupgrid.x*42) + 32, - 20 + (cupgrid.y*44) + 32 - + ((cupgrid.cache_secondrowlocked == true) ? 28 : 0), - 0, - W_CachePatchName("CUPBKUP2", PU_CACHE) - ); + // GP backup hint. + V_DrawScaledPatch(x + 32, y + 32, 0, W_CachePatchName("CUPBKUP2", PU_CACHE)); } INT16 ty = M_EaseWithTransition(Easing_Linear, 5 * 24); diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index 6d44b6854..70bfdff81 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -21,6 +21,7 @@ #include "../../k_podium.h" // K_StartCeremony #include "../../m_misc.h" // FIL_FileExists #include "../../d_main.h" // D_ClearState +#include "../../m_cond.h" // Condition Sets menuitem_t PLAY_CupSelect[] = { @@ -161,6 +162,114 @@ static void M_StartCup(UINT8 entry) } } +static UINT16 cupselecttutorial_hack = NEXTMAP_INVALID; + +static void M_GPTutorialResponse(INT32 choice) +{ + if (choice != MA_YES) + return; + + multiplayer = true; + + restoreMenu = &PLAY_CupSelectDef; + restorelevellist = levellist; + + // mild hack + levellist.newgametype = GT_TUTORIAL; + levellist.netgame = false; + M_MenuToLevelPreamble(0, false); + + D_MapChange( + cupselecttutorial_hack+1, + levellist.newgametype, + false, + true, + 1, + false, + false + ); + + M_ClearMenus(true); +} + +static boolean M_GPTutorialRecommendation(cupheader_t *cup) +{ + // Only applies to GP. + if (levellist.levelsearch.grandprix == false) + return false; + + // Does this not have a Tutorial Recommendation? + if (cup->cache_cuplock >= MAXUNLOCKABLES + || cup->hintcondition == MAXCONDITIONSETS + || !M_Achieved(cup->hintcondition)) + return false; + + // Does the thing have no condition? + const UINT16 condition = unlockables[cup->cache_cuplock].conditionset; + if (condition == 0) + return false; + + const conditionset_t *c = &conditionSets[condition-1]; + UINT32 i; + INT32 mapnum = NEXTMAP_INVALID; + + // Identify the map to visit/beat. + for (i = 0; i < c->numconditions; ++i) + { + if (c->condition[i].type < UC_MAPVISITED || c->condition[i].type > UC_MAPBEATEN) + continue; + mapnum = c->condition[i].requirement; + if (mapnum < 0 || mapnum >= nummapheaders) + continue; + if (!mapheaderinfo[mapnum]) + continue; + if (G_GuessGametypeByTOL(mapheaderinfo[mapnum]->typeoflevel) != GT_TUTORIAL) + continue; + break; + } + + // Didn't find one? + if (i == c->numconditions) + return false; + + // Not unlocked? + if (M_MapLocked(mapnum+1)) + { + M_StartMessage( + "Recommended Learning", + "This Cup will test skills that\n" + "a ""\x86""currently locked ""\x87""Tutorial\x80 teaches.\n" + "\n" + "Come back when you've made progress elsewhere.", + NULL, MM_NOTHING, NULL, NULL + ); + return true; + } + + // This is kind of a hack. + cupselecttutorial_hack = mapnum; + + M_StartMessage( + "Recommended Learning", + va( + "This Cup will test skills that\n" + "the ""\x87""%s Tutorial\x80 teaches.\n" + "\n" + "%s\n", + mapheaderinfo[mapnum]->menuttl, + (setup_numplayers > 1 + ? "You're encouraged to play it later." + : "Would you like to play it now?" + )), + setup_numplayers > 1 ? NULL : M_GPTutorialResponse, + setup_numplayers > 1 ? MM_NOTHING : MM_YESNO, + setup_numplayers > 1 ? NULL : "Yes, let's go", + setup_numplayers > 1 ? "Got it!" : "Not right now" + ); + + return true; +} + static void M_GPBackup(INT32 choice) { if (choice == MA_YES) @@ -271,7 +380,8 @@ void M_CupSelectHandler(INT32 choice) ) ) { - S_StartSound(NULL, sfx_s3kb2); + if (!M_GPTutorialRecommendation(newcup)) + S_StartSound(NULL, sfx_s3kb2); return; }