Minimum viable product of Chao Keys condition bypass

- Start with 3, per Sakurai's prior art.
- Earn them per certain number of rounds
    - DEVELOP builds: once every 4 rounds
    - Release builds: once every 50 rounds
    - Has an internal cap based on the maximum number of unlockables supported.
        - Possible future work could adjust this to restrict based on the maximum number of unlockables unlocks.pk3 actually has set.
- Use on the Challenges screen to bust open small tiles with hints (or the very first tile, if you haven't unlocked anything yet).
    - Will do a funny shake if you try anything else.
- Interrupts menu flow just like getting an unlock.
    - The matches you've played will tick upwards, giving you keys as they loop over.
This commit is contained in:
toaster 2023-03-09 22:31:34 +00:00
parent 328ab0059a
commit 8b437d5a32
6 changed files with 237 additions and 78 deletions

View file

@ -4141,6 +4141,7 @@ static void G_DoCompleted(void)
roundtype = GDGT_SPECIAL; roundtype = GDGT_SPECIAL;
gamedata->roundsplayed[roundtype]++; gamedata->roundsplayed[roundtype]++;
gamedata->pendingkeyrounds++;
// Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve // Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve
M_UpdateUnlockablesAndExtraEmblems(true); M_UpdateUnlockablesAndExtraEmblems(true);
@ -4562,6 +4563,12 @@ void G_LoadGameData(void)
gamedata->roundsplayed[i] = READUINT32(save.p); gamedata->roundsplayed[i] = READUINT32(save.p);
} }
gamedata->pendingkeyrounds = READUINT32(save.p);
gamedata->pendingkeyroundoffset = READUINT8(save.p);
gamedata->keyspending = READUINT8(save.p);
gamedata->chaokeys = READUINT8(save.p);
gamedata->usedkeys = READUINT8(save.p);
gamedata->crashflags = READUINT8(save.p); gamedata->crashflags = READUINT8(save.p);
if (gamedata->crashflags & GDCRASH_LAST) if (gamedata->crashflags & GDCRASH_LAST)
gamedata->crashflags |= GDCRASH_ANY; gamedata->crashflags |= GDCRASH_ANY;
@ -4740,7 +4747,12 @@ void G_SaveGameData(boolean dirty)
return; return;
} }
length = (4+1+4+4+(4*GDGT_MAX)+1+1+4+(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+4+4+2); length = (4+1+4+4+
(4*GDGT_MAX)+
4+1+1+1+1+
1+1+4+
(MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+
4+4+2);
if (gamedata->challengegrid) if (gamedata->challengegrid)
{ {
length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT; length += gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT;
@ -4765,6 +4777,12 @@ void G_SaveGameData(boolean dirty)
WRITEUINT32(save.p, gamedata->roundsplayed[i]); WRITEUINT32(save.p, gamedata->roundsplayed[i]);
} }
WRITEUINT32(save.p, gamedata->pendingkeyrounds); // 4
WRITEUINT8(save.p, gamedata->pendingkeyroundoffset); // 1
WRITEUINT8(save.p, gamedata->keyspending); // 1
WRITEUINT8(save.p, gamedata->chaokeys); // 1
WRITEUINT8(save.p, gamedata->usedkeys); // 1
{ {
UINT8 crashflags = (gamedata->crashflags & GDCRASH_ANY); UINT8 crashflags = (gamedata->crashflags & GDCRASH_ANY);
if (dirty) if (dirty)

View file

@ -1144,7 +1144,9 @@ void M_DrawAddons(void);
#define CC_UNLOCKED 1 #define CC_UNLOCKED 1
#define CC_TALLY 2 #define CC_TALLY 2
#define CC_ANIM 3 #define CC_ANIM 3
#define CC_MAX 4 #define CC_CHAOANIM 4
#define CC_CHAONOPE 5
#define CC_MAX 6
#define TILEFLIP_MAX 16 #define TILEFLIP_MAX 16
@ -1155,7 +1157,6 @@ extern struct timeattackmenu_s {
} timeattackmenu; } timeattackmenu;
// Keep track of some pause menu data for visual goodness. // Keep track of some pause menu data for visual goodness.
extern struct challengesmenu_s { extern struct challengesmenu_s {
@ -1174,6 +1175,7 @@ extern struct challengesmenu_s {
boolean pending; boolean pending;
boolean requestnew; boolean requestnew;
boolean chaokeyadd;
boolean requestflip; boolean requestflip;

View file

@ -5170,6 +5170,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y)
#define challengetransparentstrength 8 #define challengetransparentstrength 8
#define challengesgridstep 22 #define challengesgridstep 22
#define challengekeybarwidth 50
void M_DrawChallenges(void) void M_DrawChallenges(void)
{ {
@ -5293,6 +5294,26 @@ void M_DrawChallenges(void)
challengedesc: challengedesc:
// Chao Keys
{
patch_t *key = W_CachePatchName("UN_CHA00", PU_CACHE);
INT32 offs = challengesmenu.unlockcount[CC_CHAONOPE];
if (offs & 1)
offs = -offs;
offs /= 2;
V_DrawFixedPatch((6+offs)*FRACUNIT, 5*FRACUNIT, FRACUNIT, 0, key, NULL);
V_DrawKartString((25+offs), 9-challengesmenu.unlockcount[CC_CHAOANIM], 0, va("%u", gamedata->chaokeys));
offs = challengekeybarwidth;
if ((gamedata->chaokeys + gamedata->usedkeys) < GDMAX_CHAOKEYS)
offs = ((gamedata->pendingkeyroundoffset * challengekeybarwidth)/GDCONVERT_ROUNDSTOKEY);
if (offs > 0)
V_DrawFill(1, 25, offs, 2, 0);
if (offs < challengekeybarwidth)
V_DrawFadeFill(1+offs, 25, challengekeybarwidth-offs, 2, 0, 31, challengetransparentstrength);
}
// Tally // Tally
{ {
str = va("%d/%d", str = va("%d/%d",
@ -5347,6 +5368,7 @@ challengedesc:
#undef challengetransparentstrength #undef challengetransparentstrength
#undef challengesgridstep #undef challengesgridstep
#undef challengekeybarwidth
// Statistics menu // Statistics menu

View file

@ -548,6 +548,12 @@ void M_ClearSecrets(void)
Z_Free(gamedata->challengegrid); Z_Free(gamedata->challengegrid);
gamedata->challengegrid = NULL; gamedata->challengegrid = NULL;
gamedata->challengegridwidth = 0; gamedata->challengegridwidth = 0;
gamedata->pendingkeyrounds = 0;
gamedata->pendingkeyroundoffset = 0;
gamedata->keyspending = 0;
gamedata->chaokeys = 3; // Start with 3 !!
gamedata->usedkeys = 0;
} }
// ---------------------- // ----------------------
@ -1336,8 +1342,7 @@ static boolean M_CheckUnlockConditions(player_t *player)
boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
{ {
INT32 i; UINT16 i = 0, response = 0, newkeys = 0;
UINT8 response = 0;
if (!gamedata) if (!gamedata)
{ {
@ -1355,6 +1360,14 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
response = M_CheckUnlockConditions(NULL); response = M_CheckUnlockConditions(NULL);
while ((gamedata->keyspending + gamedata->chaokeys + gamedata->usedkeys) < GDMAX_CHAOKEYS
&& ((gamedata->pendingkeyrounds + gamedata->pendingkeyroundoffset)/GDCONVERT_ROUNDSTOKEY) > gamedata->keyspending)
{
gamedata->keyspending++;
newkeys++;
response |= true;
}
if (!demo.playback && Playing() && (gamestate == GS_LEVEL)) if (!demo.playback && Playing() && (gamestate == GS_LEVEL))
{ {
for (i = 0; i <= splitscreen; i++) for (i = 0; i <= splitscreen; i++)
@ -1367,7 +1380,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
} }
} }
if (!response && loud) if (loud && response == 0)
{ {
return false; return false;
} }
@ -1397,8 +1410,10 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
response++; response++;
} }
response += newkeys;
// Announce // Announce
if (response) if (response != 0)
{ {
if (loud) if (loud)
{ {
@ -1409,7 +1424,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
return false; return false;
} }
UINT8 M_GetNextAchievedUnlock(void) UINT16 M_GetNextAchievedUnlock(void)
{ {
UINT8 i; UINT8 i;
@ -1434,6 +1449,11 @@ UINT8 M_GetNextAchievedUnlock(void)
return i; return i;
} }
if (gamedata->keyspending > 0)
{
return PENDING_CHAOKEYS;
}
return MAXUNLOCKABLES; return MAXUNLOCKABLES;
} }

View file

@ -207,6 +207,13 @@ typedef enum
// This is the largest number of 9s that will fit in UINT32. // This is the largest number of 9s that will fit in UINT32.
#define GDMAX_RINGS 999999999 #define GDMAX_RINGS 999999999
#define GDMAX_CHAOKEYS MAXUNLOCKABLES
#ifdef DEVELOP
#define GDCONVERT_ROUNDSTOKEY 4
#else
#define GDCONVERT_ROUNDSTOKEY 50
#endif
typedef enum { typedef enum {
GDGT_RACE, GDGT_RACE,
@ -247,6 +254,13 @@ struct gamedata_t
UINT32 roundsplayed[GDGT_MAX]; UINT32 roundsplayed[GDGT_MAX];
UINT32 totalrings; UINT32 totalrings;
// Chao Key condition bypass
UINT32 pendingkeyrounds;
UINT8 pendingkeyroundoffset;
UINT8 keyspending;
UINT8 chaokeys;
UINT8 usedkeys;
// SPECIFIC SPECIAL EVENTS // SPECIFIC SPECIAL EVENTS
boolean everloadedaddon; boolean everloadedaddon;
UINT8 crashflags; UINT8 crashflags;
@ -297,7 +311,10 @@ void M_ClearStats(void);
// Updating conditions and unlockables // Updating conditions and unlockables
boolean M_CheckCondition(condition_t *cn, player_t *player); boolean M_CheckCondition(condition_t *cn, player_t *player);
boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud); boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud);
UINT8 M_GetNextAchievedUnlock(void);
#define PENDING_CHAOKEYS (UINT16_MAX-1)
UINT16 M_GetNextAchievedUnlock(void);
UINT8 M_CheckLevelEmblems(void); UINT8 M_CheckLevelEmblems(void);
UINT8 M_CompletionEmblems(void); UINT8 M_CompletionEmblems(void);

View file

@ -49,17 +49,59 @@ menu_t MISC_StatisticsDef = {
struct challengesmenu_s challengesmenu; struct challengesmenu_s challengesmenu;
static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh)
{ {
UINT8 i; UINT8 i;
SINT8 work; SINT8 work;
if (unlockid >= MAXUNLOCKABLES && gamedata->pendingkeyrounds > 0
&& ((gamedata->chaokeys + gamedata->usedkeys) < GDMAX_CHAOKEYS))
challengesmenu.chaokeyadd = true;
if (fresh && unlockid >= MAXUNLOCKABLES)
{
UINT8 selection[MAXUNLOCKABLES];
UINT8 numunlocks = 0;
// Get a random available unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
if (!gamedata->unlocked[i])
{
continue;
}
selection[numunlocks++] = i;
}
if (!numunlocks)
{
// ...OK, get a random unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
selection[numunlocks++] = i;
}
}
unlockid = selection[M_RandomKey(numunlocks)];
}
if (unlockid >= MAXUNLOCKABLES) if (unlockid >= MAXUNLOCKABLES)
return; return;
challengesmenu.currentunlock = unlockid; challengesmenu.currentunlock = unlockid;
challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock);
challengesmenu.unlockanim = 0; challengesmenu.unlockanim = (challengesmenu.pending && !challengesmenu.chaokeyadd ? 0 : MAXUNLOCKTIME);
if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL)
return; return;
@ -161,12 +203,13 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh)
menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
{ {
UINT8 i; UINT16 i, newunlock;
UINT16 newunlock = M_GetNextAchievedUnlock();
M_UpdateUnlockablesAndExtraEmblems(false); M_UpdateUnlockablesAndExtraEmblems(false);
if ((challengesmenu.pending = (newunlock < MAXUNLOCKABLES))) newunlock = M_GetNextAchievedUnlock();
if ((challengesmenu.pending = (newunlock != MAXUNLOCKABLES)))
{ {
S_StopMusic(); S_StopMusic();
MISC_ChallengesDef.prevMenu = desiredmenu; MISC_ChallengesDef.prevMenu = desiredmenu;
@ -177,6 +220,7 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
challengesmenu.ticker = 0; challengesmenu.ticker = 0;
challengesmenu.requestflip = false; challengesmenu.requestflip = false;
challengesmenu.requestnew = false; challengesmenu.requestnew = false;
challengesmenu.chaokeyadd = false;
challengesmenu.currentunlock = MAXUNLOCKABLES; challengesmenu.currentunlock = MAXUNLOCKABLES;
challengesmenu.unlockcondition = NULL; challengesmenu.unlockcondition = NULL;
@ -210,6 +254,9 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
if (challengesmenu.pending) if (challengesmenu.pending)
M_ChallengesAutoFocus(newunlock, true); M_ChallengesAutoFocus(newunlock, true);
else if (newunlock >= MAXUNLOCKABLES && gamedata->pendingkeyrounds > 0
&& ((gamedata->chaokeys + gamedata->usedkeys) < GDMAX_CHAOKEYS))
challengesmenu.chaokeyadd = true;
return &MISC_ChallengesDef; return &MISC_ChallengesDef;
} }
@ -219,7 +266,6 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
void M_Challenges(INT32 choice) void M_Challenges(INT32 choice)
{ {
UINT8 i;
(void)choice; (void)choice;
M_InterruptMenuWithChallenges(NULL); M_InterruptMenuWithChallenges(NULL);
@ -227,40 +273,7 @@ void M_Challenges(INT32 choice)
if (gamedata->challengegrid != NULL && !challengesmenu.pending) if (gamedata->challengegrid != NULL && !challengesmenu.pending)
{ {
UINT8 selection[MAXUNLOCKABLES]; M_ChallengesAutoFocus(UINT16_MAX, true);
UINT8 numunlocks = 0;
// Get a random available unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
if (!gamedata->unlocked[i])
{
continue;
}
selection[numunlocks++] = i;
}
if (!numunlocks)
{
// ...OK, get a random unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
selection[numunlocks++] = i;
}
}
M_ChallengesAutoFocus(selection[M_RandomKey(numunlocks)], true);
} }
M_SetupNextMenu(&MISC_ChallengesDef, false); M_SetupNextMenu(&MISC_ChallengesDef, false);
@ -270,7 +283,7 @@ void M_ChallengesTick(void)
{ {
const UINT8 pid = 0; const UINT8 pid = 0;
UINT16 i; UINT16 i;
UINT8 newunlock = MAXUNLOCKABLES; UINT16 newunlock = MAXUNLOCKABLES;
// Ticking // Ticking
challengesmenu.ticker++; challengesmenu.ticker++;
@ -280,8 +293,11 @@ void M_ChallengesTick(void)
if (setup_explosions[i].tics > 0) if (setup_explosions[i].tics > 0)
setup_explosions[i].tics--; setup_explosions[i].tics--;
} }
if (challengesmenu.unlockcount[CC_ANIM] > 0) for (i = CC_ANIM; i < CC_MAX; i++)
challengesmenu.unlockcount[CC_ANIM]--; {
if (challengesmenu.unlockcount[i] > 0)
challengesmenu.unlockcount[i]--;
}
M_CupSelectTick(); M_CupSelectTick();
// Update tile flip state. // Update tile flip state.
@ -307,15 +323,46 @@ void M_ChallengesTick(void)
} }
} }
if (challengesmenu.pending) if (challengesmenu.chaokeyadd == true)
{ {
// Pending mode. if (challengesmenu.ticker <= 5)
; // recreate the slight delay the unlock fades provide
else if (gamedata->pendingkeyrounds == 0)
{
challengesmenu.chaokeyadd = false;
challengesmenu.requestnew = true;
}
else if ((gamedata->chaokeys + gamedata->usedkeys) >= GDMAX_CHAOKEYS)
{
gamedata->keyspending = 0;
gamedata->pendingkeyrounds = 0;
}
else
{
if (!(--gamedata->pendingkeyrounds & 1))
{
S_StartSound(NULL, sfx_ptally);
}
if (challengesmenu.requestnew) if (++gamedata->pendingkeyroundoffset >= GDCONVERT_ROUNDSTOKEY)
{
gamedata->pendingkeyroundoffset %= GDCONVERT_ROUNDSTOKEY;
if (gamedata->keyspending > 0)
{
S_StartSound(NULL, sfx_achiev);
gamedata->keyspending--;
gamedata->chaokeys++;
challengesmenu.unlockcount[CC_CHAOANIM]++;
}
}
}
}
else if (challengesmenu.requestnew)
{ {
// The menu apparatus is requesting a new unlock. // The menu apparatus is requesting a new unlock.
challengesmenu.requestnew = false; challengesmenu.requestnew = false;
if ((newunlock = M_GetNextAchievedUnlock()) < MAXUNLOCKABLES) if ((newunlock = M_GetNextAchievedUnlock()) != MAXUNLOCKABLES)
{ {
// We got one! // We got one!
M_ChallengesAutoFocus(newunlock, false); M_ChallengesAutoFocus(newunlock, false);
@ -323,11 +370,14 @@ void M_ChallengesTick(void)
else else
{ {
// All done! Let's save the unlocks we've busted open. // All done! Let's save the unlocks we've busted open.
challengesmenu.pending = false; challengesmenu.pending = challengesmenu.chaokeyadd = false;
G_SaveGameData(true); G_SaveGameData(true);
} }
} }
else if (challengesmenu.fade < 5) else if (challengesmenu.pending)
{
// Pending mode.
if (challengesmenu.fade < 5)
{ {
// Fade increase. // Fade increase.
challengesmenu.fade++; challengesmenu.fade++;
@ -441,28 +491,58 @@ boolean M_ChallengesInputs(INT32 ch)
const boolean move = (menucmd[pid].dpad_ud != 0 || menucmd[pid].dpad_lr != 0); const boolean move = (menucmd[pid].dpad_ud != 0 || menucmd[pid].dpad_lr != 0);
(void) ch; (void) ch;
if (challengesmenu.fade) if (challengesmenu.fade || challengesmenu.chaokeyadd)
{ {
; ;
} }
#ifdef DEVELOP else if (M_MenuExtraPressed(pid)
else if (M_MenuExtraPressed(pid) && challengesmenu.extradata) // debugging && challengesmenu.extradata)
{ {
i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy;
if (challengesmenu.currentunlock < MAXUNLOCKABLES
&& !gamedata->unlocked[challengesmenu.currentunlock]
&& !unlockables[challengesmenu.currentunlock].majorunlock
&& ((challengesmenu.extradata[i].flags & CHE_HINT)
|| (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] == 0))
&& gamedata->chaokeys > 0)
{
gamedata->chaokeys--;
gamedata->usedkeys++;
challengesmenu.unlockcount[CC_CHAOANIM]++;
S_StartSound(NULL, sfx_chchng);
challengesmenu.pending = true;
M_ChallengesAutoFocus(challengesmenu.currentunlock, false);
}
else
{
challengesmenu.unlockcount[CC_CHAONOPE] = 6;
S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2
#if 0 // debugging
if (challengesmenu.currentunlock < MAXUNLOCKABLES) if (challengesmenu.currentunlock < MAXUNLOCKABLES)
{ {
if (gamedata->unlocked[challengesmenu.currentunlock] && challengesmenu.unlockanim >= UNLOCKTIME)
{
if (challengesmenu.unlockcount[CC_TALLY] > 0)
challengesmenu.unlockcount[CC_TALLY]--;
else
challengesmenu.unlockcount[CC_UNLOCKED]--;
}
Z_Free(gamedata->challengegrid); Z_Free(gamedata->challengegrid);
gamedata->challengegrid = NULL; gamedata->challengegrid = NULL;
gamedata->challengegridwidth = 0; gamedata->challengegridwidth = 0;
M_PopulateChallengeGrid(); M_PopulateChallengeGrid();
M_UpdateChallengeGridExtraData(challengesmenu.extradata); M_UpdateChallengeGridExtraData(challengesmenu.extradata);
M_ChallengesAutoFocus(challengesmenu.currentunlock, true);
challengesmenu.pending = true; challengesmenu.pending = true;
M_ChallengesAutoFocus(challengesmenu.currentunlock, true);
}
#endif
} }
return true; return true;
} }
#endif
else else
{ {
if (M_MenuBackPressed(pid) || start) if (M_MenuBackPressed(pid) || start)