Tutorial skipping challenge - first pass

- Go to a MainCfg-specified map on its guessed gametype
    - `TutorialChallengeMap = RR_ZonedCity`
- Some unique settings
    - K_CanChangeRules() == false
    - If GTR_CIRCUIT, make it Hard Speed
    - If GTR_BUMPERS, die in one hit
    - If GTR_BOTS, make them all difficulty 13 of the default bot skin
    - No Medals or Spray Cans during the Tutorial Challenge
- Complete the stage in 1st place or don't bother at all
- Has the "Give up" menu option available, but no "Try Again"
- Condition for successfully completing the Tutorial Skip
    - `Condition1 = TutorialSkip`

Related bugfixes:
- Correctly wipe skipstats when returning to the Title/menus
- Typing of `ultimatemode` (hey this isn't accessible by anything right now I wonder)
This commit is contained in:
toaster 2023-11-16 21:44:52 +00:00
parent 9f05c199c2
commit 0762b93ef2
19 changed files with 161 additions and 29 deletions

View file

@ -1443,6 +1443,7 @@ static void CL_LoadReceivedSavegame(boolean reloading)
demo.playback = false;
demo.title = false;
titlemapinaction = false;
tutorialchallenge = TUTORIALSKIP_NONE;
automapactive = false;
// load a base level

View file

@ -1053,8 +1053,11 @@ void D_ClearState(void)
memset(&grandprixinfo, 0, sizeof(struct grandprixinfo));
memset(&roundqueue, 0, sizeof(struct roundqueue));
// empty maptol so mario/etc sounds don't play in sound test when they shouldn't
// empty some other semi-important state
maptol = 0;
nextmapoverride = 0;
skipstats = 0;
tutorialchallenge = TUTORIALSKIP_NONE;
gameaction = ga_nothing;
memset(displayplayers, 0, sizeof(displayplayers));

View file

@ -2791,7 +2791,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2)
else if ((offset=0) || fastcmp(params[0], "ADDON")
|| (++offset && fastcmp(params[0], "CREDITS"))
|| (++offset && fastcmp(params[0], "REPLAY"))
|| (++offset && fastcmp(params[0], "CRASH")))
|| (++offset && fastcmp(params[0], "CRASH"))
|| (++offset && fastcmp(params[0], "TUTORIALSKIP")))
{
//PARAMCHECK(1);
ty = UC_ADDON + offset;
@ -3469,6 +3470,11 @@ void readmaincfg(MYFILE *f, boolean mainfile)
titlemap = Z_StrDup(word2);
titlechanged = true;
}
else if (fastcmp(word, "TUTORIALCHALLENGEMAP"))
{
Z_Free(tutorialchallengemap);
tutorialchallengemap = Z_StrDup(word2);
}
else if (fastcmp(word, "HIDETITLEPICS") || fastcmp(word, "TITLEPICSHIDE"))
{
hidetitlepics = (boolean)(value != 0 || word2[0] == 'T' || word2[0] == 'Y');

View file

@ -246,11 +246,16 @@ extern INT32 g_localplayers[MAXSPLITSCREENPLAYERS];
extern char * titlemap;
extern boolean hidetitlepics;
extern char * bootmap; //bootmap for loading a map on startup
extern boolean looptitle;
extern char * bootmap; //bootmap for loading a map on startup
extern char * podiummap; // map to load for podium
extern boolean looptitle;
extern char * tutorialchallengemap; // map to load for tutorial skip
extern UINT8 tutorialchallenge;
#define TUTORIALSKIP_NONE 0
#define TUTORIALSKIP_FAILED 1
#define TUTORIALSKIP_INPROGRESS 2
// CTF colors.
extern UINT16 skincolor_redteam, skincolor_blueteam, skincolor_redring, skincolor_bluering;

View file

@ -83,7 +83,7 @@
gameaction_t gameaction;
gamestate_t gamestate = GS_NULL;
UINT8 ultimatemode = false;
boolean ultimatemode = false;
JoyType_t Joystick[MAXSPLITSCREENPLAYERS];
@ -170,11 +170,13 @@ tic_t timeinmap; // Ticker for time spent in level (used for levelcard display)
char * titlemap = NULL;
boolean hidetitlepics = false;
char * bootmap = NULL; //bootmap for loading a map on startup
boolean looptitle = true;
char * bootmap = NULL; //bootmap for loading a map on startup
char * podiummap = NULL; // map to load for podium
boolean looptitle = true;
char * tutorialchallengemap = NULL; // map to load for tutorial skip
UINT8 tutorialchallenge = TUTORIALSKIP_NONE;
UINT16 skincolor_redteam = SKINCOLOR_RED;
UINT16 skincolor_blueteam = SKINCOLOR_BLUE;
@ -717,14 +719,18 @@ INT32 G_MapNumber(const char * name)
name += 8;
if (strcasecmp("TITLE", name) == 0)
return NEXTMAP_TITLE;
if (strcasecmp("EVALUATION", name) == 0)
return NEXTMAP_EVALUATION;
if (strcasecmp("CREDITS", name) == 0)
return NEXTMAP_CREDITS;
if (strcasecmp("CEREMONY", name) == 0)
return NEXTMAP_CEREMONY;
if (strcasecmp("TITLE", name) == 0)
return NEXTMAP_TITLE;
if (strcasecmp("VOTING", name) == 0)
return NEXTMAP_VOTING;
if (strcasecmp("TUTORIALCHALLENGE", name) == 0)
return NEXTMAP_TUTORIALCHALLENGE;
return NEXTMAP_INVALID;
}
@ -4170,8 +4176,6 @@ static void G_DoCompleted(void)
G_SetGamestate(GS_NULL);
wipegamestate = GS_NULL;
prevmap = (INT16)(gamemap-1);
}
// Finally, if you're not exiting, guarantee NO CONTEST.
@ -4199,6 +4203,39 @@ static void G_DoCompleted(void)
}
// And lastly, everything in anticipation for Intermission/level change.
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
if (
players[consoleplayer].position != 1
|| !players[consoleplayer].exiting
|| (players[consoleplayer].pflags & PF_NOCONTEST)
)
{
// Return to whence you came with your tail between your legs
tutorialchallenge = TUTORIALSKIP_FAILED;
G_SetGametype(GT_TUTORIAL);
nextmapoverride = prevmap+1;
}
else
{
// Proceed.
nextmapoverride = NEXTMAP_TITLE+1;
gamedata->finishedtutorialchallenge = true;
M_UpdateUnlockablesAndExtraEmblems(true, true);
G_SaveGameData();
}
}
else
{
// The "else" might not be strictly needed, but I don't
// want the "challenge" map to be considered visited before it's your time.
// ~toast 161123 (5 years of srb2kart, woooouuuu)
prevmap = gamemap-1;
tutorialchallenge = TUTORIALSKIP_NONE;
}
if (!demo.playback)
{
// Set up power level gametype scrambles
@ -4269,6 +4306,23 @@ void G_AfterIntermission(void)
//
void G_NextLevel(void)
{
if (
gametype == GT_TUTORIAL
&& nextmap == NEXTMAP_TUTORIALCHALLENGE
)
{
nextmap = G_MapNumber(tutorialchallengemap);
if (
nextmap < nummapheaders
&& mapheaderinfo[nextmap] != NULL
&& mapheaderinfo[nextmap]->typeoflevel != 0
)
{
tutorialchallenge = TUTORIALSKIP_INPROGRESS;
G_SetGametype(G_GuessGametypeByTOL(mapheaderinfo[nextmap]->typeoflevel));
}
}
if (nextmap >= NEXTMAP_SPECIAL)
{
G_EndGame();
@ -4471,6 +4525,7 @@ typedef enum
GDEVER_SPECIAL = 1<<3,
GDEVER_KEYTUTORIAL = 1<<4,
GDEVER_KEYMAJORSKIP = 1<<5,
GDEVER_TUTORIALSKIP = 1<<6,
} gdeverdone_t;
static const char *G_GameDataFolder(void)
@ -4610,6 +4665,7 @@ void G_LoadGameData(void)
gamedata->everseenspecial = !!(everflags & GDEVER_SPECIAL);
gamedata->chaokeytutorial = !!(everflags & GDEVER_KEYTUTORIAL);
gamedata->majorkeyskipattempted = !!(everflags & GDEVER_KEYMAJORSKIP);
gamedata->finishedtutorialchallenge = !!(everflags & GDEVER_TUTORIALSKIP);
}
else
{
@ -5301,6 +5357,8 @@ void G_SaveGameData(void)
everflags |= GDEVER_KEYTUTORIAL;
if (gamedata->majorkeyskipattempted)
everflags |= GDEVER_KEYMAJORSKIP;
if (gamedata->finishedtutorialchallenge)
everflags |= GDEVER_TUTORIALSKIP;
WRITEUINT32(save.p, everflags); // 4
}

View file

@ -49,7 +49,8 @@ typedef enum
NEXTMAP_CREDITS = INT16_MAX-3,
NEXTMAP_CEREMONY = INT16_MAX-4,
NEXTMAP_VOTING = INT16_MAX-5,
NEXTMAP_INVALID = INT16_MAX-6, // Always last
NEXTMAP_TUTORIALCHALLENGE = INT16_MAX-6,
NEXTMAP_INVALID = INT16_MAX-7, // Always last
NEXTMAP_SPECIAL = NEXTMAP_INVALID
} nextmapspecial_t;

View file

@ -58,7 +58,7 @@ typedef enum
extern gamestate_t gamestate;
extern boolean titlemapinaction;
extern UINT8 ultimatemode; // was sk_insane
extern boolean ultimatemode; // was sk_insane
extern gameaction_t gameaction;
void G_SetGamestate(gamestate_t newstate);

View file

@ -39,6 +39,9 @@ UINT8 numtargets = 0; // Capsules busted
INT32 K_StartingBumperCount(void)
{
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
return 0;
if (battleprisons || K_CheckBossIntro())
{
if (grandprixinfo.gp)

View file

@ -128,8 +128,8 @@ boolean K_AddBot(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p)
void K_UpdateMatchRaceBots(void)
{
const UINT8 defaultbotskin = R_BotDefaultSkin();
const UINT8 difficulty = cv_kartbot.value;
UINT8 pmax = std::min<UINT8>((dedicated ? MAXPLAYERS-1 : MAXPLAYERS), static_cast<UINT8>(cv_maxconnections.value));
UINT8 difficulty;
UINT8 pmax = (dedicated ? MAXPLAYERS-1 : MAXPLAYERS);
UINT8 numplayers = 0;
UINT8 numbots = 0;
UINT8 numwaiting = 0;
@ -145,9 +145,27 @@ void K_UpdateMatchRaceBots(void)
}
grabskins[usableskins] = MAXSKINS;
if (cv_maxplayers.value > 0)
if ((gametyperules & GTR_BOTS) == 0)
{
pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxplayers.value));
difficulty = 0;
}
else if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
pmax = 8; // can you believe this is a nerf
difficulty = MAXBOTDIFFICULTY;
}
else if (K_CanChangeRules(true) == false)
{
difficulty = 0;
}
else
{
difficulty = cv_kartbot.value;
pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxconnections.value));
if (cv_maxplayers.value > 0)
{
pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxplayers.value));
}
}
for (i = 0; i < MAXPLAYERS; i++)
@ -180,9 +198,7 @@ void K_UpdateMatchRaceBots(void)
}
}
if (K_CanChangeRules(true) == false
|| (gametyperules & GTR_BOTS) == 0
|| difficulty == 0)
if (difficulty == 0)
{
// Remove bots if there are any.
wantedbots = 0;
@ -209,7 +225,11 @@ void K_UpdateMatchRaceBots(void)
}
// Rearrange usable bot skins list to prevent gaps for randomised selection
for (i = 0; i < usableskins; i++)
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
usableskins = 0; // force a crack team of Eggrobo
}
else for (i = 0; i < usableskins; i++)
{
if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true)))
{

View file

@ -831,7 +831,7 @@ boolean K_CanChangeRules(boolean allowdemos)
return false;
}
if (gametype == GT_TUTORIAL)
if (gametype == GT_TUTORIAL || tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
// Tutorials are locked down.
return false;

View file

@ -401,6 +401,9 @@ boolean K_IsPlayerLosing(player_t *player)
if (specialstageinfo.valid == true)
return false; // anything short of DNF is COOL
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
return true; // anything short of perfect is SUCK
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)

View file

@ -291,7 +291,11 @@ void level_tally_t::Init(player_t *player)
owner = player;
gt = gametype;
const boolean game_over = ((player->pflags & PF_LOSTLIFE) == PF_LOSTLIFE);
const boolean game_over = (
G_GametypeUsesLives()
? ((player->pflags & PF_LOSTLIFE) == PF_LOSTLIFE)
: (tutorialchallenge == TUTORIALSKIP_INPROGRESS && K_IsPlayerLosing(player))
);
time = std::min(static_cast<INT32>(player->realtime), (100 * 60 * TICRATE) - 1);
ringPool = player->totalring;
@ -384,7 +388,14 @@ void level_tally_t::Init(player_t *player)
{
if (game_over == true)
{
if (player->lives <= 0)
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
snprintf(
header, sizeof header,
"NICE TRY"
);
}
else if (G_GametypeUsesLives() && player->lives <= 0)
{
snprintf(
header, sizeof header,
@ -991,7 +1002,7 @@ void level_tally_t::Draw(void)
|| state == TALLY_ST_GAMEOVER_LIVES
|| state == TALLY_ST_GAMEOVER_DONE)
{
if (owner->lives > 0)
if (G_GametypeUsesLives() && owner->lives > 0)
{
srb2::Draw lives_drawer = drawer
.xy(

View file

@ -219,6 +219,9 @@ int LUA_PushGlobals(lua_State *L, const char *word)
} else if (fastcmp(word,"podiummap")) {
lua_pushstring(L, podiummap);
return 1;
} else if (fastcmp(word,"tutorialchallengemap")) {
lua_pushstring(L, tutorialchallengemap);
return 1;
// end map vars
// begin CTF colors
} else if (fastcmp(word,"skincolor_redteam")) {

View file

@ -661,6 +661,7 @@ void M_ClearStats(void)
gamedata->evercrashed = false;
gamedata->chaokeytutorial = false;
gamedata->majorkeyskipattempted = false;
gamedata->finishedtutorialchallenge = false;
gamedata->musicstate = GDMUSIC_NONE;
gamedata->importprofilewins = false;
@ -1499,6 +1500,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player)
return true;
}
return false;
case UC_TUTORIALSKIP:
return (gamedata->finishedtutorialchallenge == true);
case UC_PASSWORD:
return (cn->stringvar == NULL);
@ -2307,6 +2310,8 @@ static const char *M_GetConditionString(condition_t *cn)
if (gamedata->evercrashed)
return "launch \"Dr. Robotnik's Ring Racers\" again after a game crash";
return NULL;
case UC_TUTORIALSKIP:
return "successfully skip the Tutorial";
case UC_PASSWORD:
return "enter a secret password";

View file

@ -63,6 +63,7 @@ typedef enum
UC_CREDITS, // Finish watching the credits
UC_REPLAY, // Save a replay
UC_CRASH, // Hee ho !
UC_TUTORIALSKIP, // Complete the Tutorial Challenge
UC_PASSWORD, // Type in something funny
@ -363,6 +364,7 @@ struct gamedata_t
boolean evercrashed;
boolean chaokeytutorial;
boolean majorkeyskipattempted;
boolean finishedtutorialchallenge;
gdmusic_t musicstate;
// BACKWARDS COMPAT ASSIST

View file

@ -184,8 +184,12 @@ void M_OpenPauseMenu(void)
&& roundqueue.size != 0
);
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
if (gamestate == GS_LEVEL && !retryallowed)
// NO RETRY, ONLY GIVE UP
giveup = true;
}
else if (gamestate == GS_LEVEL && !retryallowed)
{
if (gametype == GT_TUTORIAL)
{

View file

@ -13038,6 +13038,9 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj)
skincolornum_t emcolor;
INT16 tagnum = mthing->tid;
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
return false; // No out-of-sequence goodies
while (emblem)
{
if (emblem->type == ET_GLOBAL && emblem->tag == tagnum)
@ -13605,7 +13608,7 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj)
}
case MT_SPRAYCAN:
{
if (nummapspraycans == UINT8_MAX)
if (nummapspraycans == UINT8_MAX || tutorialchallenge == TUTORIALSKIP_INPROGRESS)
{
P_RemoveMobj(mobj);
return false;

View file

@ -7609,7 +7609,10 @@ static void P_InitLevelSettings(void)
gamespeed = grandprixinfo.gamespeed;
}
}
else if (modeattacking)
else if (
modeattacking != ATTACKING_NONE
|| tutorialchallenge == TUTORIALSKIP_INPROGRESS
)
{
if (gametyperules & GTR_CIRCUIT)
{

View file

@ -1905,7 +1905,8 @@ void Y_DetermineIntermissionType(void)
// or for failing in time attack mode
|| (modeattacking && (players[consoleplayer].pflags & PF_NOCONTEST))
// or for explicit requested skip (outside of modeattacking)
|| (modeattacking == ATTACKING_NONE && skipstats != 0))
|| (modeattacking == ATTACKING_NONE && skipstats != 0)
|| (tutorialchallenge != TUTORIALSKIP_NONE))
{
intertype = int_none;
return;