diff --git a/src/cvars.cpp b/src/cvars.cpp index 2b30a208d..c74f42254 100644 --- a/src/cvars.cpp +++ b/src/cvars.cpp @@ -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(), diff --git a/src/d_player.h b/src/d_player.h index d440ecbfe..8552ddf26 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -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 { @@ -1066,6 +1067,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! diff --git a/src/deh_tables.c b/src/deh_tables.c index a880a1766..acc6be553 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -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[] = { diff --git a/src/info.c b/src/info.c index 2e6e1ac72..a1760a822 100644 --- a/src/info.c +++ b/src/info.c @@ -801,6 +801,8 @@ char sprnames[NUMSPRITES + 1][5] = "STUN", "STON", + "TOXA", + "TOXB", // Pulley "HCCH", @@ -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 + }, }; diff --git a/src/info.h b/src/info.h index 996b721e2..ddfe07012 100644 --- a/src/info.h +++ b/src/info.h @@ -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 diff --git a/src/k_bot.cpp b/src/k_bot.cpp index 1d827138a..5855e52a8 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -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; +} diff --git a/src/k_bot.h b/src/k_bot.h index e58e4e8be..66d015ac7 100644 --- a/src/k_bot.h +++ b/src/k_bot.h @@ -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" diff --git a/src/k_hud.cpp b/src/k_hud.cpp index 5bfc23d9f..f20d884c3 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -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(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 diff --git a/src/k_kart.c b/src/k_kart.c index 6e395a97f..d88128a40 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -4143,6 +4143,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... @@ -7076,7 +7081,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; @@ -9579,6 +9584,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) @@ -14049,7 +14059,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; } @@ -15069,6 +15079,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) @@ -16363,6 +16388,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; @@ -16425,6 +16451,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; diff --git a/src/k_kart.h b/src/k_kart.h index 7c644e0ad..aff3b7596 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -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) diff --git a/src/k_objects.h b/src/k_objects.h index c894c0f18..d29dc74cb 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -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 diff --git a/src/k_roulette.c b/src/k_roulette.c index 0ec6a8a71..88eaec7d4 100644 --- a/src/k_roulette.c +++ b/src/k_roulette.c @@ -96,14 +96,15 @@ static UINT32 K_DynamicItemOddsRace[NUMKARTRESULTS-1][2] = {1, 1}, // lightningshield {25, 4}, // bubbleshield {66, 9}, // flameshield - {1, 3}, // hyudoro + {1, 2}, // hyudoro {0, 0}, // pogospring {30, 8}, // superring (SPECIAL! distance value specifies when this can NO LONGER appear) {0, 0}, // kitchensink - {1, 3}, // droptarget + {1, 2}, // droptarget {43, 5}, // gardentop {0, 0}, // gachabom - {1, 3}, // stoneshoe + {1, 2}, // stoneshoe + {1, 2}, // toxomister {45, 6}, // dualsneaker {55, 8}, // triplesneaker {25, 2}, // triplebanana @@ -140,6 +141,7 @@ static UINT32 K_DynamicItemOddsBattle[NUMKARTRESULTS-1][2] = {0, 0}, // gardentop {10, 5}, // gachabom {0, 0}, // stoneshoe + {0, 0}, // toxomister {0, 0}, // dualsneaker {20, 1}, // triplesneaker {0, 0}, // triplebanana @@ -176,6 +178,7 @@ static UINT32 K_DynamicItemOddsSpecial[NUMKARTRESULTS-1][2] = {0, 0}, // gardentop {0, 0}, // gachabom {0, 0}, // stoneshoe + {0, 0}, // toxomister {35, 2}, // dualsneaker {0, 0}, // triplesneaker {0, 0}, // triplebanana @@ -212,6 +215,7 @@ static UINT8 K_KartLegacyBattleOdds[NUMKARTRESULTS-1][2] = { 0, 0 }, // Garden Top { 5, 0 }, // Gachabom { 0, 1 }, // Stone Shoe + { 0, 1 }, // Toxomister { 0, 0 }, // Sneaker x2 { 0, 1 }, // Sneaker x3 { 0, 0 }, // Banana x3 @@ -373,6 +377,7 @@ botItemPriority_e K_GetBotItemPriority(kartitems_t result) case KITEM_EGGMAN: case KITEM_GACHABOM: case KITEM_STONESHOE: + case KITEM_TOXOMISTER: case KITEM_KITCHENSINK: { // Used when in 1st place and relatively far from players. @@ -924,6 +929,9 @@ static void K_PushToRouletteItemList(itemroulette_t *const roulette, INT32 item) --------------------------------------------------*/ static void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item) { + if (player && K_PlayerUsesBotMovement(player) && !K_BotUnderstandsItem(item)) + return; + K_PushToRouletteItemList(roulette, item); if (player == NULL) @@ -1052,6 +1060,7 @@ static boolean K_IsItemFirstOnly(kartitems_t item) case KITEM_HYUDORO: case KITEM_DROPTARGET: case KITEM_STONESHOE: + case KITEM_TOXOMISTER: return true; default: return false; @@ -1229,6 +1238,17 @@ static boolean K_TimingPermitsItem(kartitems_t item, const itemroulette_t *roule return true; } +static void K_FixEmptyRoulette(const player_t *player, itemroulette_t *const roulette) +{ + if (roulette->itemListLen > 0) + return; + + if (K_PlayerUsesBotMovement(player)) // Bots can't use certain items. Give them _something_. + K_PushToRouletteItemList(roulette, KITEM_SUPERRING); + else // Players can use all items, so this should never happen. + K_PushToRouletteItemList(roulette, KITEM_SAD); +} + /*-------------------------------------------------- void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun) @@ -1373,6 +1393,7 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet if (K_ForcedSPB(player, roulette) == true) { K_AddItemToReel(player, roulette, KITEM_SPB); + K_FixEmptyRoulette(player, roulette); return; } @@ -1397,6 +1418,7 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet // singleItem = KITEM_SAD by default, // so it will be used when all items are turned off. K_AddItemToReel(player, roulette, singleItem); + K_FixEmptyRoulette(player, roulette); return; } @@ -1752,6 +1774,8 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet totalSpawnChance--; } + + K_FixEmptyRoulette(player, roulette); } /*-------------------------------------------------- diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 7a9cb347e..bb5205738 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -67,6 +67,7 @@ target_sources(SRB2SDL2 PRIVATE stone-shoe.cpp exp.c bail.c + toxomister.cpp ) add_subdirectory(versus) diff --git a/src/objects/toxomister.cpp b/src/objects/toxomister.cpp new file mode 100644 index 000000000..1741cf7f0 --- /dev/null +++ b/src/objects/toxomister.cpp @@ -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 +#include + +#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 angle_vector(angle_t x) +{ + return Vec2 {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(); } + 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(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 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 targets; + + for (INT32 i = 0; i < MAXPLAYERS; ++i) + { + if (!playeringame[i]) + continue; + + if (!players[i].mo) + continue; + + targets.push_back(static_cast(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(); } + void follow(Mobj* n) { Mobj::hnext(n); } + + void tracer() = delete; + Pole* pole() const { return Mobj::tracer(); } + 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 weights; + std::array 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({}, 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)->init(); +} + +boolean Obj_TickToxomisterPole(mobj_t *pole) +{ + return static_cast(pole)->tick(); +} + +boolean Obj_TickToxomisterEye(mobj_t *eye) +{ + return static_cast(eye)->tick(); +} + +boolean Obj_TickToxomisterCloud(mobj_t *cloud) +{ + return static_cast(cloud)->tick(); +} + +boolean Obj_ToxomisterPoleCollide(mobj_t *pole, mobj_t *toucher) +{ + return static_cast(pole)->touch(static_cast(toucher)); +} + +boolean Obj_ToxomisterCloudCollide(mobj_t *cloud, mobj_t *toucher) +{ + return static_cast(cloud)->touch(static_cast(toucher)); +} + +fixed_t Obj_GetToxomisterCloudDrag(mobj_t *cloud) +{ + return static_cast(cloud)->drag_var(); +} diff --git a/src/p_inter.c b/src/p_inter.c index c8ba97f2b..8b3e6e25a 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -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; diff --git a/src/p_map.c b/src/p_map.c index 76f3d80bc..414296f2c 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -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 diff --git a/src/p_mobj.c b/src/p_mobj.c index 71228ab27..105fac2da 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -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: @@ -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,7 @@ 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; @@ -6864,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: @@ -10296,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); @@ -11160,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; diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 2351cc64b..41463eead 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -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)) diff --git a/src/p_user.c b/src/p_user.c index 5e0635bbf..bf8baf3df 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -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);