Add bonus pickups in conditions you couldn't grab Spray Cans before

- If you've gotten every Spray Can, or you're on a custom course...
- Only one of these spawns per map
- Correctly save and load these
- Statistics menu counts base-game bonuses
- If there are gaps in the list, or new Spray Cans are added later, these base-game bonuses are converted into the new Spray Cans
- New graphics required so far:
    - SBONA0 to SBONP0 - 16-frame prerendered circling sprite animation
    - GOTBON - 8x8 representation of the SBON object
This commit is contained in:
toaster 2025-02-25 19:05:41 +00:00
parent a6bf7f46a7
commit ecb4ffeeca
12 changed files with 165 additions and 45 deletions

View file

@ -1915,6 +1915,7 @@ bool CallFunc_GetGrabbedSprayCan(ACSVM::Thread *thread, const ACSVM::Word *argV,
// See also P_SprayCanInit
UINT16 can_id = mapheaderinfo[gamemap-1]->records.spraycan;
// Intentionally not affected by MCAN_BONUS
if (can_id < gamedata->numspraycans)
{
UINT16 col = gamedata->spraycans[can_id].col;

View file

@ -156,6 +156,9 @@ struct skinreference_t
#define MV_MYSTICMELODY (1<<4)
#define MV_MAX (MV_VISITED|MV_BEATEN|MV_ENCORE|MV_SPBATTACK|MV_MYSTICMELODY)
#define MCAN_INVALID (UINT16_MAX)
#define MCAN_BONUS (UINT16_MAX-1)
struct recordtimes_t
{
tic_t time; ///< Time in which the level was finished.

View file

@ -607,7 +607,7 @@ void srb2::load_ng_gamedata()
for (auto& mappair : js.maps)
{
UINT16 mapnum = G_MapNumber(mappair.first.c_str());
uint16_t mapnum = G_MapNumber(mappair.first.c_str());
recorddata_t dummyrecord {};
dummyrecord.mapvisited |= mappair.second.visited.visited ? MV_VISITED : 0;
dummyrecord.mapvisited |= mappair.second.visited.beaten ? MV_BEATEN : 0;
@ -630,24 +630,35 @@ void srb2::load_ng_gamedata()
dummyrecord.spraycan = (minorversion >= GD_MINIMUM_SPRAYCANSV2)
? mappair.second.spraycan
: UINT16_MAX;
: MCAN_INVALID;
if (mapnum < nummapheaders && mapheaderinfo[mapnum])
{
// Valid mapheader, time to populate with record data.
// Infill Spray Can info
if (dummyrecord.spraycan < tempcans.size())
if (
dummyrecord.spraycan < tempcans.size()
&& (mapnum < basenummapheaders)
&& (tempcans[dummyrecord.spraycan].map >= basenummapheaders)
)
{
// Assign map ID.
tempcans[dummyrecord.spraycan].map = mapnum;
}
dummyrecord.spraycan = UINT16_MAX; // We repopulate this later.
if (dummyrecord.spraycan != MCAN_INVALID)
{
// Yes, even if it's valid. We reassign later.
dummyrecord.spraycan = MCAN_BONUS;
}
mapheaderinfo[mapnum]->records = dummyrecord;
}
else if (dummyrecord.mapvisited & MV_BEATEN
|| dummyrecord.timeattack.time != 0 || dummyrecord.timeattack.lap != 0
|| dummyrecord.spbattack.time != 0 || dummyrecord.spbattack.lap != 0)
|| dummyrecord.spbattack.time != 0 || dummyrecord.spbattack.lap != 0
|| dummyrecord.spraycan != MCAN_INVALID)
{
// Invalid, but we don't want to lose all the juicy statistics.
// Instead, update a FILO linked list of "unloaded mapheaders".
@ -666,8 +677,11 @@ void srb2::load_ng_gamedata()
unloadedmap->next = unloadedmapheaders;
unloadedmapheaders = unloadedmap;
// Invalidate can.
dummyrecord.spraycan = UINT16_MAX;
if (dummyrecord.spraycan != MCAN_INVALID)
{
// Invalidate non-bonus spraycans.
dummyrecord.spraycan = MCAN_BONUS;
}
// Finally, copy into.
unloadedmap->records = dummyrecord;
@ -744,7 +758,7 @@ void srb2::load_ng_gamedata()
skincolors[tempcan.col].cache_spraycan = i;
if (tempcan.map < nummapheaders)
if (tempcan.map < basenummapheaders)
mapheaderinfo[tempcan.map]->records.spraycan = i;
gamedata->spraycans[i] = tempcan;

View file

@ -50,6 +50,7 @@ char sprnames[NUMSPRITES + 1][5] =
"BSPH", // Sphere
"EMBM",
"SPCN", // Spray Can
"SBON", // Spray Can replacement bonus
"MMSH", // Ancient Shrine
"MORB", // One Morbillion
"EMRC", // Chaos Emeralds

View file

@ -589,6 +589,7 @@ typedef enum sprite
SPR_BSPH, // Sphere
SPR_EMBM,
SPR_SPCN, // Spray Can
SPR_SBON, // Spray Can replacement bonus
SPR_MMSH, // Ancient Shrine
SPR_MORB, // One Morbillion
SPR_EMRC, // Chaos Emeralds

View file

@ -1486,6 +1486,7 @@ extern struct statisticsmenu_s {
INT32 gotmedals;
INT32 nummedals;
INT32 numextramedals;
INT32 numcanbonus;
UINT32 statgridplayed[9][9];
INT32 maxscroll;
UINT16 *maplist;

View file

@ -3315,7 +3315,7 @@ void M_DrawCupSelect(void)
incj = false;
work_array[j].medal = NULL;
work_array[j].col = work_array[j].dotcol = UINT16_MAX;
work_array[j].col = work_array[j].dotcol = MCAN_INVALID;
if (templevelsearch.timeattack)
{
@ -3354,11 +3354,15 @@ void M_DrawCupSelect(void)
emblem = M_GetLevelEmblems(-1);
}
}
else if ((gamedata->gotspraycans > 0) && (mapheaderinfo[map]->typeoflevel & TOL_RACE))
else if (mapheaderinfo[map]->typeoflevel & TOL_RACE)
{
incj = true;
if (mapheaderinfo[map]->records.spraycan < gamedata->numspraycans)
if (mapheaderinfo[map]->records.spraycan == MCAN_BONUS)
{
work_array[j].col = MCAN_BONUS;
}
else if (mapheaderinfo[map]->records.spraycan < gamedata->numspraycans)
{
work_array[j].col = gamedata->spraycans[mapheaderinfo[map]->records.spraycan].col;
}
@ -3409,7 +3413,13 @@ void M_DrawCupSelect(void)
}
else
{
if (work_array[i].col < numskincolors)
if (work_array[i].col == MCAN_BONUS)
{
// Bonus in place of Spray Can
V_DrawScaledPatch(x, y, 0, W_CachePatchName("GOTBON", PU_CACHE));
}
else if (work_array[i].col < numskincolors)
{
// Spray Can
@ -8324,7 +8334,14 @@ static INT32 M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y, boolean allowtime,
if (hasmedals)
x -= 4;
if (mapheaderinfo[mapnum]->records.spraycan < gamedata->numspraycans)
if (mapheaderinfo[mapnum]->records.spraycan == MCAN_BONUS)
{
if (draw)
V_DrawScaledPatch(x, y, 0, W_CachePatchName("GOTBON", PU_CACHE));
x -= 8;
}
else if (mapheaderinfo[mapnum]->records.spraycan < gamedata->numspraycans)
{
UINT16 col = gamedata->spraycans[mapheaderinfo[mapnum]->records.spraycan].col;
@ -8374,11 +8391,18 @@ static void M_DrawStatsMaps(void)
if (gamedata->numspraycans)
{
medalspos = 30 + V_ThinStringWidth(medalcountstr, 0);
medalcountstr = va("x %d/%d", gamedata->gotspraycans, gamedata->numspraycans);
medalcountstr = va("x %d/%d", gamedata->gotspraycans + statisticsmenu.numcanbonus, gamedata->numspraycans);
V_DrawThinString(20 + medalspos, 60, 0, medalcountstr);
V_DrawMappedPatch(10 + medalspos, 60, 0, W_CachePatchName("GOTCAN", PU_CACHE),
R_GetTranslationColormap(TC_DEFAULT, gamedata->spraycans[0].col, GTC_MENUCACHE));
}
else if (statisticsmenu.numcanbonus)
{
medalspos = 30 + V_ThinStringWidth(medalcountstr, 0);
medalcountstr = va("x %d", statisticsmenu.numcanbonus);
V_DrawThinString(20 + medalspos, 60, 0, medalcountstr);
V_DrawScaledPatch(10 + medalspos, 60, 0, W_CachePatchName("GOTBON", PU_CACHE));
}
medalspos = BASEVIDWIDTH - 20;

View file

@ -743,7 +743,7 @@ void M_ClearSecrets(void)
continue;
mapheaderinfo[i]->records.mapvisited = 0;
mapheaderinfo[i]->records.spraycan = UINT16_MAX;
mapheaderinfo[i]->records.spraycan = MCAN_INVALID;
mapheaderinfo[i]->cache_maplock = MAXUNLOCKABLES;
@ -813,6 +813,30 @@ static void M_AssignSpraycans(void)
conditionset_t *c;
condition_t *cn;
UINT16 bonustocanmap = 0;
// First, turn outstanding bonuses into existing uncollected Spray Cans.
while (gamedata->gotspraycans < gamedata->numspraycans)
{
while (bonustocanmap < basenummapheaders)
{
if (mapheaderinfo[bonustocanmap]->records.spraycan != MCAN_BONUS)
{
bonustocanmap++;
continue;
}
break;
}
if (bonustocanmap == basenummapheaders)
break;
mapheaderinfo[bonustocanmap]->records.spraycan = gamedata->gotspraycans;
gamedata->spraycans[gamedata->gotspraycans].map = bonustocanmap;
gamedata->gotspraycans++;
}
const UINT16 prependoffset = MAXSKINCOLORS-1;
// None of the following accounts for cans being removed, only added...
@ -828,7 +852,7 @@ static void M_AssignSpraycans(void)
if (cn->type != UC_SPRAYCAN)
continue;
// G_LoadGamedata, G_SaveGameData doesn't support custom skincolors right now.
// This will likely never support custom skincolors.
if (cn->requirement >= SKINCOLOR_FIRSTFREESLOT) //numskincolors)
continue;
@ -890,7 +914,24 @@ static void M_AssignSpraycans(void)
for (i = 0; i < listlen; i++)
{
gamedata->spraycans[gamedata->numspraycans].map = NEXTMAP_INVALID;
// Convert bonus pickups into Spray Cans if new ones have been added.
while (bonustocanmap < basenummapheaders)
{
if (mapheaderinfo[bonustocanmap]->records.spraycan != MCAN_BONUS)
{
bonustocanmap++;
continue;
}
gamedata->gotspraycans++;
mapheaderinfo[bonustocanmap]->records.spraycan = gamedata->numspraycans;
break;
}
gamedata->spraycans[gamedata->numspraycans].map = (
(bonustocanmap == basenummapheaders)
? NEXTMAP_INVALID
: bonustocanmap
);
gamedata->spraycans[gamedata->numspraycans].col = tempcanlist[i];
skincolors[tempcanlist[i]].cache_spraycan = gamedata->numspraycans;

View file

@ -290,10 +290,22 @@ void M_Statistics(INT32 choice)
{
(void)choice;
UINT16 i;
statisticsmenu.gotmedals = M_CountMedals(false, false);
statisticsmenu.nummedals = M_CountMedals(true, false);
statisticsmenu.numextramedals = M_CountMedals(true, true);
statisticsmenu.numcanbonus = 0;
for (i = 0; i < basenummapheaders; i++)
{
if (!mapheaderinfo[i])
continue;
if (mapheaderinfo[i]->records.spraycan != MCAN_BONUS)
continue;
statisticsmenu.numcanbonus++;
}
M_StatisticsPageInit();
MISC_StatisticsDef.prevMenu = currentMenu;

View file

@ -781,13 +781,25 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
// See also P_SprayCanInit
UINT16 can_id = mapheaderinfo[gamemap-1]->records.spraycan;
if (can_id < gamedata->numspraycans)
if (can_id < gamedata->numspraycans || can_id == MCAN_BONUS)
{
// Assigned to this level, has been grabbed
return;
}
// Prevent footguns - these won't persist when custom levels are unloaded
else if (gamemap-1 < basenummapheaders)
if (
(gamemap-1 >= basenummapheaders)
|| (gamedata->gotspraycans >= gamedata->numspraycans)
)
{
// Custom course OR we ran out of assignables.
if (special->threshold != 0)
return;
can_id = MCAN_BONUS;
}
else
{
// Unassigned, get the next grabbable colour
can_id = gamedata->gotspraycans;
@ -810,18 +822,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
skincolors[swapcol].cache_spraycan = can_id;
}
}
if (can_id >= gamedata->numspraycans)
{
// We've exhausted all the spraycans to grab.
return;
}
if (gamedata->spraycans[can_id].map >= nummapheaders)
{
gamedata->spraycans[can_id].map = gamemap-1;
mapheaderinfo[gamemap-1]->records.spraycan = can_id;
if (gamedata->gotspraycans == 0
&& gametype == GT_TUTORIAL
@ -837,12 +838,14 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
}
gamedata->gotspraycans++;
if (!M_UpdateUnlockablesAndExtraEmblems(true, true))
S_StartSound(NULL, sfx_ncitem);
gamedata->deferredsave = true;
}
mapheaderinfo[gamemap-1]->records.spraycan = can_id;
if (!M_UpdateUnlockablesAndExtraEmblems(true, true))
S_StartSound(NULL, sfx_ncitem);
gamedata->deferredsave = true;
{
mobj_t *canmo = NULL;
mobj_t *next = NULL;

View file

@ -12917,7 +12917,7 @@ void P_SprayCanInit(mobj_t* mobj)
// See also P_TouchSpecialThing
UINT16 can_id = mapheaderinfo[gamemap-1]->records.spraycan;
if (can_id < gamedata->numspraycans)
if (can_id < gamedata->numspraycans || can_id == MCAN_BONUS)
{
// Assigned to this level, has been grabbed
mobj->renderflags = (tr_trans50 << RF_TRANSSHIFT);
@ -12925,19 +12925,38 @@ void P_SprayCanInit(mobj_t* mobj)
// Prevent footguns - these won't persist when custom levels are unloaded
else if (gamemap-1 < basenummapheaders)
{
// Unassigned, get the next grabbable colour (offset by threshold)
can_id = gamedata->gotspraycans;
if (gamedata->gotspraycans >= gamedata->numspraycans)
{
can_id = MCAN_BONUS;
}
else
{
// Unassigned, get the next grabbable colour (offset by threshold)
can_id = gamedata->gotspraycans;
// It's ok if this goes over gamedata->numspraycans, as they're
// capped below in this func... but NEVER let this go backwards!!
if (mobj->threshold != 0)
can_id += (mobj->threshold & UINT8_MAX);
// It's ok if this goes over gamedata->numspraycans, as they're
// capped below in this func... but NEVER let this go backwards!!
if (mobj->threshold != 0)
can_id += (mobj->threshold & UINT8_MAX);
}
mobj->renderflags = 0;
}
if (can_id < gamedata->numspraycans)
else
{
// Custom course, bonus only
can_id = MCAN_BONUS;
}
if (can_id == MCAN_BONUS && mobj->threshold == 0)
{
// Only one bonus possible
// We modify sprite instead of state for netsync reasons
mobj->sprite = SPR_SBON;
}
else if (can_id < gamedata->numspraycans)
{
mobj->sprite = mobj->state->sprite;
mobj->color = gamedata->spraycans[can_id].col;
}
else

View file

@ -498,7 +498,7 @@ static void P_ClearSingleMapHeaderInfo(INT16 num)
#endif
memset(&mapheaderinfo[num]->records, 0, sizeof(recorddata_t));
mapheaderinfo[num]->records.spraycan = UINT16_MAX;
mapheaderinfo[num]->records.spraycan = MCAN_INVALID;
mapheaderinfo[num]->justPlayed = 0;
mapheaderinfo[num]->anger = 0;