Add Goner Choice

- Selection between Tails' Way (existing Tutorial) and Eggman's Way (Playground)
    - Semi-passable UI
    - Characterful descriptions
- Add "PlaygroundRoute" condition to Challenges
    - Fires if you select Eggman's Way
- 0 Chao Keys unless you go back to Goner for the outro (which Playground skips)
This commit is contained in:
toaster 2025-07-23 19:25:52 +01:00
parent c06fc9cccf
commit 295e8dd0ce
10 changed files with 305 additions and 32 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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