// BLANKART //----------------------------------------------------------------------------- // Copyright (C) 2018-2025 by Kart Krew. // Copyright (C) 2025 by "Anonimus". // Copyright (C) 2025 Blankart Team. // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file k_odds.cpp /// \brief Kart item odds systems. // SRB2kart Roulette Code - Position Based #include "doomdef.h" #include "doomstat.h" #include "d_netcmd.h" #include "d_player.h" #include "g_game.h" #include "info.h" #include "m_easing.h" // Invincibility gradienting #include "m_fixed.h" #include "m_random.h" #include "p_local.h" #include "p_mobj.h" #include "p_setup.h" #include "s_sound.h" #include "typedef.h" #include "k_battle.h" #include "k_boss.h" #include "k_bot.h" #include "k_kart.h" #include "k_waypoint.h" #include "k_cluster.hpp" #include "k_odds.h" consvar_t *KartItemCVars[NUMKARTRESULTS-1] = { &cv_sneaker, &cv_rocketsneaker, &cv_invincibility, &cv_banana, &cv_eggmanmonitor, &cv_orbinaut, &cv_jawz, &cv_mine, &cv_ballhog, &cv_selfpropelledbomb, &cv_grow, &cv_shrink, &cv_thundershield, &cv_hyudoro, &cv_pogospring, &cv_kitchensink, &cv_superring, &cv_landmine, &cv_bubbleshield, &cv_flameshield, &cv_dualsneaker, &cv_triplesneaker, &cv_triplebanana, &cv_decabanana, &cv_tripleorbinaut, &cv_quadorbinaut, &cv_dualjawz }; #define NUMKARTODDS (MAXODDS*10) // Base multiplication to ALL item odds to simulate fractional precision. // Reduced from 4 to 1 due to 75-count probability. #define BASEODDSMUL 1 // Multiplication to odds for Battle item odds specifically. #define BATTLEODDSMUL 4 // Maximum probability. #define REALMAXPROB 75 #define MAXPROBABILITY (REALMAXPROB * BASEODDSMUL) #define REALMAXBATTLEPROB 10 #define MAXBATTLEPROBABILITY (REALMAXBATTLEPROB * BATTLEODDSMUL) // Less ugly 2D arrays // Expanded 16-tier useodds, for more flexible calculations. // Item odds are now based around the max number being 75 as well. static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][MAXODDS] = { //0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //B C D E F G H I J K L M N O P Q { 0, 0, 0, 0, 9, 18, 28, 8, 0, 0, 0, 0, 0, 0, 0, 0}, // Sneaker { 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 7, 15, 42, 48, 37, 37}, // Rocket Sneaker { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 33, 60, 75, 75}, // Invincibility {67, 52, 24, 22, 15, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana {18, 15, 12, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Eggman Monitor {15, 30, 25, 12, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Orbinaut { 0, 11, 22, 9, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Jawz { 0, 0, 10, 9, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Mine { 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Ballhog { 0, 0, 2, 3, 4, 5, 6, 9, 10, 12, 8, 5, 2, 2, 2, 0}, // Self-Propelled Bomb { 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 18, 9, 0, 0, 0}, // Grow { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 7, 3, 0, 0}, // Shrink {18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Thunder Shield { 0, 0, 0, 0, 3, 5, 7, 9, 5, 0, 0, 0, 0, 0, 0, 0}, // Hyudoro { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Pogo Spring { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Kitchen Sink { 7, 11, 15, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Super Ring {22, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Land Mine { 0, 3, 18, 32, 20, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Bubble Shield { 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 18, 36, 37, 18, 7, 0}, // Flame Shield { 0, 0, 0, 0, 0, 3, 5, 25, 20, 15, 9, 0, 0, 0, 0, 0}, // Sneaker x2 { 0, 0, 0, 0, 0, 0, 0, 0, 4, 7, 21, 45, 32, 7, 0, 0}, // Sneaker x3 { 0, 7, 7, 7, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana x3 { 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana x10 { 0, 0, 0, 2, 5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Orbinaut x3 { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Orbinaut x4 { 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // Jawz x2 }; static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS][2] = { //R S { 2, 1 }, // Sneaker { 0, 0 }, // Rocket Sneaker { 4, 1 }, // Invincibility { 0, 0 }, // Banana { 1, 0 }, // Eggman Monitor { 8, 0 }, // Orbinaut { 8, 1 }, // Jawz { 6, 1 }, // Mine { 2, 1 }, // Ballhog { 0, 0 }, // Self-Propelled Bomb { 2, 1 }, // Grow { 0, 0 }, // Shrink { 4, 0 }, // Thunder Shield { 2, 0 }, // Hyudoro { 3, 0 }, // Pogo Spring { 0, 0 }, // Kitchen Sink { 0, 0 }, // Super Ring { 2, 0 }, // Land Mine { 1, 0 }, // Bubble Shield { 1, 0 }, // Flame Shield { 0, 0 }, // Sneaker x2 { 0, 1 }, // Sneaker x3 { 0, 0 }, // Banana x3 { 1, 1 }, // Banana x10 { 2, 0 }, // Orbinaut x3 { 1, 1 }, // Orbinaut x4 { 5, 1 } // Jawz x2 }; // Cooldown time table; contains both base (index 0) and current (index 1) // times. Base times are in seconds, current times are in tics. tic_t ItemBGone[NUMKARTRESULTS][2] = { //BASE CURR { 0, 0 }, // Null/Sad Face { 0, 0 }, // Sneaker { 0, 0 }, // Rocket Sneaker { 0, 0 }, // Invincibility { 0, 0 }, // Banana { 10, 0 }, // Eggman Monitor { 0, 0 }, // Orbinaut { 5, 0 }, // Jawz { 0, 0 }, // Mine { 10, 0 }, // Ballhog { 20, 0 }, // Self-Propelled Bomb { 3, 0 }, // Grow { 20, 0 }, // Shrink { 0, 0 }, // Thunder Shield { 20, 0 }, // Hyudoro { 0, 0 }, // Pogo Spring { 0, 0 }, // Kitchen Sink { 0, 0 }, // Super Ring { 0, 0 }, // Land Mine { 5, 0 }, // Bubble Shield { 8, 0 }, // Flame Shield { 0, 0 }, // Sneaker x2 { 0, 0 }, // Sneaker x3 { 0, 0 }, // Banana x3 { 30, 0 }, // Banana x10 { 10, 0 }, // Orbinaut x3 { 20, 0 }, // Orbinaut x4 { 10, 0 } // Jawz x2 }; // TODO: Vectorize all item tables and shove them into gamemode-uniqe pools (k_oddstable.cpp?). // Basically necessary for any hopes of easy SOC support in the near future. /** \brief Sets the cooldown timer for an item. When active, the item is removed * from all players' item pools for some time. \param item (SINT8) item to assign cooldown for \param time (tic_t) cooldown time \return void */ void K_SetBGone(SINT8 item, tic_t time) { ItemBGone[item][GONER_CURRCOOLDOWN] = time; } /** \brief Sets the cooldown timer for an item to its "base" (intended) time. \param item (SINT8) item to assign base cooldown for \return void */ void K_SetBGoneToBase(SINT8 item) { ItemBGone[item][GONER_CURRCOOLDOWN] = (ItemBGone[item][GONER_BASECOOLDOWN] * TICRATE); } /** \brief Gets the cooldown timer for an item. \param item (SINT8) item to retrieve cooldown for \param base (bool) if true, returns the "base" (intended) cooldown instead \return (tic_t) cooldown time */ tic_t K_GetBGone(SINT8 item, boolean base) { return ItemBGone[item][base ? GONER_BASECOOLDOWN : GONER_CURRCOOLDOWN] * (base ? TICRATE : 1); } // Magic number distance for use with item roulette tiers #define ACTIVEDISTVAR (K_UsingLegacyCheckpoints() ? DISTVAR_LEGACY : DISTVAR) #define SPBSTARTDIST (SPBDISTVAR) // Distance when SPB can start appearing #define SPBFORCEDIST (7 * SPBDISTVAR / 2) // Distance when SPB is forced onto 2nd place (3.5x SPBDISTVAR) #define ENDDIST (12*ACTIVEDISTVAR) // Distance when the game stops giving you bananas // 1/21/2025: I hate tiptoeing around the integer limit. // This is at a smaller scale. static UINT32 K_Dist2D(INT32 x1, INT32 y1, INT32 x2, INT32 y2) { // d = √((x2 - x1)² + (y2 - y1)²) INT32 xdiff, ydiff; INT64 xprod, yprod; xdiff = (x2 - x1); ydiff = (y2 - y1); xprod = ((INT64)xdiff * (INT64)xdiff); yprod = ((INT64)ydiff * (INT64)ydiff); return (UINT32)(IntSqrt64(xprod + yprod)); } // Basic integer distancing, to quote myself: // "Even if you did 256 units for 1 fracunit in distancing, it'd be a better result than trying to // deal with overflows on a system that's already being pushed to the limit by needing 65536 units // for precision. No seriously, I don't think anyone's losing sleep over "hmmmmm, 0.0000152 or // 0.0039?????" when most 2D game engines only give a fuck about MAYBE 0.001" /*static UINT32 K_IntDistance(fixed_t curx, fixed_t cury, fixed_t curz, fixed_t destx, fixed_t desty, fixed_t destz) { return K_Dist2D(0, curz / FRACUNIT, K_Dist2D(curx / FRACUNIT, cury / FRACUNIT, destx / FRACUNIT, desty / FRACUNIT), destz / FRACUNIT); }*/ // This one uses map scaling instead, use in case of loss of depth on mobjscaled maps. static UINT32 K_IntDistanceForMap(fixed_t curx, fixed_t cury, fixed_t curz, fixed_t destx, fixed_t desty, fixed_t destz) { return K_Dist2D(0, curz / mapobjectscale, K_Dist2D(curx / mapobjectscale, cury / mapobjectscale, destx / mapobjectscale, desty / mapobjectscale), destz / mapobjectscale); } SINT8 K_ItemResultToType(SINT8 getitem) { if (getitem <= 0 || getitem >= NUMKARTRESULTS) // Sad (Fallback) { if (getitem != 0) { CONS_Printf("ERROR: K_GetItemResultToItemType - Item roulette gave bad item (%d) :(\n", getitem); } return KITEM_SAD; } if (getitem >= NUMKARTITEMS) { switch (getitem) { case KRITEM_DUALSNEAKER: case KRITEM_TRIPLESNEAKER: return KITEM_SNEAKER; case KRITEM_TRIPLEBANANA: case KRITEM_TENFOLDBANANA: return KITEM_BANANA; case KRITEM_TRIPLEORBINAUT: case KRITEM_QUADORBINAUT: return KITEM_ORBINAUT; case KRITEM_DUALJAWZ: return KITEM_JAWZ; default: I_Error("K_ItemResultToType: Bad item redirect for result %d\n", getitem); break; } } return getitem; } UINT8 K_ItemResultToAmount(SINT8 getitem) { switch (getitem) { case KRITEM_DUALSNEAKER: case KRITEM_DUALJAWZ: return 2; case KRITEM_TRIPLESNEAKER: case KRITEM_TRIPLEBANANA: case KRITEM_TRIPLEORBINAUT: return 3; case KRITEM_QUADORBINAUT: return 4; case KRITEM_TENFOLDBANANA: return 10; default: return 1; } } /** \brief Item Roulette for Kart \param player player \param getitem what item we're looking for \return void */ static void K_KartGetItemResult(player_t *player, SINT8 getitem) { if (getitem == KITEM_SPB || getitem == KITEM_SHRINK) // Indirect items indirectitemcooldown = 20*TICRATE; if (K_GetBGone(getitem, true) > 0) // Item cooldowns { K_SetBGoneToBase(getitem); } K_BotResetItemConfirm(player, true); player->itemtype = K_ItemResultToType(getitem); UINT8 itemamount = K_ItemResultToAmount(getitem); if (cv_kartdebugitem.value != KITEM_NONE && cv_kartdebugitem.value == player->itemtype && cv_kartdebugamount.value > 1) itemamount = cv_kartdebugamount.value; player->itemamount = itemamount; } static fixed_t K_ItemOddsScale(UINT8 numPlayers, boolean spbrush) { // CEP: due to how baseplayer works, 17P+ lobbies will STILL have the disastrous odds of 0.22 prior, if not WORSE // let's try adding another condition const UINT8 basePlayer = 16; // The player count we design most of the game around. const UINT8 vanillaMax = 17; // CEP: Maximum players in "vanilla" (non-30P) clients. const UINT8 extPlayer = 24; // CEP: Cap for 17P+ so that odds don't get too muddled. UINT8 playerCount = (spbrush ? 2 : numPlayers); fixed_t playerScaling = 0; // Then, it multiplies it further if the player count isn't equal to basePlayer. // This is done to make low player count races more interesting and high player count rates more fair. // (If you're in SPB mode and in 2nd place, it acts like it's a 1v1, so the catch-up game is not weakened.) if (playerCount < basePlayer) { // Less than basePlayer: increase odds significantly. // 2P: x1.4 playerScaling = (basePlayer - playerCount) * (FRACUNIT / 10); } else if (playerCount > basePlayer) { // More than basePlayer: reduce odds slightly. // CEP: 17P+ adjustments if (playerCount < vanillaMax) { // Less than vanillaMax: Use standard calculations. // 16P: x0.6 playerScaling = (basePlayer - playerCount) * (FRACUNIT / 20); } else if (playerCount > vanillaMax) { // More than vanillaMax: Increase odds to fit with the increased playercount // 24P: x0.6 // 30P: x0.45 playerScaling = (basePlayer - min(extPlayer, playerCount)) * (FRACUNIT / 40); // adding a cap here to be sure } } return playerScaling; } UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers, boolean spbrush) { if (mapobjectscale != FRACUNIT) { // Bring back to normal scale. distance = FixedDiv(distance * FRACUNIT, mapobjectscale) / FRACUNIT; } if (franticitems == true) { // Frantic items pretends everyone's farther apart, for crazier items. distance = (15 * distance) / 14; } if (numPlayers > 0) { // Items get crazier with the fewer players that you have. distance = FixedMul( distance * FRACUNIT, FRACUNIT + (K_ItemOddsScale(numPlayers, spbrush) / 2) ) / FRACUNIT; } return distance; } #define INVODDS 30 // Prevent integer overflows; don't let this go past 16383 #define INVINDESPERATION 4 #define MAXINVODDS ((MAXPROBABILITY * 2) * INVINDESPERATION) // Odds value for Alt. Invin. to force itself on trailing players. #define INVFORCEODDS (MAXINVODDS / 2) static INT32 K_KartGetInvincibilityOdds(UINT32 dist) { UINT32 invindist = INVINDIST/2; if (dist < invindist) return 0; INT32 finodds = 0; fixed_t fac = (min(32000, (fixed_t)dist) * FRACUNIT) / (INVINDIST); if (fac > FRACUNIT) { // Desperation! Climb exponentially until Invincibility is practically guaranteed. fac = (((min(32000, (fixed_t)dist) * FRACUNIT) / (INVINDIST)) - FRACUNIT) >> 1; finodds = Easing_InCubic(fac, INVODDS, MAXINVODDS); } else { if (fac <= FRACHALF) { // Invincibility is practically useless at lower distances. // At below half, remove it from the item pool. return 0; } // Basic linear climb to "reasonable" odds. finodds = FixedMul(INVODDS, fac); } return min(MAXINVODDS, finodds); } // Assigns general cooldowns to shield items. void K_KartHandleShieldCooldown(SINT8 item) { INT32 i; UINT8 pingame = 0, pexiting = 0; INT32 shieldtype = KSHIELD_NONE; shieldtype = K_GetShieldFromItem(item); if (shieldtype == KSHIELD_NONE) { // Not a shield! return; } for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; if (!(gametyperules & GTR_BUMPERS) || players[i].bumper) pingame++; if (players[i].exiting) pexiting++; if (((shieldtype == K_GetShieldFromItem(players[i].itemtype)) || (shieldtype == K_GetShieldFromPlayer(&players[i])))) { // If this shield has a cooldown, force-apply the cooldown preeemptively for // the entire time the shield is being held by a player. if (K_GetBGone(item, true) > 0) { K_SetBGoneToBase(item); break; } } } } /** \brief Item Roulette for Kart \param player player object passed from P_KartPlayerThink \return void */ INT32 K_KartGetItemOdds( UINT8 pos, SINT8 item, UINT32 ourDist, UINT32 clusterDist, fixed_t mashed, boolean spbrush, boolean bot, boolean rival, boolean inBottom) { INT32 newodds; INT32 i; UINT8 pingame = 0, pexiting = 0; SINT8 first = -1, second = -1; UINT32 firstDist = UINT32_MAX; UINT32 secondToFirst = UINT32_MAX; boolean powerItem = false; boolean cooldownOnStart = false; boolean indirectItem = false; boolean notNearEnd = false; boolean notForBottom = false; INT32 shieldtype = KSHIELD_NONE; I_Assert(item > KITEM_NONE); // too many off by one scenarioes. I_Assert(KartItemCVars[NUMKARTRESULTS-2] != NULL); // Make sure this exists if (!KartItemCVars[item-1]->value && !modeattacking) return 0; /* if (bot) { // TODO: Item use on bots should all be passed-in functions. // Instead of manually inserting these, it should return 0 // for any items without an item use function supplied switch (item) { case KITEM_SNEAKER: break; default: return 0; } } */ (void)bot; INT32 oddsmul = BASEODDSMUL; if (gametyperules & GTR_BATTLEODDS) { I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table newodds = K_KartItemOddsBattle[item-1][pos]; oddsmul = BATTLEODDSMUL; } else if (gametyperules & GTR_RACEODDS) { I_Assert(pos < MAXODDS); // Ditto newodds = K_KartItemOddsRace[item-1][pos]; } else { newodds = 0; } // Blow up the odds with a multiplier. newodds *= oddsmul; shieldtype = K_GetShieldFromItem(item); for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; if (!(gametyperules & GTR_BUMPERS) || players[i].bumper) pingame++; if (players[i].exiting) pexiting++; if (shieldtype != KSHIELD_NONE && ((shieldtype == K_GetShieldFromItem(players[i].itemtype)) || (shieldtype == K_GetShieldFromPlayer(&players[i])))) { // Don't allow more than one of each shield type at a time return 0; } if (players[i].mo && gametype == GT_RACE) { if (players[i].position == 1 && first == -1) first = i; if (players[i].position == 2 && second == -1) second = i; } } if (first != -1 && second != -1) // calculate 2nd's distance from 1st, for SPB { if (!K_UsingLegacyCheckpoints()) { firstDist = players[first].distancetofinish; if (mapobjectscale != FRACUNIT) { firstDist = FixedDiv(firstDist * FRACUNIT, mapobjectscale) / FRACUNIT; } secondToFirst = K_ScaleItemDistance( players[second].distancetofinish - players[first].distancetofinish, pingame, spbrush ); } else { secondToFirst = P_AproxDistance(P_AproxDistance( players[first].mo->x/4 - players[second].mo->x/4, players[first].mo->y/4 - players[second].mo->y/4), players[first].mo->z/4 - players[second].mo->z/4); // Scale it to prevent overflow issues. secondToFirst = (secondToFirst / FRACUNIT)*2; secondToFirst = K_ScaleItemDistance( secondToFirst, pingame, spbrush ); } } switch (item) { case KITEM_BANANA: notNearEnd = true; notForBottom = true; break; case KITEM_EGGMAN: // It blows you up and is overall ridiculous. This was *overdue*. cooldownOnStart = true; powerItem = true; notNearEnd = true; notForBottom = true; break; case KITEM_SUPERRING: notNearEnd = true; notForBottom = true; if ((K_RingsActive() == false)) // No rings rolled if rings are turned off. { newodds = 0; } break; case KITEM_ROCKETSNEAKER: case KITEM_LANDMINE: case KRITEM_TRIPLESNEAKER: powerItem = true; break; case KITEM_BUBBLESHIELD: // Experiment: Given the refactoring of the item, remove it from the cooldown pool. // Also: a given. case KITEM_BALLHOG: case KITEM_JAWZ: case KRITEM_DUALJAWZ: case KRITEM_TRIPLEORBINAUT: case KRITEM_QUADORBINAUT: notForBottom = true; powerItem = true; break; case KRITEM_TRIPLEBANANA: case KRITEM_TENFOLDBANANA: powerItem = true; notForBottom = true; notNearEnd = true; break; case KITEM_INVINCIBILITY: if ((K_GetKartInvinType() == KARTINVIN_ALTERN) && (gametyperules & GTR_RACEODDS)) { // It's a power item, yes, but we don't want mashing to lessen // its chances, so we lie to the game's face. // Nonetheless, apply the start cooldown. cooldownOnStart = true; // Also, PLEASE prevent shitty last lap bagging endings. notNearEnd = true; // Unique odds for Invincibility. newodds = K_KartGetInvincibilityOdds(clusterDist); // Special case: if you're SERIOUSLY far behind before the cooldown // finishes, remove the cooldown flag. if (newodds >= INVFORCEODDS) { cooldownOnStart = false; } newodds *= BASEODDSMUL; break; } /*FALLTHRU*/ case KITEM_MINE: case KITEM_GROW: case KITEM_FLAMESHIELD: cooldownOnStart = true; powerItem = true; break; case KITEM_SPB: cooldownOnStart = true; indirectItem = true; notNearEnd = true; if (!K_UsingLegacyCheckpoints() && firstDist < (UINT32)ENDDIST) // No SPB near the end of the race { newodds = 0; } else if (K_UsingLegacyCheckpoints() && pexiting > 0) { newodds = 0; } else { const INT32 distFromStart = max(secondToFirst - SPBSTARTDIST, 0); const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST; const INT32 mulMax = 24; INT32 multiplier = (distFromStart * mulMax) / distRange; if (multiplier < 0) multiplier = 0; if (multiplier > mulMax) multiplier = mulMax; newodds *= multiplier; } break; case KITEM_SHRINK: cooldownOnStart = true; powerItem = true; indirectItem = true; notNearEnd = true; if (pingame-1 <= pexiting) newodds = 0; break; case KITEM_THUNDERSHIELD: cooldownOnStart = true; powerItem = true; if (spbplace != -1) newodds = 0; break; case KITEM_HYUDORO: cooldownOnStart = true; break; default: break; } if (newodds == 0) { // Nothing else we want to do with odds matters at this point :p return newodds; } if (K_GetBGone(item, false) > 0) { // (Replaces hyubgone) This item is on cooldown; don't let it get rolled. newodds = 0; } else if ((indirectItem == true) && (indirectitemcooldown > 0)) { // Too many items that act indirectly in a match can feel kind of bad. newodds = 0; } else if ((cooldownOnStart == true) && (leveltime < (30*TICRATE)+starttime)) { // This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items) newodds = 0; } else if (!K_UsingLegacyCheckpoints() && (notNearEnd == true) && (ourDist < (UINT32)ENDDIST)) { // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) newodds = 0; } else if ((notForBottom == true) && (inBottom == true)) { // This item should not appear for losing players. (Usually items that feel less effective at these positions) newodds = 0; } else if (powerItem == true) { // This item is a "power item". This activates "frantic item" toggle related functionality. fixed_t fracOdds = newodds * FRACUNIT; if (franticitems == true) { // First, power items multiply their odds by 2 if frantic items are on; easy-peasy. fracOdds *= 2; } if (rival == true) { // The Rival bot gets frantic-like items, also :p fracOdds *= 2; } fracOdds = FixedMul(fracOdds, FRACUNIT + K_ItemOddsScale(pingame, spbrush)); if (mashed > 0) { // Lastly, it *divides* it based on your mashed value, so that power items are less likely when you mash. fracOdds = FixedDiv(fracOdds, FRACUNIT + mashed); } newodds = fracOdds / FRACUNIT; } return newodds; } //{ SRB2kart Roulette Code - Distance Based, yes waypoints UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper, boolean spbrush) { fixed_t oddsfac = max(FRACUNIT, (MAXODDS * FRACUNIT) / 8); INT32 oddsdiv = ((MAXODDS - 1) * 2); UINT8 i, j; UINT8 useodds = 0; UINT8 disttable[oddsdiv]; UINT8 distlen = 0; boolean oddsvalid[MAXODDS]; // Unused now, oops :V (void)bestbumper; for (i = 0; i < MAXODDS; i++) { boolean available = false; if ((gametyperules & GTR_BATTLEODDS) && i > 1) { oddsvalid[i] = false; break; } for (j = 1; j < NUMKARTRESULTS; j++) { if (K_KartGetItemOdds( i, j, player->distancetofinish, player->distancefromcluster, mashed, spbrush, player->bot, (player->bot && player->botvars.rival), K_IsPlayerLosing(player) ) > 0) { available = true; break; } } oddsvalid[i] = available; } #define SETUPDISTTABLE(odds, num) \ if (oddsvalid[odds]) \ for (i = num; i; --i) \ { \ disttable[distlen++] = odds; \ distlen = min(oddsdiv - 1, distlen); \ } if (gametyperules & GTR_BATTLEODDS) // Battle Mode { if (player->roulettetype == KROULETTETYPE_KARMA && oddsvalid[1] == true) { // 1 is the extreme odds of player-controlled "Karma" items useodds = 1; } else { useodds = 0; if (oddsvalid[0] == false && oddsvalid[1] == true) { // try to use karma odds as a fallback useodds = 1; } } } else if (gametyperules & GTR_RACEODDS) { INT32 tablediv = FixedMul(2, oddsfac); INT32 jj; // "Why j instead of i"? // Using i causes an infinite loop due to SETUPDISTTABLE. Yep. for (j = 0; j < MAXODDS; j++) { if (j == (MAXODDS - 1)) { // Attempt to replicate vanilla behavior; Useodds 8 is set up like this. SETUPDISTTABLE(j,1); } else { jj = max(1, ((j - 3) / tablediv) + 1); if (jj < 1) { jj = 1; } //CONS_Printf("SETUPDISTTABLE(%d, %d)\n", j, jj); SETUPDISTTABLE(j,jj); } } /*SETUPDISTTABLE(0,1); SETUPDISTTABLE(1,1); SETUPDISTTABLE(2,1); SETUPDISTTABLE(3,2); SETUPDISTTABLE(4,2); SETUPDISTTABLE(5,3); SETUPDISTTABLE(6,3); SETUPDISTTABLE(7,1);*/ const INT32 usedistvar = FixedDiv(ACTIVEDISTVAR, oddsfac); if (pdis == 0) useodds = disttable[0]; else if (pdis > (UINT32)ACTIVEDISTVAR * ((12 * distlen) / oddsdiv)) useodds = disttable[distlen-1]; else { for (i = 1; i < (oddsdiv - 1); i++) { INT32 distcalc = min(distlen-1, (i * distlen) / oddsdiv); if (pdis <= (UINT32)usedistvar * distcalc) { useodds = disttable[distcalc]; break; } } } } #undef SETUPDISTTABLE return min(MAXODDS - 1, useodds); } INT32 K_GetRollingRouletteItem(player_t *player) { static UINT8 translation[NUMKARTITEMS-1]; static UINT16 roulette_size; static INT16 odds_cached = -1; // Race odds have more columns than Battle const UINT8 EMPTYODDS[sizeof K_KartItemOddsRace[0]] = {0}; if (odds_cached != gametype) { UINT8 *odds_row; size_t odds_row_size; UINT8 i; roulette_size = 0; if (gametyperules & GTR_BATTLEODDS) { odds_row = K_KartItemOddsBattle[0]; odds_row_size = sizeof K_KartItemOddsBattle[0]; } else { odds_row = K_KartItemOddsRace[0]; odds_row_size = sizeof K_KartItemOddsRace[0]; } for (i = 1; i < NUMKARTITEMS; ++i) { if (memcmp(odds_row, EMPTYODDS, odds_row_size)) { translation[roulette_size] = i; roulette_size++; } odds_row += odds_row_size; } roulette_size *= 3; odds_cached = gametype; } return translation[(player->itemroulette % roulette_size) / 3]; } // Legacy odds are fickle and finicky, so we exaggerate distances // to simulate parity with pathfind odds. #define LEGACYODDSEXAGGERATE (2*FRACUNIT/3) UINT32 K_CalculateInitalPDIS(const player_t *player, UINT8 pingame) { UINT8 i; UINT32 pdis = 0; (void)pingame; if (!K_UsingLegacyCheckpoints()) { for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i] && !players[i].spectator && players[i].position == 1) { // This player is first! Yay! if (player->distancetofinish <= players[i].distancetofinish) { // Guess you're in first / tied for first? pdis = 0; } else { // Subtract 1st's distance from your distance, to get your distance from 1st! pdis = player->distancetofinish - players[i].distancetofinish; } break; } } } else { SINT8 sortedPlayers[MAXPLAYERS]; UINT8 sortLength = 0; memset(sortedPlayers, -1, sizeof(sortedPlayers)); if (player->mo != NULL && P_MobjWasRemoved(player->mo) == false) { // Sort all of the players ahead of you. // Then tally up their distances in a conga line. // This will create a much more consistent item // distance algorithm than the "spider web" thing // that it was doing before. // Add yourself to the list. // You'll always be the end of the list, // so we can also calculate the length here. sortedPlayers[ player->position - 1 ] = player - players; sortLength = player->position; // Will only need to do this if there's goint to be // more than yourself in the list. if (sortLength > 1) { SINT8 firstIndex = -1; SINT8 secondIndex = -1; INT32 startFrom = INT32_MAX; // Add all of the other players. for (i = 0; i < MAXPLAYERS; i++) { INT32 pos = INT32_MAX; if (!playeringame[i] || players[i].spectator) { continue; } if (players[i].mo == NULL || P_MobjWasRemoved(players[i].mo) == true) { continue; } pos = players[i].position; if (pos <= 0 || pos > MAXPLAYERS) { // Invalid position. continue; } if (pos >= player->position) { // Tied / behind us. // Also handles ourselves, obviously. continue; } // Ties are done with port priority, if there are any. if (sortedPlayers[ pos - 1 ] == -1) { sortedPlayers[ pos - 1 ] = i; } } // The chance of this list having gaps is improbable, // but not impossible. So we need to spend some extra time // to prevent the gaps from mattering. for (i = 0; i < sortLength-1; i++) { if (sortedPlayers[i] >= 0 && sortedPlayers[i] < MAXPLAYERS) { // First valid index in the list found. firstIndex = sortedPlayers[i]; // Start the next loop after this player. startFrom = i + 1; break; } } if (firstIndex >= 0 && firstIndex < MAXPLAYERS && startFrom < sortLength) { // First index is valid, so we can // start comparing the players. player_t *firstPlayer = NULL; player_t *secondPlayer = NULL; for (i = startFrom; i < sortLength; i++) { if (sortedPlayers[i] >= 0 && sortedPlayers[i] < MAXPLAYERS) { secondIndex = sortedPlayers[i]; firstPlayer = &players[firstIndex]; secondPlayer = &players[secondIndex]; // Add the distance to the player behind you. // At a (relative to map) integer scale using basic distancing // arithmetic; more accurate and less concern for overflows. // TODO: Overflow-safe addition (caps at UINT32_MAX). pdis += K_IntDistanceForMap( secondPlayer->mo->x, secondPlayer->mo->y, secondPlayer->mo->z, firstPlayer->mo->x, firstPlayer->mo->y, firstPlayer->mo->z); // Advance to next index. firstIndex = secondIndex; } } } } } } // Exaggerate odds; don't you love the legacy system? :Trollic: pdis = FixedMul(pdis, LEGACYODDSEXAGGERATE); return pdis; } #undef LEGACYODDSEXAGGERATE UINT32 K_CalculatePDIS(const player_t *player, UINT8 numPlayers, boolean *spbrush) { UINT32 pdis = 0; pdis = K_CalculateInitalPDIS(player, numPlayers); if (spbplace != -1 && player->position == spbplace+1) { // SPB Rush Mode: It's 2nd place's job to catch-up items and make 1st place's job hell pdis = (3 * pdis) / 2; *spbrush = true; } pdis = K_ScaleItemDistance(pdis, numPlayers, *spbrush); if (player->bot && player->botvars.rival) { // Rival has better odds :) pdis = (15 * pdis) / 14; } // Can almost barely overflow this calc, fudge to prevent this. if (pdis > 30000) pdis = 30000; return pdis; } static boolean K_CanForceSPB(player_t *player, UINT32 pdis) { boolean battlecond, racecond; battlecond = ((gametyperules & GTR_WANTED) && (gametyperules & GTR_WANTEDSPB) && (mostwanted != -1) && (!K_IsPlayerMostWanted(player))); racecond = ((gametyperules & GTR_CIRCUIT) && player->position == 2 && pdis > (UINT32)SPBFORCEDIST); return ((battlecond) || (racecond)); } void K_KartItemRoulette(player_t *player, ticcmd_t *cmd) { INT32 i; UINT8 pingame = 0; UINT8 roulettestop; UINT32 pdis = 0; UINT8 useodds = 0; INT32 spawnchance[NUMKARTRESULTS]; INT64 totalspawnchance = 0; // 75-scale numbers are going to get BIG. This is for paranoia's sake. UINT8 bestbumper = 0; fixed_t mashed = 0; boolean dontforcespb = false; boolean spbrush = false; // This makes the roulette cycle through items - if this is 0, you shouldn't be here. if (!player->itemroulette) return; player->itemroulette++; // Gotta check how many players are active at this moment. for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) continue; pingame++; if (players[i].exiting) dontforcespb = true; if (players[i].bumper > bestbumper) bestbumper = players[i].bumper; } // No forced SPB in 1v1s, it has to be randomly rolled if (pingame <= 2) dontforcespb = true; // This makes the roulette produce the random noises. if ((player->itemroulette % 3) == 1 && P_IsDisplayPlayer(player)) { #define PLAYROULETTESND S_StartSound(NULL, sfx_itrol1 + ((player->itemroulette / 3) % 8)) for (i = 0; i <= r_splitscreen; i++) { if (player == &players[displayplayers[i]] && players[displayplayers[i]].itemroulette) PLAYROULETTESND; } #undef PLAYROULETTESND } roulettestop = TICRATE + (3*(pingame - player->position)); // If the roulette finishes or the player presses BT_ATTACK, stop the roulette and calculate the item. // I'm returning via the exact opposite, however, to forgo having another bracket embed. Same result either way, I think. // Finally, if you get past this check, now you can actually start calculating what item you get. if ((cmd->buttons & BT_ATTACK) && (player->itemroulette >= roulettestop) && (K_RingsActive() || (modeattacking == ATTACKING_NONE)) && !(player->itemflags & (IF_ITEMOUT|IF_EGGMANOUT|IF_USERINGS))) { // Mashing reduces your chances for the good items mashed = FixedDiv((player->itemroulette)*FRACUNIT, ((TICRATE*3)+roulettestop)*FRACUNIT) - FRACUNIT; } else if (!(player->itemroulette >= (TICRATE*3))) return; pdis = K_CalculatePDIS(player, pingame, &spbrush); // SPECIAL CASE No. 1: // Fake Eggman items if (player->roulettetype == KROULETTETYPE_EGGMAN) { player->eggmanexplode = 4*TICRATE; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrole); return; } // SPECIAL CASE No. 2: // Give a debug item instead if specified if (cv_kartdebugitem.value != 0 && !modeattacking) { K_KartGetItemResult(player, cv_kartdebugitem.value); player->itemblink = TICRATE; player->itemblinkmode = KITEMBLINKMODE_KARMA; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_dbgsal); return; } // SPECIAL CASE No. 3: // This Gametype never specified an odds type. Roll something random please! if (!(gametyperules & GTR_RACEODDS) && !(gametyperules & GTR_BATTLEODDS)) { SINT8 itemroll = P_RandomRange(KITEM_SNEAKER, NUMKARTITEMS - 1); K_KartGetItemResult(player, itemroll); player->itemblink = TICRATE; player->itemblinkmode = KITEMBLINKMODE_NORMAL; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolf); return; } // SPECIAL CASE No. 4: // Record Attack / alone mashing behavior if ((modeattacking || pingame == 1) && ((gametyperules & GTR_RACEODDS) || ((gametyperules & GTR_BATTLEODDS) && (itembreaker || bossinfo.boss)))) { if ((gametyperules & GTR_RACEODDS)) { if (mashed && ((K_RingsActive() == true) && (modeattacking || cv_superring.value))) // ANY mashed value? You get rings. { K_KartGetItemResult(player, KITEM_SUPERRING); player->itemblinkmode = KITEMBLINKMODE_MASHED; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolm); } else { if (modeattacking || cv_sneaker.value) // Waited patiently? You get a sneaker! K_KartGetItemResult(player, KITEM_SNEAKER); else // Default to sad if nothing's enabled... K_KartGetItemResult(player, KITEM_SAD); player->itemblinkmode = KITEMBLINKMODE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolf); } } else if (gametyperules & GTR_BATTLEODDS) { if (mashed && (bossinfo.boss || cv_banana.value) && !itembreaker) // ANY mashed value? You get a banana. { K_KartGetItemResult(player, KITEM_BANANA); player->itemblinkmode = KITEMBLINKMODE_MASHED; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolm); } else if (bossinfo.boss) { K_KartGetItemResult(player, KITEM_ORBINAUT); player->itemblinkmode = KITEMBLINKMODE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolf); } else if (itembreaker) { K_KartGetItemResult(player, KITEM_SNEAKER); player->itemblinkmode = KITEMBLINKMODE_MASHED; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolm); } } player->itemblink = TICRATE; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; return; } // SPECIAL CASE No. 5: // Being in ring debt occasionally forces Super Ring on you if you mashed if ((K_RingsActive() == true) && mashed && player->rings < 0 && cv_superring.value) { INT32 debtamount = min(abs(player->ringmin), abs(player->rings)); if (P_RandomChance((debtamount*FRACUNIT)/abs(player->ringmin))) { K_KartGetItemResult(player, KITEM_SUPERRING); player->itemblink = TICRATE; player->itemblinkmode = KITEMBLINKMODE_MASHED; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolm); return; } } // SPECIAL CASE No. 6: // Force SPB onto 2nd if they get too far behind // In battle, an SPB is forced onto players to target the "most wanted" player if (K_CanForceSPB(player, pdis) && spbplace == -1 && !indirectitemcooldown && !dontforcespb && cv_selfpropelledbomb.value) { K_KartGetItemResult(player, KITEM_SPB); player->itemblink = TICRATE; player->itemblinkmode = KITEMBLINKMODE_KARMA; player->itemroulette = KROULETTE_DISABLED; player->roulettetype = KROULETTETYPE_NORMAL; if (P_IsDisplayPlayer(player)) S_StartSound(NULL, sfx_itrolk); return; } // NOW that we're done with all of those specialized cases, we can move onto the REAL item roulette tables. // Initializes existing spawnchance values for (i = 0; i < NUMKARTRESULTS; i++) spawnchance[i] = 0; // Split into another function for a debug function below useodds = K_FindUseodds(player, mashed, pdis, bestbumper, spbrush); for (i = 1; i < NUMKARTRESULTS; i++) { spawnchance[i] = (totalspawnchance += K_KartGetItemOdds( useodds, i, player->distancetofinish, player->distancefromcluster, mashed, spbrush, player->bot, (player->bot && player->botvars.rival), K_IsPlayerLosing(player)) ); } // Award the player whatever power is rolled if (totalspawnchance > 0) { totalspawnchance = P_RandomKey(totalspawnchance); for (i = 0; i < NUMKARTRESULTS && spawnchance[i] <= totalspawnchance; i++); K_KartGetItemResult(player, i); } else { player->itemtype = KITEM_SAD; player->itemamount = 1; } if (P_IsDisplayPlayer(player)) S_StartSound(NULL, ((player->roulettetype == KROULETTETYPE_KARMA) ? sfx_itrolk : (mashed ? sfx_itrolm : sfx_itrolf))); player->itemblink = TICRATE; player->itemblinkmode = ((player->roulettetype == KROULETTETYPE_KARMA) ? KITEMBLINKMODE_KARMA : (mashed ? KITEMBLINKMODE_MASHED : KITEMBLINKMODE_NORMAL)); player->itemroulette = KROULETTE_DISABLED; // Since we're done, clear the roulette number player->roulettetype = KROULETTETYPE_NORMAL; // This too }