diff --git a/src/deh_soc.c b/src/deh_soc.c index 53da34d0b..53ad24ae6 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -2965,7 +2965,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) || (++offset && fastcmp(params[0], "REPLAY")) || (++offset && fastcmp(params[0], "CRASH")) || (++offset && fastcmp(params[0], "TUTORIALSKIP")) - || (++offset && fastcmp(params[0], "TUTORIALDONE"))) + || (++offset && fastcmp(params[0], "TUTORIALDONE")) + || (++offset && fastcmp(params[0], "PLAYGROUNDROUTE"))) { //PARAMCHECK(1); ty = UC_ADDON + offset; @@ -3633,6 +3634,11 @@ void readmaincfg(MYFILE *f, boolean mainfile) titlemap = Z_StrDup(word2); titlechanged = true; } + else if (fastcmp(word, "TUTORIALPLAYGROUNDMAP")) + { + Z_Free(tutorialplaygroundmap); + tutorialplaygroundmap = Z_StrDup(word2); + } else if (fastcmp(word, "TUTORIALCHALLENGEMAP")) { Z_Free(tutorialchallengemap); diff --git a/src/doomstat.h b/src/doomstat.h index 748b0773e..9a8efea18 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -280,6 +280,7 @@ extern boolean looptitle; extern char * bootmap; //bootmap for loading a map on startup extern char * podiummap; // map to load for podium +extern char * tutorialplaygroundmap; // map to load for playground extern char * tutorialchallengemap; // map to load for tutorial skip extern UINT8 tutorialchallenge; #define TUTORIALSKIP_NONE 0 diff --git a/src/g_game.c b/src/g_game.c index d9c8c1551..be9af9549 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -179,6 +179,7 @@ boolean looptitle = true; char * bootmap = NULL; //bootmap for loading a map on startup char * podiummap = NULL; // map to load for podium +char * tutorialplaygroundmap = NULL; // map to load for playground char * tutorialchallengemap = NULL; // map to load for tutorial skip UINT8 tutorialchallenge = TUTORIALSKIP_NONE; @@ -5076,7 +5077,7 @@ void G_EndGame(void) // Only do evaluation and credits in singleplayer contexts if (!netgame) { - if (gametype == GT_TUTORIAL) + if (gametype == GT_TUTORIAL && gamedata->gonerlevel < GDGONER_DONE) { // Tutorial was finished gamedata->tutorialdone = true; diff --git a/src/g_gamedata.cpp b/src/g_gamedata.cpp index 077eb0b81..2695dffc9 100644 --- a/src/g_gamedata.cpp +++ b/src/g_gamedata.cpp @@ -83,6 +83,7 @@ void srb2::save_ng_gamedata() ng.milestones.enteredtutorialchallenge = gamedata->enteredtutorialchallenge; ng.milestones.sealedswapalerted = gamedata->sealedswapalerted; ng.milestones.tutorialdone = gamedata->tutorialdone; + ng.milestones.playgroundroute = gamedata->playgroundroute; ng.milestones.gonerlevel = gamedata->gonerlevel; ng.prisons.thisprisoneggpickup = gamedata->thisprisoneggpickup; ng.prisons.prisoneggstothispickup = gamedata->prisoneggstothispickup; @@ -471,6 +472,7 @@ void srb2::load_ng_gamedata() gamedata->enteredtutorialchallenge = js.milestones.enteredtutorialchallenge; gamedata->sealedswapalerted = js.milestones.sealedswapalerted; gamedata->tutorialdone = js.milestones.tutorialdone; + gamedata->playgroundroute = js.milestones.playgroundroute; gamedata->gonerlevel = js.milestones.gonerlevel; gamedata->thisprisoneggpickup = js.prisons.thisprisoneggpickup; diff --git a/src/g_gamedata.h b/src/g_gamedata.h index 0027ef582..9209ecb2b 100644 --- a/src/g_gamedata.h +++ b/src/g_gamedata.h @@ -96,6 +96,7 @@ struct GamedataMilestonesJson final bool enteredtutorialchallenge; bool sealedswapalerted; bool tutorialdone; + bool playgroundroute; SRB2_JSON_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( GamedataMilestonesJson, @@ -109,7 +110,8 @@ struct GamedataMilestonesJson final finishedtutorialchallenge, enteredtutorialchallenge, sealedswapalerted, - tutorialdone + tutorialdone, + playgroundroute ) }; diff --git a/src/k_menu.h b/src/k_menu.h index 28db4810e..b612151da 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -229,9 +229,12 @@ void M_GonerBGTick(void); void M_GonerBGImplyPassageOfTime(void); void M_DrawGonerBack(void); void M_GonerProfile(INT32 choice); +void M_GonerChoice(INT32 choice); void M_GonerTutorial(INT32 choice); +void M_GonerPlayground(INT32 choice); void M_GonerResetLooking(int type); void M_GonerCheckLooking(void); +void M_GonerResetText(void); void M_GonerGDQ(boolean opinion); boolean M_GonerMusicPlayable(void); diff --git a/src/lua_script.c b/src/lua_script.c index 1f01010cb..c7e9745f3 100644 --- a/src/lua_script.c +++ b/src/lua_script.c @@ -216,6 +216,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,"tutorialplaygroundmap")) { + lua_pushstring(L, tutorialplaygroundmap); + return 1; } else if (fastcmp(word,"tutorialchallengemap")) { lua_pushstring(L, tutorialchallengemap); return 1; diff --git a/src/m_cond.c b/src/m_cond.c index 026dace10..81908d533 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -663,6 +663,7 @@ void M_ClearStats(void) gamedata->finishedtutorialchallenge = false; gamedata->sealedswapalerted = false; gamedata->tutorialdone = false; + gamedata->playgroundroute = false; gamedata->musicstate = GDMUSIC_NONE; gamedata->importprofilewins = false; @@ -1755,6 +1756,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (gamedata->finishedtutorialchallenge == true); case UC_TUTORIALDONE: return (gamedata->tutorialdone == true); + case UC_PLAYGROUND: + return (gamedata->playgroundroute == true); case UC_PASSWORD: return (cn->stringvar == NULL); @@ -2641,6 +2644,8 @@ static const char *M_GetConditionString(condition_t *cn) return "successfully skip the Tutorial"; case UC_TUTORIALDONE: return "complete the Tutorial"; + case UC_PLAYGROUND: + return "pick the Playground"; case UC_PASSWORD: return "enter a secret password"; diff --git a/src/m_cond.h b/src/m_cond.h index 10b9e4883..f636b9515 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -68,6 +68,7 @@ typedef enum UC_CRASH, // Hee ho ! UC_TUTORIALSKIP, // Complete the Tutorial Challenge UC_TUTORIALDONE, // Complete the Tutorial at all + UC_PLAYGROUND, // Go to the playground instead..? UC_PASSWORD, // Type in something funny @@ -301,7 +302,7 @@ typedef enum { #define GDCONVERT_ROUNDSTOKEY 5 -#define GDINIT_CHAOKEYS 10 // Start with 10 Chao Keys !! +#define GDINIT_CHAOKEYS 0 // Start with ZERO Chao Keys. You get NONE. fizzy lifting dink #define GDINIT_PRISONSTOPRIZE 15 // 15 Prison Eggs to your [Wild Prize] !! typedef enum { @@ -395,6 +396,7 @@ struct gamedata_t boolean finishedtutorialchallenge; boolean sealedswapalerted; boolean tutorialdone; + boolean playgroundroute; gdmusic_t musicstate; UINT8 gonerlevel; diff --git a/src/menus/main-goner.cpp b/src/menus/main-goner.cpp index 75b41d172..2e2a2f542 100644 --- a/src/menus/main-goner.cpp +++ b/src/menus/main-goner.cpp @@ -32,6 +32,7 @@ #include "../core/string.h" static void M_GonerDrawer(void); +static void M_GonerChoiceDrawer(void); static void M_GonerConclude(INT32 choice); static boolean M_GonerInputs(INT32 ch); @@ -55,9 +56,9 @@ menuitem_t MAIN_Goner[] = "ASSIGN VEHICLE INPUTS.", NULL, {.routine = M_GonerProfile}, 0, 0}, - {IT_STRING | IT_CALL, "BEGIN TUTORIAL", - "PREPARE FOR INTEGRATION.", NULL, - {.routine = M_GonerTutorial}, 0, 0}, + {IT_STRING | IT_CALL, "MAKE CHOICE", + "PREPARE FOR INTEGRATION?", NULL, + {.routine = M_GonerChoice}, 0, 0}, {IT_STRING | IT_CALL, "START GAME", "I WILL SUCCEED.", NULL, @@ -82,9 +83,57 @@ menu_t MAIN_GonerDef = { M_GonerInputs, }; +menuitem_t MAIN_GonerChoice[] = +{ + {IT_STRING | IT_CALL, "Tails' way", + "As a child scientist, Tails has recorded bits\n" + "and pieces of an adventure he and Eggman went\n" + "on while trying out their new Ring Racers.\n" + "\n" + "This is a structured, back-to-basics session\n" + "that will likely take ""\x88""10-20 minutes""\x80"" of your time.", + NULL, {.routine = M_GonerTutorial}, 0, 0}, + + //{IT_STRING, NULL, NULL, NULL, {.routine = M_QuitSRB2}, 0, 0}, // will be replaced + + {IT_STRING | IT_CALL, "Eggman's way", + "As a childlike scientist, Eggman has turned the\n" + "wrecked Egg Carrier into a giant skatepark,\n" + "dotted with fun collectables to test drivers.\n" + "\n" + "You can ""\x88""exit immediately""\x80"" and get to racing...\n" + "or spend ""\x88""as long as you want""\x80"" in the playground!", + NULL, {.routine = M_GonerPlayground}, 0, 0}, +}; + +menu_t MAIN_GonerChoiceDef = { + sizeof (MAIN_GonerChoice) / sizeof (menuitem_t), + &MAIN_GonerDef, + 0, + MAIN_GonerChoice, + 26, 160, + 0, 0, + MBF_UD_LR_FLIPPED, + "_GONER", + 0, 0, + M_GonerChoiceDrawer, + M_DrawGonerBack, + NULL, + NULL, + NULL, + NULL, +}; + namespace { +typedef enum +{ + GONERCHOICE_TAILS = 0, + //GONERCHOICE_NONEBINEY, + GONERCHOICE_EGGMAN +} gonerchoices_t; + typedef enum { GONERSPEAKER_EGGMAN = 0, @@ -429,16 +478,6 @@ void Miles_Electric_Lower() int goner_levelworking = GDGONER_INIT; bool goner_gdq = false; -void M_GonerResetText(void) -{ - goner_typewriter.ClearText(); - LinesToDigest.clear(); - LinesOutput.clear(); - - goner_scroll = 0; - goner_scrollend = -1; -} - static void Initial_Control_Info(void) { if (cv_currprofile.value != -1) @@ -669,11 +708,22 @@ void M_AddGonerLines(void) LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, 0, "Now, Metal... it's important you pay attention."); LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/5, - "It's time to ""\x87""begin your Tutorial""\x80""!"); + "We have a ""\x88""choice""\x80"" ready for you."); LinesToDigest.emplace_front(GONERSPEAKER_TAILS, 0, - "Remember, MS-1. Even when you move on from this setup, you "\ - "can always change your ""\x87""Options""\x80"" at any time from the menu."); + "You can play back our testing data as a sort of ""\x82""tutorial""\x80"\ + " and learn the core parts of driving in a safe environment..."); + + LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/5, + "...or if you're too headstrong and want to figure things out"\ + " for yourself, we can let you loose in our ""\x85""playground""\x80""!"); + LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/2, + "If you do run into trouble, the ""\x82""tutorial""\x80"" can"\ + " always be found in the ""\x87""Extras""\x80"" menu later on."); + + LinesToDigest.emplace_front(GONERSPEAKER_TAILS, 0, + "Either way, MS-1. Even when you move on from this setup,"\ + " you can always change your ""\x87""Options""\x80"" at any time."); LinesToDigest.emplace_front(0, Miles_Look_Electric); break; @@ -704,8 +754,6 @@ void M_AddGonerLines(void) LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/2, "But yes. Perhaps now you have a better appreciation of what "\ "we're building here, Metal."); - LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/2, - "If you need to learn more, you can always come back to the Tutorial later in the ""\x87""Extras""\x80"" menu."); LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/5, "Now, I'm willing to let bygones be bygones."); LinesToDigest.emplace_front(GONERSPEAKER_EGGMAN, TICRATE/2, @@ -1218,6 +1266,123 @@ static void M_GonerDrawer(void) M_DrawHorizontalMenu(); } +static void M_GonerChoiceDrawer(void) +{ + srb2::Draw drawer = srb2::Draw(); + + const INT32 lex = (24 + BASEVIDWIDTH/2)/2; + + if (itemOn == GONERCHOICE_TAILS) + { + drawer + .size((BASEVIDWIDTH/2) + 25, BASEVIDHEIGHT) + .fill(60); + + drawer + .xy((BASEVIDWIDTH/2) + 40 + 1, 28+3) + .colormap(SKINCOLOR_ORANGE) + .flags(V_FLIP) + .patch("MENUPLTR"); + + drawer + .xy(lex, 28) + .font(srb2::Draw::Font::kGamemode) + .align(srb2::Draw::Align::kCenter) + .text(currentMenu->menuitems[itemOn].text); + + drawer + .xy(8, 72) + .font(srb2::Draw::Font::kThin) + .align(srb2::Draw::Align::kLeft) + .text(currentMenu->menuitems[itemOn].tooltip); + + drawer + .xy(lex, 154) + .font(srb2::Draw::Font::kFreeplay) + .align(srb2::Draw::Align::kCenter) + .text("(unlocks 20 )"); + + drawer + .xy(lex, 154+14) + .font(srb2::Draw::Font::kThin) + .align(srb2::Draw::Align::kCenter) + .flags(V_TRANSLUCENT) + .text("+ more surprises to find"); + + drawer + .xy(lex + 26, 154-4) + .patch("UN_CHA00"); + } + else if (itemOn == GONERCHOICE_EGGMAN) + { + drawer + .x((BASEVIDWIDTH/2) - 24) + .size((BASEVIDWIDTH/2) + 24, BASEVIDHEIGHT) + .fill(44); + + drawer + .xy((BASEVIDWIDTH/2) - 40, 28+3) + .colormap(SKINCOLOR_RED) + .patch("MENUPLTR"); + + drawer + .xy(BASEVIDWIDTH - lex, 28) + .font(srb2::Draw::Font::kGamemode) + .align(srb2::Draw::Align::kCenter) + .text(currentMenu->menuitems[itemOn].text); + + drawer + .xy(BASEVIDWIDTH - 8, 72) + .font(srb2::Draw::Font::kThin) + .align(srb2::Draw::Align::kRight) + .text(currentMenu->menuitems[itemOn].tooltip); + + drawer + .xy(BASEVIDWIDTH - lex, 154) + .font(srb2::Draw::Font::kFreeplay) + .align(srb2::Draw::Align::kCenter) + .text("(unlocks Addons/Online)"); + } + + // Un-highlighteds done this weird way because of GONERCHOICE_NONEBINEY + + if (itemOn != GONERCHOICE_TAILS) + { + drawer + .size(20, BASEVIDHEIGHT) + .fill(60); + + drawer + .xy(25, 39) + .font(srb2::Draw::Font::kFreeplay) + .align(srb2::Draw::Align::kLeft) + .text(currentMenu->menuitems[GONERCHOICE_TAILS].text); + + drawer + .xy(20 - 3 - (skullAnimCounter/5), 39+6) + .patch("CUPARROW"); + } + + if (itemOn != GONERCHOICE_EGGMAN) + { + drawer + .x(BASEVIDWIDTH - 20) + .size(20, BASEVIDHEIGHT) + .fill(44); + + drawer + .xy(BASEVIDWIDTH - 25, 39) + .font(srb2::Draw::Font::kFreeplay) + .align(srb2::Draw::Align::kRight) + .text(currentMenu->menuitems[GONERCHOICE_EGGMAN].text); + + drawer + .xy((BASEVIDWIDTH - 20 + 3) + (skullAnimCounter/5), 39+6) + .flags(V_FLIP) + .patch("CUPARROW"); + } +} + // --- void M_GonerProfile(INT32 choice) @@ -1245,19 +1410,16 @@ void M_GonerProfile(INT32 choice) M_GonerResetLooking(GDGONER_PROFILE); } -static void M_GonerSurveyResponse(INT32 ch) +static void M_GonerTutorialResponse(INT32 ch) { if (ch != MA_YES) return; - if (gamedata->gonerlevel < GDGONER_OUTRO) - gamedata->gonerlevel = GDGONER_OUTRO; + M_GonerTutorial(0); } -void M_GonerTutorial(INT32 choice) +void M_GonerChoice(INT32 choice) { - (void)choice; - if (cv_currprofile.value == -1) { const INT32 maxp = PR_GetNumProfiles(); @@ -1270,6 +1432,43 @@ void M_GonerTutorial(INT32 choice) PR_ApplyProfile(profilen, 0); } + if (gamedata->gonerlevel >= GDGONER_OUTRO) + { + M_StartMessage("First Boot Tutorial", + "You've already played the Tutorial! Do you want to see it again?", + &M_GonerTutorialResponse, MM_YESNO, "I'd love to", "Not right now"); + return; + } + + M_SetupNextMenu(&MAIN_GonerChoiceDef, false); +} + +static void M_GonerSurveyResponse(INT32 ch) +{ + if (ch != MA_YES) + return; + + if (gamedata->gonerlevel < GDGONER_OUTRO) + gamedata->gonerlevel = GDGONER_OUTRO; + + if (currentMenu == &MAIN_GonerChoiceDef) + M_GoBack(0); +} + +static void M_GonerSurvey(INT32 choice) +{ + (void)choice; + + // The game is incapable of progression, but I can't bring myself to put an I_Error here. + M_StartMessage("First Boot Error", + "YOU ACCEPT EVERYTHING THAT\nWILL HAPPEN FROM NOW ON.", + &M_GonerSurveyResponse, MM_YESNO, "I agree", "Cancel"); +} + +void M_GonerTutorial(INT32 choice) +{ + (void)choice; + // Please also see M_LevelSelectInit as called in extras-1.c levellist.netgame = false; levellist.canqueue = false; @@ -1279,19 +1478,58 @@ void M_GonerTutorial(INT32 choice) if (!M_LevelListFromGametype(GT_TUTORIAL) && gamedata->gonerlevel < GDGONER_OUTRO) { - // The game is incapable of progression, but I can't bring myself to put an I_Error here. - M_StartMessage("Agreement", - "YOU ACCEPT EVERYTHING THAT WILL HAPPEN FROM NOW ON.", - &M_GonerSurveyResponse, MM_YESNO, "I agree", "Cancel"); + M_GonerSurvey(0); + return; } } +void M_GonerPlayground(INT32 choice) +{ + (void)choice; + + UINT16 playgroundmap = NEXTMAP_INVALID; + if (tutorialplaygroundmap) + playgroundmap = G_MapNumber(tutorialplaygroundmap); + + if (playgroundmap >= nummapheaders) + { + M_GonerSurvey(0); + return; + } + + multiplayer = true; + + M_MenuToLevelPreamble(0, false); + + D_MapChange( + playgroundmap+1, + GT_TUTORIAL, + false, + true, + 0, + false, + false + ); + + M_ClearMenus(true); + restoreMenu = NULL; // Playground Hack + + // need to do all this here because it will skip returning to goner and there are circumstances (game close) where DoCompleted won't be called + M_GonerResetText(); + gamedata->gonerlevel = GDGONER_DONE; + gamedata->playgroundroute = true; + gamedata->deferredsave = true; +} + static void M_GonerConclude(INT32 choice) { (void)choice; gamedata->gonerlevel = GDGONER_DONE; + if (gamedata->chaokeys < 20) + gamedata->chaokeys = 20; + F_StartIntro(); M_ClearMenus(true); M_GonerResetText(); @@ -1380,3 +1618,13 @@ static boolean M_GonerInputs(INT32 ch) return false; } + +void M_GonerResetText(void) +{ + goner_typewriter.ClearText(); + LinesToDigest.clear(); + LinesOutput.clear(); + + goner_scroll = 0; + goner_scrollend = -1; +}