Merge remote-tracking branch 'origin/master' into lua-roulette

This commit is contained in:
Antonio Martinez 2025-07-18 20:17:56 -04:00
commit b6cee4850e
25 changed files with 1007 additions and 180 deletions

View file

@ -687,6 +687,7 @@ consvar_t cv_items[] = {
UnsavedNetVar("gardentop", "On").on_off(),
UnsavedNetVar("gachabom", "On").on_off(),
UnsavedNetVar("stoneshoe", "On").on_off(),
UnsavedNetVar("toxomister", "On").on_off(),
UnsavedNetVar("dualsneaker", "On").on_off(),
UnsavedNetVar("triplesneaker", "On").on_off(),
UnsavedNetVar("triplebanana", "On").on_off(),

View file

@ -166,7 +166,7 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
for (; i < numwadfiles; i++) //mainwads+1, otherwise we start on the first mainwad
{
// If it has only music/sound lumps, don't put it in the list
if (i > mainwads && !wadfiles[i]->important)
if (!wadfiles[i]->important)
continue;
if (firstfile)
@ -175,6 +175,8 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
continue;
}
// CONS_Printf("putting %d (%s) - mw %d\n", i, wadfiles[i]->filename, mainwads);
nameonly(strcpy(wadfilename, wadfiles[i]->filename));
// Look below at the WRITE macros to understand what these numbers mean.
@ -564,6 +566,9 @@ INT32 CL_CheckFiles(void)
#endif
for (i = 0; i < fileneedednum || j < numwadfiles;)
{
// CONS_Printf("checking %d of %d / %d of %d?\n", i, fileneedednum, j, numwadfiles);
// CONS_Printf("i: %s / j: %s \n", fileneeded[i].filename, wadfiles[j]->filename);
if (j < numwadfiles && !wadfiles[j]->important)
{
// Unimportant on our side.
@ -574,11 +579,17 @@ INT32 CL_CheckFiles(void)
// If this test is true, we've reached the end of one file list
// and the other still has a file that's important
if (i >= fileneedednum || j >= numwadfiles)
{
return 2;
}
// For the sake of speed, only bother with a md5 check
if (memcmp(wadfiles[j]->md5sum, fileneeded[i].md5sum, 16))
{
return 2;
}
// It's accounted for! let's keep going.
CONS_Debug(DBG_NETPLAY, "'%s' accounted for\n", fileneeded[i].filename);

View file

@ -199,7 +199,8 @@ Run this macro, then #undef FOREACH afterward
FOREACH (DROPTARGET, 21),\
FOREACH (GARDENTOP, 22),\
FOREACH (GACHABOM, 23),\
FOREACH (STONESHOE, 24)
FOREACH (STONESHOE, 24),\
FOREACH (TOXOMISTER, 25)
typedef enum
{
@ -1068,6 +1069,7 @@ struct player_t
mobj_t *hand;
mobj_t *flickyAttacker;
mobj_t *stoneShoe;
mobj_t *toxomisterCloud;
SINT8 pitblame; // Index of last player that hit you, resets after being in control for a bit. If you deathpit, credit the old attacker!

View file

@ -3120,6 +3120,11 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
"S_FLYBOT767",
"S_STON",
"S_TOXAA",
"S_TOXAA_DEAD",
"S_TOXAB",
"S_TOXBA",
};
// RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1",
@ -4028,6 +4033,10 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t
"MT_STONESHOE",
"MT_STONESHOE_CHAIN",
"MT_TOXOMISTER_POLE",
"MT_TOXOMISTER_EYE",
"MT_TOXOMISTER_CLOUD",
};
const char *const MOBJFLAG_LIST[] = {

View file

@ -45,7 +45,8 @@ void ScreenshotPass::capture(Rhi& rhi)
{
// Read the aligned data into unaligned linear memory, flipping the rows in the process.
uint32_t pixel_data_row = (height_ - row) - 1;
std::move(&pixel_data_[pixel_data_row * read_stride], &pixel_data_[pixel_data_row * read_stride + stride], &packed_data_[row * stride]);
uint8_t* pixel_data_row_ptr = &pixel_data_[pixel_data_row * read_stride];
std::move(pixel_data_row_ptr, pixel_data_row_ptr + stride, &packed_data_[row * stride]);
}
if (g_takemapthumbnail != TMT_NO)

View file

@ -801,6 +801,8 @@ char sprnames[NUMSPRITES + 1][5] =
"STUN",
"STON",
"TOXA",
"TOXB",
// Pulley
"HCCH",
@ -3264,7 +3266,7 @@ state_t states[NUMSTATES] =
{SPR_WAYP, 1|FF_FLOORSPRITE, 1, {NULL}, 0, 0, S_NULL}, // S_WAYPOINTSPLAT
{SPR_EGOO, 0, 1, {NULL}, 0, 0, S_NULL}, // S_EGOORB
{SPR_AMPA, FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 41, 1, S_NULL}, // S_AMPS
{SPR_AMPA, FF_FULLBRIGHT|FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 41, 1, S_NULL}, // S_AMPS
{SPR_EXPC, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_EXP
// Water Trail
@ -3704,6 +3706,11 @@ state_t states[NUMSTATES] =
{SPR_STUN, FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 4, 4, S_NULL}, // S_FLYBOT767
{SPR_STON, 0, -1, {NULL}, 0, 0, S_STON}, // S_STON
//
{SPR_TOXA, 0, -1, {NULL}, 0, 0, S_TOXAA}, // S_TOXAA
{SPR_TOXA, 0, 175, {NULL}, 0, 0, S_NULL}, // S_TOXAA_DEAD
{SPR_TOXA, 1, -1, {NULL}, 0, 0, S_TOXAB}, // S_TOXAB
{SPR_TOXB, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 6, 5, S_TOXBA}, // S_TOXBA
};
mobjinfo_t mobjinfo[NUMMOBJTYPES] =
@ -22712,6 +22719,84 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
MF_SPECIAL|MF_SCENERY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_PICKUPFROMBELOW|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_TOXOMISTER_POLE
-1, // doomednum
S_TOXAA, // spawnstate
1, // spawnhealth
S_NULL, // seestate
sfx_tossed, // seesound
8, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_TOXAA_DEAD, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
32*FRACUNIT, // radius
64*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_SHOOTABLE|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_TOXOMISTER_EYE
-1, // doomednum
S_TOXAB, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
8, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
32*FRACUNIT, // radius
64*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPTHING|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
{ // MT_TOXOMISTER_CLOUD
-1, // doomednum
S_TOXBA, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
8, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
70*FRACUNIT, // radius
70*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_SPECIAL|MF_NOGRAVITY|MF_DONTENCOREMAP, // flags
S_NULL // raisestate
},
};

View file

@ -1338,6 +1338,8 @@ typedef enum sprite
SPR_STUN,
SPR_STON,
SPR_TOXA,
SPR_TOXB,
// Pulley
SPR_HCCH,
@ -4188,6 +4190,11 @@ typedef enum state
S_STON,
S_TOXAA,
S_TOXAA_DEAD,
S_TOXAB,
S_TOXBA,
S_FIRSTFREESLOT,
S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1,
NUMSTATES
@ -5119,6 +5126,10 @@ typedef enum mobj_type
MT_STONESHOE,
MT_STONESHOE_CHAIN,
MT_TOXOMISTER_POLE,
MT_TOXOMISTER_EYE,
MT_TOXOMISTER_CLOUD,
MT_FIRSTFREESLOT,
MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1,
NUMMOBJTYPES

View file

@ -2160,3 +2160,10 @@ void K_UpdateBotGameplayVars(player_t *player)
K_UpdateBotGameplayVarsItemUsage(player);
}
boolean K_BotUnderstandsItem(kartitems_t item)
{
if (item == KITEM_BALLHOG)
return false; // Sorry. MRs welcome!
return true;
}

View file

@ -401,6 +401,7 @@ void K_BotItemUsage(const player_t *player, ticcmd_t *cmd, INT16 turnamt);
void K_BotPickItemPriority(player_t *player);
boolean K_BotUnderstandsItem(kartitems_t item);
#ifdef __cplusplus
} // extern "C"

View file

@ -172,6 +172,7 @@ static patch_t *kp_droptarget[3];
static patch_t *kp_gardentop[3];
static patch_t *kp_gachabom[3];
static patch_t *kp_stoneshoe[3];
static patch_t *kp_toxomister[3];
static patch_t *kp_bar[2];
static patch_t *kp_doublebar[2];
static patch_t *kp_triplebar[2];
@ -239,8 +240,11 @@ static patch_t *kp_team_you;
static patch_t *kp_duel_foe;
static patch_t *kp_duel_you;
static patch_t *kp_duel_sticker;
static patch_t *kp_duel_4sticker;
static patch_t *kp_duel_under;
static patch_t *kp_duel_4under;
static patch_t *kp_duel_over;
static patch_t *kp_duel_4over;
static patch_t *kp_duel_margin[24];
patch_t *kp_autoroulette;
@ -649,6 +653,7 @@ void K_LoadKartHUDGraphics(void)
HU_UpdatePatch(&kp_gardentop[0], "K_ITGTOP");
HU_UpdatePatch(&kp_gachabom[0], "K_ITGBOM");
HU_UpdatePatch(&kp_stoneshoe[0], "K_ITSTON");
HU_UpdatePatch(&kp_toxomister[0], "K_ITTOX");
HU_UpdatePatch(&kp_bar[0], "K_RBBAR");
HU_UpdatePatch(&kp_doublebar[0], "K_RBBAR2");
HU_UpdatePatch(&kp_triplebar[0], "K_RBBAR3");
@ -710,6 +715,7 @@ void K_LoadKartHUDGraphics(void)
HU_UpdatePatch(&kp_gardentop[1], "K_ISGTOP");
HU_UpdatePatch(&kp_gachabom[1], "K_ISGBOM");
HU_UpdatePatch(&kp_stoneshoe[1], "K_ISSTON");
HU_UpdatePatch(&kp_toxomister[1], "K_ISTOX");
HU_UpdatePatch(&kp_bar[1], "K_SBBAR");
HU_UpdatePatch(&kp_doublebar[1], "K_SBBAR2");
HU_UpdatePatch(&kp_triplebar[1], "K_SBBAR3");
@ -769,6 +775,7 @@ void K_LoadKartHUDGraphics(void)
HU_UpdatePatch(&kp_gardentop[2], "ISPYGTOP");
HU_UpdatePatch(&kp_gachabom[2], "ISPYGBOM");
HU_UpdatePatch(&kp_stoneshoe[2], "ISPYSTON");
HU_UpdatePatch(&kp_toxomister[2], "ISPYTOX");
// CHECK indicators
sprintf(buffer, "K_CHECKx");
@ -1083,8 +1090,11 @@ void K_LoadKartHUDGraphics(void)
HU_UpdatePatch(&kp_duel_foe, "DUEL_FOE");
HU_UpdatePatch(&kp_duel_sticker, "DUEL_S");
HU_UpdatePatch(&kp_duel_4sticker, "DUEL4_S");
HU_UpdatePatch(&kp_duel_under, "DUEL_B");
HU_UpdatePatch(&kp_duel_4under, "DUEL4_B");
HU_UpdatePatch(&kp_duel_over, "DUEL_B2");
HU_UpdatePatch(&kp_duel_4over, "DUEL4_B2");
HU_UpdatePatch(&kp_duel_you, "DUEL_YOU");
sprintf(buffer, "DUELMBxx");
@ -1190,6 +1200,7 @@ static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset)
kp_gardentop,
kp_gachabom,
kp_stoneshoe,
kp_toxomister,
};
if (item == KITEM_SAD || (item > KITEM_NONE && item < NUMKARTITEMS))
@ -3306,20 +3317,50 @@ INT32 K_GetTransFlagFromFixed(fixed_t value)
}
}
static tic_t duel_lastleveltime = 0;
static INT32 duel_marginanim = 0;
static INT32 duel_lastmargin = 0;
static INT32 youheight = 0;
// We want to draw teams and duel HUD in a player context,
// but also precisely control how often it's drawn, even if
// some players have no view.
static UINT8 K_FirstActiveDisplayPlayer(player_t *player)
{
UINT8 i;
for (i = 0; i <= r_splitscreen; i++)
{
player_t *pl = &players[displayplayers[i]];
if (!pl->spectator && !camera[i].freecam)
break;
}
if (player == &players[displayplayers[i]])
return true;
return false;
}
// MAXSPLITSCREENPLAYERS not allowed here, warning for change later
static tic_t duel_lastleveltime[4];
static INT32 duel_marginanim[4];
static INT32 duel_lastmargin[4];
static INT32 youheight[4];
static void K_drawKartDuelScores(void)
{
if (!K_InRaceDuel())
return;
if (r_splitscreen > 1 && !K_FirstActiveDisplayPlayer(stplyr))
return;
using srb2::Draw;
player_t *foe = K_DuelOpponent(stplyr);
if (stplyr == foe)
return;
boolean use4p = (r_splitscreen) ? 1 : 0;
UINT8 vn = R_GetViewNumber();
INT32 basex = 0;
INT32 basey = 48;
INT32 flags = V_SNAPTOLEFT|V_HUDTRANS|V_SLIDEIN;
@ -3342,13 +3383,66 @@ static void K_drawKartDuelScores(void)
INT32 youscorex = 16;
INT32 youscorey = 69;
Draw::Font scorefont = Draw::Font::kThinTimer;
INT32 margx = 0;
INT32 margy = 0;
boolean redraw = false; // Draw a duplicate?
boolean redrawn = false;
if (use4p)
{
basex = BASEVIDWIDTH/2 - 40;
basey = 0;
flags = V_SNAPTOTOP|V_HUDTRANS|V_SLIDEIN;
redraw = true;
if (r_splitscreen == 1)
{
redraw = false;
flags |= V_SNAPTORIGHT;
if (R_GetViewNumber() == 1)
{
flags |= V_SNAPTOBOTTOM;
flags &= ~V_SNAPTOTOP;
basey = BASEVIDHEIGHT - 40;
}
basex = BASEVIDWIDTH - 80;
}
barx = 40;
bary = 7;
barheight = 35; // MOTHERFUCK FLIPPED IN 4P
barwidth = 4; // DITTO
foex = 6;
foey = 12;
youx = 63;
youy = 12;
foescorex = foex + 6;
foescorey = foey + 12;
youscorex = youx + 6;
youscorey = youy + 12;
margx = 15;
margy = -40;
}
redraw:
Draw::Font scorefont = use4p ? Draw::Font::kZVote : Draw::Font::kThinTimer;
Draw::Align scorealign = use4p ? Draw::Align::kCenter : Draw::Align::kLeft;
UINT8 ri = 6;
INT32 youfill = skincolors[stplyr->skincolor].ramp[ri];
INT32 foefill = skincolors[foe->skincolor].ramp[ri];
V_DrawScaledPatch(basex, basey, flags, kp_duel_sticker);
if (use4p)
V_DrawScaledPatch(basex, basey, flags, kp_duel_4sticker);
else
V_DrawScaledPatch(basex, basey, flags, kp_duel_sticker);
INT32 scoredelta = stplyr->duelscore - foe->duelscore;
INT32 clutchscore = DUELWINNINGSCORE - 1; // we want the bar to be full when NEXT checkpoint wins...
@ -3371,27 +3465,43 @@ static void K_drawKartDuelScores(void)
targetyouheight = 2*barheight - savemargin;
}
if (leveltime != duel_lastleveltime)
if (leveltime != duel_lastleveltime[vn])
{
INT32 slide = std::max(1, abs(targetyouheight - youheight)/3);
if (targetyouheight > youheight)
youheight += slide;
else if (targetyouheight < youheight)
youheight -= slide;
INT32 slide = std::max(1, abs(targetyouheight - youheight[vn])/3);
if (targetyouheight > youheight[vn])
youheight[vn] += slide;
else if (targetyouheight < youheight[vn])
youheight[vn] -= slide;
}
INT32 foeheight = 2*barheight-youheight; // barheight is a single tied bar, so total height of the full gauge is 2x barheight
INT32 foeheight = 2*barheight-youheight[vn]; // barheight is a single tied bar, so total height of the full gauge is 2x barheight
V_DrawFill(basex+barx, basey+bary-barheight, barwidth, foeheight, foefill|flags);
V_DrawFill(basex+barx, basey+bary-barheight+foeheight, barwidth, youheight, youfill|flags);
if (use4p)
{
V_DrawFill(basex+barx-barheight, basey+bary, foeheight, barwidth, foefill|flags);
V_DrawFill(basex+barx-barheight+foeheight, basey+bary, youheight[vn], barwidth, youfill|flags);
}
else
{
V_DrawFill(basex+barx, basey+bary-barheight, barwidth, foeheight, foefill|flags);
V_DrawFill(basex+barx, basey+bary-barheight+foeheight, barwidth, youheight[vn], youfill|flags);
}
V_DrawScaledPatch(basex, basey, flags, kp_duel_under);
V_DrawScaledPatch(basex, basey-barheight+foeheight, flags, kp_duel_over);
V_DrawScaledPatch(basex, basey, flags, kp_duel_foe);
V_DrawScaledPatch(basex, basey, flags, kp_duel_you);
V_DrawScaledPatch(basex, basey, flags, use4p ? kp_duel_4under : kp_duel_under);
Draw foenum = Draw(basex+foescorex, basey+foescorey).flags(flags).font(scorefont).align(Draw::Align::kLeft);
Draw younum = Draw(basex+youscorex, basey+youscorey).flags(flags).font(scorefont).align(Draw::Align::kLeft);
if (use4p)
V_DrawScaledPatch(basex-barheight+foeheight, basey, flags, kp_duel_4over);
else
V_DrawScaledPatch(basex, basey-barheight+foeheight, flags, kp_duel_over);
if (!use4p)
{
V_DrawScaledPatch(basex, basey, flags, kp_duel_foe);
V_DrawScaledPatch(basex, basey, flags, kp_duel_you);
}
Draw foenum = Draw(basex+foescorex, basey+foescorey).flags(flags).font(scorefont).align(scorealign);
Draw younum = Draw(basex+youscorex, basey+youscorey).flags(flags).font(scorefont).align(scorealign);
if (abs(scoredelta) == clutchscore && ((leveltime % 2) || cv_reducevfx.value))
{
@ -3414,8 +3524,8 @@ static void K_drawKartDuelScores(void)
for (UINT8 draw = 0; draw < 2; draw++)
{
UINT8 drawme = draw ? (stplyr - players) : (foe - players);
UINT8 drawx = basex + (draw ? youx : foex);
UINT8 drawy = basey + (draw ? youy : foey);
UINT16 drawx = basex + (draw ? youx : foex);
UINT16 drawy = basey + (draw ? youy : foey);
if (!playeringame[drawme] || players[drawme].spectator)
continue;
@ -3450,7 +3560,7 @@ static void K_drawKartDuelScores(void)
else
colormap = R_GetTranslationColormap(workingskin, static_cast<skincolornum_t>(players[drawme].mo->color), GTC_CACHE);
V_DrawMappedPatch(drawx+xoff, drawy+yoff, flags|flipflag, faceprefix[workingskin][FACE_RANK], colormap);
V_DrawMappedPatch(drawx+xoff, drawy+yoff, flags|flipflag, faceprefix[workingskin][use4p ? FACE_MINIMAP : FACE_RANK], colormap);
}
// Dogshit. Should have just figured out how to do log base 5 in C++.
@ -3466,139 +3576,150 @@ static void K_drawKartDuelScores(void)
INT32 boostspersymbol = 3; // How many boosts should it take to see a new symbol?
// rawmargin = (leveltime/10)%(3*boostspersymbol);
if (duel_lastleveltime != leveltime) // Trigger the "slide" animation when rawmargin changes.
if (duel_lastleveltime[vn] != leveltime) // Trigger the "slide" animation when rawmargin changes.
{
duel_marginanim = std::min(duel_marginanim + 1, 100); // not magic just arbitrary
if (duel_lastmargin != rawmargin)
duel_marginanim[vn] = std::min(duel_marginanim[vn] + 1, 100); // not magic just arbitrary
if (duel_lastmargin[vn] != rawmargin)
{
duel_marginanim = 0;
duel_lastmargin = rawmargin;
duel_marginanim[vn] = 0;
duel_lastmargin[vn] = rawmargin;
}
}
duel_lastleveltime = leveltime;
duel_lastleveltime[vn] = leveltime;
// CONS_Printf("=== RAWMARGIN %d\n", rawmargin);
if (rawmargin == 0)
return;
rawmargin--; // Start at 0, idiot
// We're invoking the RNG to get a slightly chaotic symbol distribution,
// but we're a HUD hook, so we need to keep the results of the call consistent.
P_SetRandSeed(PR_NUISANCE, 69 + rawmargin);
INT32 highsymbol = rawmargin/boostspersymbol + 1; // Highest symbol that should appear.
INT32 symbolsperupgrade = 5; // What is each symbol worth relative to each other? Like, 5 Stars = 1 Moon, etc.
// Okay, so we would LOVE to do this in a way that isn't a big clusterfuck, like just
// doing rawmargin^3 and then subtracting powers of 5 out of that. Unfortunately, UINT64
// is too small for the values that feel intuitively right here, so we have to do some of
// the math on a limited set of symbols, then shift up. This is the concept of "symbol
// headroom" that's in use here.
//
// (Note that Puyo~n uses a super inconsistent symbol table, probably to avoid this problem,
// but we're assholes and want things to feel logically consistent I guess?
// I dunno. I sort of feel like I should have just directly used the Puyo~n garbage table and
// avoided most of this, LOL)
INT32 symbolheadroom = 5; // Maximum # symbols we can "step down".
INT32 frac = rawmargin % boostspersymbol; // Used in intermediate calculations.
INT32 minsymbol = std::max(1, highsymbol - symbolheadroom); // The lowest symbol that should appear.
INT32 symbolheadroominuse = highsymbol - minsymbol; // The # of symbols we are stepping down.
INT32 minscore = std::pow(symbolsperupgrade, symbolheadroominuse+1);
INT32 maxscore = std::pow(symbolsperupgrade, symbolheadroominuse+2) - 1;
// CONS_Printf("min %d max %d\n", minscore, maxscore);
// We show the player successive combos with the same leading symbol, but we
// waht them to feel intuitively like they're increasing each time.
// Maxscore and minscore have been mapped to the correct power-of-N, so any
// point we pick between them will lead with the correct symbol once we adjust
// for symbol headroom. Pick a point that's appropriate for how "far" into the
// current symbol we are.
fixed_t lobound = FRACUNIT * frac / boostspersymbol;
fixed_t hibound = FRACUNIT * (frac+1) / boostspersymbol;
fixed_t roll = P_RandomRange(PR_NUISANCE, lobound, hibound);
INT32 margin = Easing_Linear(roll, minscore, maxscore); // The score we're trying to draw a garbage stack for.
INT32 margindigits[5];
memset(margindigits, -1, sizeof(margindigits));
INT32 nummargindigits = 0;
// CONS_Printf("margin %d min %d max %d roll %d shiu %d ms %d\n", margin, minscore, maxscore, roll, symbolheadroominuse, minsymbol);
if (rawmargin/boostspersymbol >= (MARGINLEVELS-1))
if (rawmargin != 0)
{
// Capped out. Show 5 Chaos.
nummargindigits = 5;
for(UINT8 i = 0; i < nummargindigits; i++)
rawmargin--; // Start at 0, idiot
// We're invoking the RNG to get a slightly chaotic symbol distribution,
// but we're a HUD hook, so we need to keep the results of the call consistent.
P_SetRandSeed(PR_NUISANCE, 69 + rawmargin);
INT32 highsymbol = rawmargin/boostspersymbol + 1; // Highest symbol that should appear.
INT32 symbolsperupgrade = 5; // What is each symbol worth relative to each other? Like, 5 Stars = 1 Moon, etc.
// Okay, so we would LOVE to do this in a way that isn't a big clusterfuck, like just
// doing rawmargin^3 and then subtracting powers of 5 out of that. Unfortunately, UINT64
// is too small for the values that feel intuitively right here, so we have to do some of
// the math on a limited set of symbols, then shift up. This is the concept of "symbol
// headroom" that's in use here.
//
// (Note that Puyo~n uses a super inconsistent symbol table, probably to avoid this problem,
// but we're assholes and want things to feel logically consistent I guess?
// I dunno. I sort of feel like I should have just directly used the Puyo~n garbage table and
// avoided most of this, LOL)
INT32 symbolheadroom = 5; // Maximum # symbols we can "step down".
INT32 frac = rawmargin % boostspersymbol; // Used in intermediate calculations.
INT32 minsymbol = std::max(1, highsymbol - symbolheadroom); // The lowest symbol that should appear.
INT32 symbolheadroominuse = highsymbol - minsymbol; // The # of symbols we are stepping down.
INT32 minscore = std::pow(symbolsperupgrade, symbolheadroominuse+1);
INT32 maxscore = std::pow(symbolsperupgrade, symbolheadroominuse+2) - 1;
// CONS_Printf("min %d max %d\n", minscore, maxscore);
// We show the player successive combos with the same leading symbol, but we
// waht them to feel intuitively like they're increasing each time.
// Maxscore and minscore have been mapped to the correct power-of-N, so any
// point we pick between them will lead with the correct symbol once we adjust
// for symbol headroom. Pick a point that's appropriate for how "far" into the
// current symbol we are.
fixed_t lobound = FRACUNIT * frac / boostspersymbol;
fixed_t hibound = FRACUNIT * (frac+1) / boostspersymbol;
fixed_t roll = P_RandomRange(PR_NUISANCE, lobound, hibound);
INT32 margin = Easing_Linear(roll, minscore, maxscore); // The score we're trying to draw a garbage stack for.
INT32 margindigits[5];
memset(margindigits, -1, sizeof(margindigits));
INT32 nummargindigits = 0;
// CONS_Printf("margin %d min %d max %d roll %d shiu %d ms %d\n", margin, minscore, maxscore, roll, symbolheadroominuse, minsymbol);
if (rawmargin/boostspersymbol >= (MARGINLEVELS-1))
{
margindigits[i] = MARGINLEVELS-1;
}
}
else
{
// Subtract powers of N from our chosen score to create a decent-enough-looking
// garbage stack, then queue up the right patches to be drawn, shifting all the math
// up by "minsymbol"—remember, once maxsymbol goes above symbolheadroom, we are doing
// a low-precision version of the math that ignores low enough symbols.
while (margin > 0)
{
INT32 significant_margin = 0;
for (UINT8 i = symbolheadroominuse+1; i >= 0; i--)
// Capped out. Show 5 Chaos.
nummargindigits = 5;
for(UINT8 i = 0; i < nummargindigits; i++)
{
INT32 test = std::pow(symbolsperupgrade, i);
// CONS_Printf("testing %d (%d)\n", i, test);
if (margin >= test)
{
significant_margin = i;
break;
}
margindigits[i] = MARGINLEVELS-1;
}
}
else
{
// Subtract powers of N from our chosen score to create a decent-enough-looking
// garbage stack, then queue up the right patches to be drawn, shifting all the math
// up by "minsymbol"—remember, once maxsymbol goes above symbolheadroom, we are doing
// a low-precision version of the math that ignores low enough symbols.
while (margin > 0)
{
INT32 significant_margin = 0;
for (UINT8 i = symbolheadroominuse+1; i >= 0; i--)
{
INT32 test = std::pow(symbolsperupgrade, i);
// CONS_Printf("testing %d (%d)\n", i, test);
if (margin >= test)
{
significant_margin = i;
break;
}
}
INT32 index = significant_margin;
INT32 index = significant_margin;
margindigits[nummargindigits] = index + minsymbol - 1;
// CONS_Printf("digit %d %d\n", nummargindigits, margindigits[nummargindigits]);
margindigits[nummargindigits] = index + minsymbol - 1;
// CONS_Printf("digit %d %d\n", nummargindigits, margindigits[nummargindigits]);
nummargindigits++;
nummargindigits++;
// CONS_Printf("margin was %d ", margin);
margin -= std::pow(symbolsperupgrade, index);
// CONS_Printf("is %d\n", margin);
// CONS_Printf("margin was %d ", margin);
margin -= std::pow(symbolsperupgrade, index);
// CONS_Printf("is %d\n", margin);
if (nummargindigits >= 3 + frac)
break;
if (nummargindigits >= 3 + frac)
break;
}
}
INT32 marginspacing = std::min(6, duel_marginanim[vn]);
INT32 marginx = ((nummargindigits-1) * marginspacing)/2;
for (INT32 i = nummargindigits - 1; i >= 0; i--)
{
// CONS_Printf("draw %d - %d\n", i, margindigits[i]);
V_DrawScaledPatch(basex + margx + marginx, basey + margy, flags, kp_duel_margin[margindigits[i]]);
marginx -= marginspacing;
}
}
INT32 marginspacing = std::min(6, duel_marginanim);
INT32 marginx = ((nummargindigits-1) * marginspacing)/2;
for (INT32 i = nummargindigits - 1; i >= 0; i--)
if (redraw && !redrawn)
{
// CONS_Printf("draw %d - %d\n", i, margindigits[i]);
V_DrawScaledPatch(basex + marginx, basey, flags, kp_duel_margin[margindigits[i]]);
marginx -= marginspacing;
basey = BASEVIDHEIGHT - 40;
flags &= ~V_SNAPTOTOP;
flags |= V_SNAPTOBOTTOM;
redrawn = true;
goto redraw;
}
}
static INT32 easedallyscore = 0;
static tic_t scorechangecooldown = 0;
// MAXSPLITSCREENPLAYERS not allowed here, warning for changes later
static INT32 easedallyscore[4];
static tic_t scorechangecooldown[4];
// Mildly ugly. Don't want to export this to khud when it's so nicely handled here,
// but HUD hooks run at variable timing based on your actual framerate.
static tic_t teams_lastleveltime = 0;
static tic_t teams_lastleveltime[4];
void K_drawKartTeamScores(boolean fromintermission, INT32 interoffset)
{
if (G_GametypeHasTeams() == false)
{
return;
}
if (r_splitscreen > 1 && !K_FirstActiveDisplayPlayer(stplyr))
return;
if (TEAM__MAX != 3)
return; // "maybe someday" - the magic conch
@ -3606,7 +3727,8 @@ void K_drawKartTeamScores(boolean fromintermission, INT32 interoffset)
// I get to write HUD code from scratch, so it's going to be horribly
// verbose and obnoxious.
UINT8 use4p = (r_splitscreen > 1) ? 1 : 0;
UINT8 use4p = !!(r_splitscreen);
UINT8 vn = R_GetViewNumber();
INT32 basex = BASEVIDWIDTH/2 + 20;
INT32 basey = 0;
@ -3660,6 +3782,18 @@ void K_drawKartTeamScores(boolean fromintermission, INT32 interoffset)
facex = -2;
facey = -5;
faceoff = 4;
if (r_splitscreen == 1 && !fromintermission)
{
basex += 110;
flags |= V_SNAPTORIGHT;
if (R_GetViewNumber() == 1)
{
flags |= V_SNAPTOBOTTOM;
flags &= ~V_SNAPTOTOP;
basey = 170;
}
}
}
if (fromintermission)
@ -3723,47 +3857,47 @@ void K_drawKartTeamScores(boolean fromintermission, INT32 interoffset)
R_GetTranslationColormap(TC_RAINBOW, g_teaminfo[allies].color, GTC_CACHE) :
R_GetTranslationColormap(TC_RAINBOW, g_teaminfo[enemies].color, GTC_CACHE);
if (scorechangecooldown)
scorechangecooldown--;
if (scorechangecooldown[vn])
scorechangecooldown[vn]--;
// prevent giga flicker on team scoring
if (easedallyscore == allyscore)
if (easedallyscore[vn] == allyscore)
{
// :O
}
else
{
if (teams_lastleveltime != leveltime) // Timing consistency
if (teams_lastleveltime[vn] != leveltime) // Timing consistency
{
INT32 delta = abs(easedallyscore - allyscore); // how wrong is display score?
INT32 delta = abs(easedallyscore[vn] - allyscore); // how wrong is display score?
if (scorechangecooldown == 0 && delta)
if (scorechangecooldown[vn] == 0 && delta)
{
if (allyscore > easedallyscore)
if (allyscore > easedallyscore[vn])
{
easedallyscore++;
easedallyscore[vn]++;
if (!cv_reducevfx.value)
allycolor = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_WHITE, GTC_CACHE);
}
else
{
easedallyscore--;
easedallyscore[vn]--;
if (!cv_reducevfx.value)
enemycolor = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_WHITE, GTC_CACHE);
}
scorechangecooldown = TICRATE/delta;
scorechangecooldown[vn] = TICRATE/delta;
}
}
if (!fromintermission)
{
// replace scores with eased scores
allyscore = easedallyscore;
allyscore = easedallyscore[vn];
enemyscore = totalscore - allyscore;
}
}
teams_lastleveltime = leveltime;
teams_lastleveltime[vn] = leveltime;
fixed_t enemypercent = FixedDiv(enemyscore*FRACUNIT, totalscore*FRACUNIT);
// fixed_t allypercent = FixedDiv(allyscore*FRACUNIT, totalscore*FRACUNIT);
@ -3806,7 +3940,7 @@ void K_drawKartTeamScores(boolean fromintermission, INT32 interoffset)
// Draw at the top and bottom of the screen in 4P.
// Draw only at the bottom in intermission.
boolean shouldsecondpass = use4p;
boolean shouldsecondpass = (r_splitscreen > 1);
boolean onsecondpass = fromintermission;
draw:
@ -7746,11 +7880,8 @@ void K_drawKartHUD(void)
K_DrawKartPositionNum(stplyr->position);
}
if (R_GetViewNumber() == 0)
{
K_drawKartTeamScores(false, 0);
K_drawKartDuelScores();
}
K_drawKartTeamScores(false, 0);
K_drawKartDuelScores();
}
// This sucks, but we need to draw rings before EXP because 4P amps

View file

@ -462,7 +462,7 @@ std::optional<TargetTracking::Tooltip> object_tooltip(const mobj_t* mobj)
case MT_PLAYER:
{
if (stplyr->fastfall == 0 && K_CanSuperTransfer(stplyr))
if (mobj->player == stplyr && stplyr->fastfall == 0 && K_CanSuperTransfer(stplyr))
return Tooltip(
TextElement(
TextElement().parse("<c_animated>").font(splitfont))

View file

@ -4146,6 +4146,11 @@ fixed_t K_GetNewSpeed(const player_t *player)
p_speed = 15 * p_speed / 10;
}
if (!P_MobjWasRemoved(player->toxomisterCloud))
{
p_speed = FixedMul(p_speed, Obj_GetToxomisterCloudDrag(player->toxomisterCloud));
}
if (K_PlayerUsesBotMovement(player) == true && player->botvars.rubberband > 0)
{
// Acceleration is tied to top speed...
@ -7079,7 +7084,7 @@ mobj_t *K_ThrowKartItemEx(player_t *player, boolean missile, mobjtype_t mapthing
{
mobj_t *lasttrail = K_FindLastTrailMobj(player);
if (mapthing == MT_BUBBLESHIELDTRAP) // Drop directly on top of you.
if (mapthing == MT_BUBBLESHIELDTRAP || mapthing == MT_TOXOMISTER_POLE) // Drop directly on top of you.
{
newangle = player->mo->angle;
newx = player->mo->x + player->mo->momx;
@ -9582,6 +9587,11 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
player->pflags2 &= ~PF2_SUPERTRANSFERVFX;
}
if (K_PlayerUsesBotMovement(player) && !K_BotUnderstandsItem(player->itemtype) && player->itemamount)
{
K_DropItems(player);
}
if (player->transfer)
{
if (player->fastfall)
@ -12785,6 +12795,7 @@ static INT32 K_FlameShieldMax(player_t *player)
UINT32 distv = 1024; // Pre no-scams: 2048
distv = distv * 16 / FLAMESHIELD_MAX; // Old distv was based on a 16-segment bar
UINT32 scamradius = 1500*4; // How close is close enough that we shouldn't be allowed to scam 1st?
// UINT8 i;
disttofinish = K_GetItemRouletteDistance(player, 8);
@ -14010,23 +14021,35 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
player->instaWhipCharge = 0;
}
if ((player->cmd.buttons & BT_BAIL) && (player->cmd.buttons & BT_RESPAWNMASK) != BT_RESPAWNMASK && ((player->itemtype && player->itemamount) || (player->rings > 0) || player->superring > 0 || player->pickuprings > 0 || player->itemRoulette.active))
if (player->cmd.buttons & BT_BAIL && (player->cmd.buttons & BT_RESPAWNMASK) != BT_RESPAWNMASK)
{
boolean grounded = P_IsObjectOnGround(player->mo);
onground && player->tumbleBounces == 0 ? player->bailcharge += 2 : player->bailcharge++; // charge twice as fast on the ground
if ((P_PlayerInPain(player) && player->bailcharge == 1) || (grounded && P_PlayerInPain(player) && player->bailcharge == 2)) // this is brittle ..
if (leveltime < introtime || (gametyperules & GTR_SPHERES))
{
mobj_t *bail = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_BAILCHARGE);
S_StartSound(bail, sfx_gshb9); // I tried to use info.c, but you can't play sounds on mobjspawn via A_PlaySound
S_StartSound(bail, sfx_kc4e);
P_SetTarget(&bail->target, player->mo);
bail->renderflags |= RF_FULLBRIGHT; // set fullbright here, were gonna animate frames in the thinker and it saves us from setting FF_FULLBRIGHT every frame
// No bailing in GTR_SPHERES because I cannot be fucked to do manual Last Chance right now.
// Maybe someday!
if (!(player->oldcmd.buttons & BT_BAIL))
if (P_IsDisplayPlayer(player))
S_StartSound(player->mo, sfx_s3k7b);
player->bailcharge = 0;
}
else if ((player->itemtype && player->itemamount) || player->rings > 0 || player->superring > 0 || player->pickuprings > 0 || player->itemRoulette.active)
{
// Set up bail charge, provided we have something to bail with (any rings or item resource).
boolean grounded = P_IsObjectOnGround(player->mo);
onground && player->tumbleBounces == 0 ? player->bailcharge += 2 : player->bailcharge++; // charge twice as fast on the ground
if ((P_PlayerInPain(player) && player->bailcharge == 1) || (grounded && P_PlayerInPain(player) && player->bailcharge == 2)) // this is brittle ..
{
mobj_t *bail = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_BAILCHARGE);
S_StartSound(bail, sfx_gshb9); // I tried to use info.c, but you can't play sounds on mobjspawn via A_PlaySound
S_StartSound(bail, sfx_kc4e);
P_SetTarget(&bail->target, player->mo);
bail->renderflags |= RF_FULLBRIGHT; // set fullbright here, were gonna animate frames in the thinker and it saves us from setting FF_FULLBRIGHT every frame
}
}
else
{
player->bailcharge = 0;
}
}
else
{
player->bailcharge = 0;
}
if ((!P_PlayerInPain(player) && player->bailcharge >= 5) || player->bailcharge >= BAIL_MAXCHARGE)
@ -14039,7 +14062,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
UINT32 debtrings = 20;
if (player->rings < 0)
{
debtrings -= player->rings;
debtrings += player->rings;
player->rings = 0;
}
@ -14951,8 +14974,15 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
S_StartSound(player->mo, sfx_gsha7);
P_Thrust(player->mo, K_MomentumAngle(player->mo), 25*player->mo->scale);
P_Thrust(player->mo, player->mo->angle, 25*player->mo->scale);
if (P_IsObjectOnGround(player->mo)) // facing angle blends w/ momentum angle for game-feel
{
P_Thrust(player->mo, player->mo->angle, 25*player->mo->scale);
P_Thrust(player->mo, K_MomentumAngle(player->mo), 25*player->mo->scale);
}
else // air version is momentum angle only, reduces cheese, is twice as strong to compensate
{
P_Thrust(player->mo, K_MomentumAngle(player->mo), 50*player->mo->scale);
}
UINT8 numsparks = 8;
for (UINT8 i = 0; i < numsparks; i++)
@ -15059,6 +15089,21 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
player->botvars.itemconfirm = 0;
}
break;
case KITEM_TOXOMISTER:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO)
{
K_SetItemOut(player); // need this to set itemscale
mobj_t *pole = K_ThrowKartItem(player, false, MT_TOXOMISTER_POLE, -1, 0, 0);
Obj_InitToxomisterPole(pole);
K_UnsetItemOut(player);
player->itemamount--;
K_PlayAttackTaunt(player->mo);
player->botvars.itemconfirm = 0;
}
break;
case KITEM_SAD:
if (ATTACK_IS_DOWN && !HOLDING_ITEM && NO_HYUDORO
&& !player->sadtimer)
@ -16353,6 +16398,7 @@ boolean K_IsPickMeUpItem(mobjtype_t type)
case MT_SSMINE:
case MT_SSMINE_SHIELD:
case MT_FLOATINGITEM: // Stone Shoe
case MT_TOXOMISTER_POLE:
return true;
default:
return false;
@ -16415,6 +16461,9 @@ static boolean K_PickUp(player_t *player, mobj_t *picked)
else
type = KITEM_SAD;
break;
case MT_TOXOMISTER_POLE:
type = KITEM_TOXOMISTER;
break;
default:
type = KITEM_SAD;
break;

View file

@ -46,10 +46,10 @@ Make sure this matches the actual number of states
#define BAIL_MAXCHARGE (84) // tics to bail when in painstate nad in air, on ground is half, if you touch this, also update Obj_BailChargeThink synced animation logic
#define BAIL_DROP (FRACUNIT)
#define BAIL_BOOST (FRACUNIT)
#define BAIL_BOOST (6*FRACUNIT/5)
#define BAIL_CREDIT_DEBTRINGS (true)
#define BAIL_DROPFREQUENCY (2)
#define BAILSTUN (TICRATE*10)
#define BAILSTUN (TICRATE*7)
#define MAXCOMBOTHRUST (mapobjectscale*20)
#define MAXCOMBOFLOAT (mapobjectscale*10)

View file

@ -475,6 +475,16 @@ boolean Obj_TickStoneShoeChain(mobj_t *chain);
player_t *Obj_StoneShoeOwnerPlayer(mobj_t *shoe);
void Obj_CollideStoneShoe(mobj_t *mover, mobj_t *mobj);
/* Toxomister */
void Obj_InitToxomisterPole(mobj_t *pole);
boolean Obj_TickToxomisterPole(mobj_t *pole);
boolean Obj_TickToxomisterEye(mobj_t *eye);
boolean Obj_TickToxomisterCloud(mobj_t *cloud);
boolean Obj_ToxomisterPoleCollide(mobj_t *pole, mobj_t *toucher);
boolean Obj_ToxomisterCloudCollide(mobj_t *cloud, mobj_t *toucher);
fixed_t Obj_GetToxomisterCloudDrag(mobj_t *cloud);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -71,13 +71,14 @@ void M_MPRoomSelectInit(INT32 choice)
if (modifiedgame)
{
M_StartMessage("Server Browser & Add-Ons", M_GetText("You have add-ons loaded.\nYou won't be able to join netgames!\n\nTo play online, restart the game\nand don't load any addons.\n\n\"Dr. Robotnik's Ring Racers\" will\nautomatically add everything\nyou need when you join.\n"), NULL, MM_NOTHING, NULL, NULL);
return;
}
// The following behaviour is affected by modifiedgame despite the above restriction.
// It's a sanity check were that to be removed, wheither by us or by a modified client.
// "wheither"? That typo rules, I'm keeping that ~toast 280823
// thanks toaster - Tyron 2025-07-02
mpmenu.room = (modifiedgame == true) ? 1 : 0;
mpmenu.ticker = 0;
mpmenu.servernum = 0;

View file

@ -67,6 +67,7 @@ target_sources(SRB2SDL2 PRIVATE
stone-shoe.cpp
exp.c
bail.c
toxomister.cpp
)
add_subdirectory(versus)

View file

@ -325,10 +325,10 @@ kill_monitor_part (mobj_t *part)
static inline UINT32
restore_item_rng (UINT32 seed)
{
const UINT32 oldseed = P_GetRandSeed(PR_ITEM_ROULETTE);
const UINT32 oldseed = P_GetRandSeed(PR_ITEM_SPAWNER);
P_SetRandSeedNet(PR_ITEM_ROULETTE,
P_GetInitSeed(PR_ITEM_ROULETTE), seed);
P_SetRandSeedNet(PR_ITEM_SPAWNER,
P_GetInitSeed(PR_ITEM_SPAWNER), seed);
return oldseed;
}
@ -478,7 +478,7 @@ Obj_MonitorSpawnParts (mobj_t *monitor)
P_SetScale(monitor, (monitor->destscale *= 2));
monitor_itemcount(monitor) = 0;
monitor_rngseed(monitor) = P_GetRandSeed(PR_ITEM_ROULETTE);
monitor_rngseed(monitor) = P_GetRandSeed(PR_ITEM_SPAWNER);
monitor_spawntic(monitor) = leveltime;
monitor_emerald(monitor) = 0;

View file

@ -230,7 +230,7 @@ private:
if (P_IsObjectOnGround(this))
{
momz = 32 * mapobjectscale;
momz = flip(32 * mapobjectscale);
bouncing(true);
voice(sfx_s3k5f);
P_StartQuakeFromMobj(5, 40 * scale(), 512 * scale(), this);
@ -271,6 +271,11 @@ private:
follow()->player->stonedrag = dist > minDist();
sprzoff(30 * scale());
if (is_flipped() != follow()->is_flipped())
{
K_FlipFromObject(this, follow());
}
}
void move_chain()
@ -292,6 +297,7 @@ private:
while (Mobj::valid(node))
{
node->move_origin({p, pz});
K_FlipFromObject(node, this);
node->sprzoff(sprzoff());
// Let chain flicker like shoe does

439
src/objects/toxomister.cpp Normal file
View file

@ -0,0 +1,439 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by James Robert Roman
// Copyright (C) 2025 by Kart Krew
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include <algorithm>
#include <array>
#include "objects.hpp"
#include "../core/static_vec.hpp"
#include "../d_player.h"
#include "../doomdef.h"
#include "../doomtype.h"
#include "../g_game.h"
#include "../k_hud.h" // transflag
#include "../m_easing.h"
#include "../m_fixed.h"
#include "../m_random.h"
#include "../r_main.h"
#include "../tables.h"
using namespace srb2::objects;
namespace
{
Fixed distance3d(const Mobj* a, const Mobj* b)
{
return FixedHypot(FixedHypot(a->x - b->x, a->y - b->y), a->z - b->z);
}
Vec2<Fixed> angle_vector(angle_t x)
{
return Vec2<Fixed> {FCOS(x), FSIN(x)};
}
// copied from objects/hyudoro.c
static void
sine_bob
( mobj_t * hyu,
INT32 height,
angle_t a,
fixed_t sineofs)
{
hyu->sprzoff = FixedMul(height * hyu->scale,
sineofs + FINESINE(a >> ANGLETOFINESHIFT)) * P_MobjFlip(hyu);
}
static void
bob_in_place
( mobj_t * hyu,
INT32 height,
INT32 bob_speed)
{
sine_bob(hyu,
height,
(leveltime & (bob_speed - 1)) *
(ANGLE_MAX / bob_speed), -(3*FRACUNIT/4));
}
struct Eye;
struct Pole;
struct Cloud;
struct Eye : Mobj
{
static constexpr INT32 kOrbitRadius = 24;
bool valid() const { return Mobj::valid(owner()) && owner()->health > 0; }
bool tick()
{
if (!valid())
{
remove();
return false;
}
return true;
}
};
struct Pole : Mobj
{
static constexpr sfxenum_t kSound = sfx_s3kdal;
void extravalue1() = delete;
tic_t last_touch0() const { return mobj_t::extravalue1; }
void last_touch0(tic_t n) { mobj_t::extravalue1 = n; }
void extravalue2() = delete;
bool clouds_spawned() const { return mobj_t::extravalue2; }
void clouds_spawned(bool n) { mobj_t::extravalue2 = n; }
void reactiontime() = delete;
tic_t sound_started() const { return mobj_t::reactiontime; }
void sound_started(tic_t n) { mobj_t::reactiontime = n; }
void tracer() = delete;
Eye* eye() const { return Mobj::tracer<Eye>(); }
void eye(Eye* n) { Mobj::tracer(n); }
bool valid() const
{
if (!Mobj::valid(eye()))
return false;
return true;
}
void init()
{
Eye* p_eye = spawn_from<Eye>(MT_TOXOMISTER_EYE);
p_eye->owner(this);
p_eye->spriteyoffset(96*FRACUNIT);
last_touch0(leveltime);
clouds_spawned(false);
eye(p_eye);
flags |= MF_SPECIAL;
}
void spawn_clouds_in_orbit();
bool tick()
{
if (!valid())
{
remove();
return false;
}
if (P_IsObjectOnGround(this))
{
if (!clouds_spawned())
{
spawn_clouds_in_orbit();
clouds_spawned(true);
voice(sfx_s3k9e);
}
if (!voice_playing(kSound))
{
voice(kSound);
sound_started(leveltime);
}
if ((leveltime - sound_started()) % 256 == 0)
voice(kSound);
}
else
{
P_SpawnGhostMobj(this);
}
tick_eye();
return true;
}
void tick_eye()
{
Mobj::PosArg p = {pos2d(), z};
p.x += momx;
p.y += momy;
p.z += momz;
Mobj* targ = find_nearest_eyeball_target();
if (targ)
{
INT32 angle_to_targ = angle_to2d(targ);
Vec2<Fixed> v = angle_vector(angle_to_targ) * Fixed {Eye::kOrbitRadius * mapobjectscale};
p.x += v.x;
p.y += v.y;
eye()->angle = angle_to_targ;
}
eye()->move_origin(p);
}
angle_t angle_to2d(Mobj* mobj) const
{
return R_PointToAngle2(x, y, mobj->x, mobj->y);
}
Mobj* find_nearest_eyeball_target() const
{
srb2::StaticVec<Mobj*, MAXPLAYERS> targets;
for (INT32 i = 0; i < MAXPLAYERS; ++i)
{
if (!playeringame[i])
continue;
if (!players[i].mo)
continue;
targets.push_back(static_cast<Mobj*>(players[i].mo));
}
if (targets.empty())
return nullptr;
return *std::min_element(
targets.begin(),
targets.end(),
[this](Mobj* a, Mobj* b) { return distance3d(this, a) < distance3d(this, b); }
);
}
bool touch(Mobj* toucher)
{
if (touch_cooldown(toucher, 0))
return false;
if (K_TryPickMeUp(this, toucher, false))
return false;
// Adapted from P_XYMovement, MT_JAWZ
voice(info->deathsound);
P_KillMobj(this, NULL, NULL, DMG_NORMAL);
P_SetObjectMomZ(this, 24*FRACUNIT, false);
instathrust(R_PointToAngle2(toucher->x, toucher->y, x, y), 32 * mapobjectscale);
flags &= ~MF_NOGRAVITY;
hitlag(toucher, toucher, 8, true);
return false;
}
bool touch_cooldown
( Mobj* toucher,
UINT8 k)
{
tic_t cooldown = leveltime - last_touch0();
if (toucher == target() && cooldown < 10)
{
last_touch0(leveltime);
return true;
}
return false;
}
};
struct Cloud : Mobj
{
static constexpr INT32 kMaxFuse = 5*TICRATE;
void hnext() = delete;
Mobj* follow() const { return Mobj::hnext<Mobj>(); }
void follow(Mobj* n) { Mobj::hnext(n); }
void tracer() = delete;
Pole* pole() const { return Mobj::tracer<Pole>(); }
void pole(Pole* n) { Mobj::tracer(n); }
Fixed fuse_frac() const { return FRACUNIT - fuse * FRACUNIT / kMaxFuse; }
Fixed drag_var() const { return Easing_Linear(fuse_frac(), FRACUNIT/3, FRACUNIT); }
bool tick()
{
if (Mobj::valid(follow()))
return tick_follow();
return tick_patrol();
}
bool tick_follow()
{
if (!Mobj::valid(follow()))
{
remove();
return false;
}
move_origin(follow()->pos());
momx = 0;
momy = 0;
momz = 0;
bob_in_place(this, 8, 64);
voice_loop(sfx_s3kcfl);
if (leveltime % (TICRATE/3) == 0 && follow()->player->rings > -20) // toxomister ring drain
{
follow()->player->rings--;
S_StartSound(follow()->player->mo, sfx_antiri);
}
if (fuse < 3*TICRATE && leveltime % (1 + fuse / TICRATE) == 0)
{
renderflags ^= RF_DONTDRAW;
}
if (fuse < kMaxFuse && (kMaxFuse - fuse) % 20 == 0 && Mobj::valid(target()) && target()->player && follow()->player)
{
K_SpawnAmps(target()->player, K_PvPAmpReward(3, target()->player, follow()->player), this);
}
follow()->player->stunned = fuse; // stunned as long as cloud is here
return true;
}
bool tick_patrol()
{
if (Mobj::valid(pole()) && pole()->health > 0)
{
move_origin(pole()->pos());
instathrust(angle, 64 * mapobjectscale);
}
else
{
if (!fuse)
{
fuse = 3*TICRATE;
instathrust(angle, 2 * mapobjectscale);
}
if (leveltime & 1)
{
renderflags ^= RF_DONTDRAW;
}
}
return true;
}
bool touch(Mobj* toucher)
{
if (toucher == target())
return false;
if (toucher->player)
{
if (this == toucher->player->toxomisterCloud) // already attached
return true;
if (!P_MobjWasRemoved(toucher->player->toxomisterCloud))
{
toucher->player->pflags |= PF_CASTSHADOW;
return true;
}
P_SetTarget(&toucher->player->toxomisterCloud, this);
}
toucher->hitlag(8);
scale_to(destscale);
follow(toucher);
fuse = kMaxFuse;
renderflags &= ~RF_DONTDRAW;
voice(sfx_s3k8a);
return true;
}
};
void Pole::spawn_clouds_in_orbit()
{
constexpr INT32 kNumClouds = 6;
std::array<UINT32, kNumClouds> weights;
std::array<INT32, kNumClouds> order;
angle_t a = 0;
angle_t a_incr = ANGLE_MAX / kNumClouds;
for (INT32 i = 0; i < kNumClouds; ++i)
{
weights[i] = P_Random(PR_TRACKHAZARD);
order[i] = i;
}
std::stable_sort(order.begin(), order.end(), [&](INT32 a, INT32 b) { return weights[a] < weights[b]; });
for (INT32 i : order)
{
Cloud* cloud = spawn_from<Cloud>({}, MT_TOXOMISTER_CLOUD);
cloud->pole(this);
cloud->angle = a;
cloud->target(target());
cloud->spriteyoffset(24*FRACUNIT);
cloud->hitlag(2 + i * 4);
cloud->scale_between(1, cloud->scale(), cloud->scale() / 5);
a += a_incr;
}
}
}; // namespace
void Obj_InitToxomisterPole(mobj_t *pole)
{
static_cast<Pole*>(pole)->init();
}
boolean Obj_TickToxomisterPole(mobj_t *pole)
{
return static_cast<Pole*>(pole)->tick();
}
boolean Obj_TickToxomisterEye(mobj_t *eye)
{
return static_cast<Eye*>(eye)->tick();
}
boolean Obj_TickToxomisterCloud(mobj_t *cloud)
{
return static_cast<Cloud*>(cloud)->tick();
}
boolean Obj_ToxomisterPoleCollide(mobj_t *pole, mobj_t *toucher)
{
return static_cast<Pole*>(pole)->touch(static_cast<Mobj*>(toucher));
}
boolean Obj_ToxomisterCloudCollide(mobj_t *cloud, mobj_t *toucher)
{
return static_cast<Cloud*>(cloud)->touch(static_cast<Mobj*>(toucher));
}
fixed_t Obj_GetToxomisterCloudDrag(mobj_t *cloud)
{
return static_cast<Cloud*>(cloud)->drag_var();
}

View file

@ -1129,6 +1129,14 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
Obj_CollideStoneShoe(toucher, special);
return;
case MT_TOXOMISTER_POLE:
Obj_ToxomisterPoleCollide(special, toucher);
return;
case MT_TOXOMISTER_CLOUD:
Obj_ToxomisterCloudCollide(special, toucher);
return;
default: // SOC or script pickup
P_SetTarget(&special->target, toucher);
break;

View file

@ -1025,6 +1025,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
|| g_tm.thing->type == MT_MONITOR
|| g_tm.thing->type == MT_BATTLECAPSULE
|| g_tm.thing->type == MT_KART_LEFTOVER
|| g_tm.thing->type == MT_TOXOMISTER_POLE
|| (g_tm.thing->type == MT_PLAYER)))
{
// see if it went over / under
@ -1043,6 +1044,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
|| thing->type == MT_MONITOR
|| thing->type == MT_BATTLECAPSULE
|| thing->type == MT_KART_LEFTOVER
|| thing->type == MT_TOXOMISTER_POLE
|| (thing->type == MT_PLAYER)))
{
// see if it went over / under

View file

@ -1095,7 +1095,7 @@ fixed_t P_GetMobjGravity(mobj_t *mo)
wasflip = (mo->eflags & MFE_VERTICALFLIP) != 0;
if (mo->type != MT_SPINFIRE)
if (mo->type != MT_SPINFIRE && mo->type != MT_STONESHOE)
mo->eflags &= ~MFE_VERTICALFLIP;
if (mo->subsector->sector->ffloors) // Check for 3D floor gravity too.
@ -1248,6 +1248,10 @@ fixed_t P_GetMobjGravity(mobj_t *mo)
case MT_GACHABOM:
gravityadd = (5*gravityadd)/2;
break;
case MT_TOXOMISTER_POLE:
if (mo->health > 0)
gravityadd = (5*gravityadd)/2;
break;
case MT_BANANA:
case MT_BALLHOG:
case MT_BALLHOG_RETICULE_TEST:
@ -1303,7 +1307,7 @@ fixed_t P_GetMobjGravity(mobj_t *mo)
gravityadd *= 2;
break;
case MT_STONESHOE:
gravityadd *= 4;
gravityadd = -4 * abs(gravityadd) * P_MobjFlip(mo);
break;
default:
break;
@ -2337,6 +2341,7 @@ boolean P_ZMovement(mobj_t *mo)
case MT_BIGTUMBLEWEED:
case MT_LITTLETUMBLEWEED:
case MT_EMERALD:
case MT_TOXOMISTER_POLE:
if (!(mo->flags & MF_NOCLIPHEIGHT) && P_CheckDeathPitCollide(mo))
{
P_RemoveMobj(mo);
@ -5320,6 +5325,7 @@ boolean P_IsKartItem(INT32 type)
case MT_HYUDORO:
case MT_SINK:
case MT_GACHABOM:
case MT_TOXOMISTER_POLE:
return true;
default:
@ -5346,6 +5352,7 @@ boolean P_IsKartFieldItem(INT32 type)
case MT_DROPTARGET:
case MT_DUELBOMB:
case MT_GACHABOM:
case MT_TOXOMISTER_POLE:
return true;
default:
@ -5379,6 +5386,8 @@ boolean P_IsRelinkItem(INT32 type)
case MT_HYUDORO_CENTER:
case MT_SINK:
case MT_GACHABOM:
case MT_TOXOMISTER_POLE:
case MT_FLOATINGITEM: // Stone Shoe Trap
return true;
default:
@ -6863,6 +6872,12 @@ static boolean P_MobjDeadThink(mobj_t *mobj)
P_SetMobjState(mobj, mobj->info->xdeathstate);
/* FALLTHRU */
case MT_JAWZ_SHIELD:
mobj->renderflags ^= RF_DONTDRAW;
break;
case MT_TOXOMISTER_POLE:
if (mobj->momz == 0 && P_IsObjectOnGround(mobj))
P_SetMobjState(mobj, mobj->info->xdeathstate);
mobj->renderflags ^= RF_DONTDRAW;
break;
case MT_SSMINE:
@ -10295,6 +10310,15 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
case MT_STONESHOE:
return Obj_TickStoneShoe(mobj);
case MT_TOXOMISTER_POLE:
return Obj_TickToxomisterPole(mobj);
case MT_TOXOMISTER_EYE:
return Obj_TickToxomisterEye(mobj);
case MT_TOXOMISTER_CLOUD:
return Obj_TickToxomisterCloud(mobj);
default:
// check mobj against possible water content, before movement code
P_MobjCheckWater(mobj);
@ -11159,6 +11183,9 @@ static void P_DefaultMobjShadowScale(mobj_t *thing)
case MT_STONESHOE_CHAIN:
thing->shadowscale = FRACUNIT/5;
break;
case MT_TOXOMISTER_POLE:
thing->shadowscale = FRACUNIT;
break;
default:
if (thing->flags & (MF_ENEMY|MF_BOSS))
thing->shadowscale = FRACUNIT;
@ -12978,12 +13005,12 @@ static boolean P_AllowMobjSpawn(mapthing_t* mthing, mobjtype_t i)
// spawn all the duel mode objects itself, which ends up
// calling this function again.
// So that's why this check is even here.
if (inDuel == false && (grandprixinfo.gp == false || grandprixinfo.eventmode != GPEVENT_BONUS))
if (inDuel == false && (grandprixinfo.gp == false || grandprixinfo.eventmode != GPEVENT_BONUS) && gametype != GT_TUTORIAL)
{
if (K_IsDuelItem(i) == true
&& K_DuelItemAlwaysSpawns(mthing) == false)
{
// Only spawns in Duels or GP bonus rounds.
// Only spawns in Duels, GP bonus rounds or Tutorials.
return false;
}
}
@ -15487,6 +15514,7 @@ boolean P_MobjCanChangeFlip(mobj_t *mobj)
case MT_SHRINK_CHAIN:
case MT_SHRINK_LASER:
case MT_SHRINK_PARTICLE:
case MT_STONESHOE:
return false;
default:

View file

@ -93,6 +93,7 @@ typedef enum
BALLHOGRETICULE = 0x8000,
STONESHOE = 0x10000,
FLYBOT = 0x20000,
TOXOMISTERCLOUD = 0x40000,
} player_saveflags;
static inline void P_ArchivePlayer(savebuffer_t *save)
@ -368,6 +369,9 @@ static void P_NetArchivePlayers(savebuffer_t *save)
if (players[i].stoneShoe)
flags |= STONESHOE;
if (players[i].toxomisterCloud)
flags |= TOXOMISTERCLOUD;
if (players[i].flybot)
flags |= FLYBOT;
@ -421,6 +425,9 @@ static void P_NetArchivePlayers(savebuffer_t *save)
if (flags & STONESHOE)
WRITEUINT32(save->p, players[i].stoneShoe->mobjnum);
if (flags & TOXOMISTERCLOUD)
WRITEUINT32(save->p, players[i].toxomisterCloud->mobjnum);
if (flags & FLYBOT)
WRITEUINT32(save->p, players[i].flybot->mobjnum);
@ -1082,6 +1089,9 @@ static void P_NetUnArchivePlayers(savebuffer_t *save)
if (flags & STONESHOE)
players[i].stoneShoe = (mobj_t *)(size_t)READUINT32(save->p);
if (flags & TOXOMISTERCLOUD)
players[i].toxomisterCloud = (mobj_t *)(size_t)READUINT32(save->p);
if (flags & FLYBOT)
players[i].flybot = (mobj_t *)(size_t)READUINT32(save->p);
@ -6247,6 +6257,11 @@ static void P_RelinkPointers(void)
if (!RelinkMobj(&players[i].stoneShoe))
CONS_Debug(DBG_GAMELOGIC, "stoneShoe not found on player %d\n", i);
}
if (players[i].toxomisterCloud)
{
if (!RelinkMobj(&players[i].toxomisterCloud))
CONS_Debug(DBG_GAMELOGIC, "toxomisterCloud not found on player %d\n", i);
}
if (players[i].flybot)
{
if (!RelinkMobj(&players[i].flybot))

View file

@ -8434,6 +8434,14 @@ void P_FreeLevelState(void)
HWR_ClearAllTextures();
#endif
if (rendermode == render_soft)
{
// Queued draws might reference patches or colormaps about to be freed.
// Flush 2D to make sure no read-after-free occurs.
srb2::rhi::Rhi* rhi = srb2::sys::get_rhi(srb2::sys::g_current_rhi);
srb2::sys::main_hardware_state()->twodee_renderer->flush(*rhi, srb2::g_2d);
}
G_FreeGhosts(); // ghosts are allocated with PU_LEVEL
Patch_FreeTag(PU_PATCH_LOWPRIORITY);
Patch_FreeTag(PU_PATCH_ROTATED);

View file

@ -4259,6 +4259,7 @@ void P_PlayerThink(player_t *player)
PlayerPointerErase(player->ballhogreticule);
PlayerPointerErase(player->flickyAttacker);
PlayerPointerErase(player->stoneShoe);
PlayerPointerErase(player->toxomisterCloud);
PlayerPointerErase(player->powerup.flickyController);
PlayerPointerErase(player->powerup.barrier);