diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp index b89347f28..47502cb0f 100644 --- a/src/acs/call-funcs.cpp +++ b/src/acs/call-funcs.cpp @@ -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; diff --git a/src/doomstat.h b/src/doomstat.h index 2e41932b5..022cd054e 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -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. diff --git a/src/g_gamedata.cpp b/src/g_gamedata.cpp index 3c8acfba4..556def12a 100644 --- a/src/g_gamedata.cpp +++ b/src/g_gamedata.cpp @@ -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; diff --git a/src/info.c b/src/info.c index b543282a6..dad41781d 100644 --- a/src/info.c +++ b/src/info.c @@ -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 diff --git a/src/info.h b/src/info.h index 19061c438..2519259e8 100644 --- a/src/info.h +++ b/src/info.h @@ -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 diff --git a/src/k_menu.h b/src/k_menu.h index 21cef5fa5..c9564c484 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1486,6 +1486,7 @@ extern struct statisticsmenu_s { INT32 gotmedals; INT32 nummedals; INT32 numextramedals; + INT32 numcanbonus; UINT32 statgridplayed[9][9]; INT32 maxscroll; UINT16 *maplist; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 12db99d55..61853ece5 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -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; diff --git a/src/m_cond.c b/src/m_cond.c index b853eea60..5659205ee 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -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; diff --git a/src/menus/extras-statistics.cpp b/src/menus/extras-statistics.cpp index efea7af67..e270b2db6 100644 --- a/src/menus/extras-statistics.cpp +++ b/src/menus/extras-statistics.cpp @@ -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; diff --git a/src/p_inter.c b/src/p_inter.c index 0f2ed7471..8c9a559a8 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -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; diff --git a/src/p_mobj.c b/src/p_mobj.c index d65d995bb..baceb1aed 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -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 diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 771ec4d3a..3ad9d6a16 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -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;