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
This commit is contained in:
toaster 2025-08-27 20:43:46 +01:00
parent 5de0539316
commit c3131f697e
5 changed files with 184 additions and 62 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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
};

View file

@ -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);

View file

@ -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;
}