From a8dc5774e26f01dcf5c4bf3d8cf665cdc925e9e9 Mon Sep 17 00:00:00 2001 From: Anonimus Date: Wed, 10 Sep 2025 15:58:11 -0400 Subject: [PATCH] Refactor odds and move them to their own file * Race odds now use 16 tiers and are intended to be balanced around 16 players * DISTVAR has been increased to 2048 to match the new tier capacity * Race odds now use a max-75 scale, with the x4 multiplier in calcs being used only in a battle odds context --- src/Sourcefile | 1 + src/d_main.cpp | 4 +- src/k_botitem.cpp | 1 + src/k_collide.c | 1 + src/k_hud.c | 1 + src/k_kart.c | 1483 +---------------------------------------- src/k_kart.h | 16 - src/k_odds.cpp | 1623 +++++++++++++++++++++++++++++++++++++++++++++ src/k_odds.hpp | 50 ++ src/m_menu.c | 3 +- src/p_inter.c | 1 + src/p_mobj.c | 1 + src/p_user.c | 1 + 13 files changed, 1685 insertions(+), 1501 deletions(-) create mode 100644 src/k_odds.cpp create mode 100644 src/k_odds.hpp diff --git a/src/Sourcefile b/src/Sourcefile index 22db137b6..0577eb836 100644 --- a/src/Sourcefile +++ b/src/Sourcefile @@ -128,6 +128,7 @@ k_bot.cpp k_botitem.cpp k_botsearch.cpp k_cluster.cpp +k_odds.cpp k_grandprix.c k_boss.c k_hud.c diff --git a/src/d_main.cpp b/src/d_main.cpp index f1c7fbb12..d74e59e1f 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -89,8 +89,8 @@ #define ASSET_HASH_TEXTURES_KART 0xb4211b2f32b6a291 #define ASSET_HASH_CHARS_KART 0x1e68a3e01aa5c68b #define ASSET_HASH_MAPS_KART 0x38558ed00da41ce9 -#define ASSET_HASH_MAIN_PK3 0xd9cf115699dc58c4 -#define ASSET_HASH_MAPPATCH_PK3 0xf1c7678ded3a2d1b +#define ASSET_HASH_MAIN_PK3 0xb706e22cf2696294 +#define ASSET_HASH_MAPPATCH_PK3 0x01ca675676c950a1 #define ASSET_HASH_BONUSCHARS_KART 0x60e6f13d822a7461 #ifdef USE_PATCH_FILE #define ASSET_HASH_PATCH_PK3 0x0000000000000000 diff --git a/src/k_botitem.cpp b/src/k_botitem.cpp index 96da3b04c..468ed9291 100644 --- a/src/k_botitem.cpp +++ b/src/k_botitem.cpp @@ -29,6 +29,7 @@ #include "m_random.h" #include "r_things.h" // numskins #include "m_easing.h" +#include "k_odds.hpp" // Looks for players around the bot, and presses the item button // if there is one in range. diff --git a/src/k_collide.c b/src/k_collide.c index f65383704..a2960437b 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -7,6 +7,7 @@ #include "doomtype.h" #include "p_mobj.h" #include "k_kart.h" +#include "k_odds.hpp" #include "p_local.h" #include "s_sound.h" #include "r_main.h" // R_PointToAngle2, R_PointToDist2 diff --git a/src/k_hud.c b/src/k_hud.c index fef1c9d7a..9c164ad0e 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -17,6 +17,7 @@ #include "k_boss.h" #include "k_color.h" #include "k_director.h" +#include "k_odds.hpp" #include "p_mobj.h" #include "screen.h" #include "doomtype.h" diff --git a/src/k_kart.c b/src/k_kart.c index ea10ed098..f5df1c051 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -64,6 +64,7 @@ #include "k_follower.h" #include "k_grandprix.h" #include "k_cluster.hpp" +#include "k_odds.hpp" #include "h_timers.h" #include "blan/b_soc.h" @@ -435,1488 +436,6 @@ fixed_t K_GetKartGameSpeedScalar(SINT8 value) return ((13 + (3*value)) << FRACBITS) / 16; } -//{ SRB2kart Roulette Code - Position Based - -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) - -// Less ugly 2D arrays -static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][MAXODDS] = -{ - //B C D E F G H I - { 0, 0, 3, 3, 2, 0, 0, 0 }, // Sneaker - { 0, 0, 0, 0, 2, 5, 8, 5 }, // Rocket Sneaker - { 0, 0, 0, 0, 1, 4, 6,10 }, // Invincibility - { 9, 4, 2, 1, 0, 0, 0, 0 }, // Banana - { 3, 2, 1, 0, 0, 0, 0, 0 }, // Eggman Monitor - { 5, 6, 4, 2, 0, 0, 0, 0 }, // Orbinaut - { 0, 3, 2, 1, 1, 0, 0, 0 }, // Jawz - { 0, 2, 2, 1, 0, 0, 0, 0 }, // Mine - { 0, 0, 2, 1, 0, 0, 0, 0 }, // Ballhog - { 0, 1, 2, 3, 4, 2, 2, 0 }, // Self-Propelled Bomb - { 0, 0, 0, 0, 0, 2, 5, 7 }, // Grow - { 0, 0, 0, 0, 0, 0, 1, 0 }, // Shrink - { 1, 2, 0, 0, 0, 0, 0, 0 }, // Thunder Shield - { 0, 0, 0, 1, 2, 1, 0, 0 }, // Hyudoro - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring - { 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink - { 1, 2, 2, 0, 0, 0, 0, 0 }, // Super Ring - { 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine - { 0, 0, 0, 2, 3, 1, 0, 0 }, // Bubble Shield - { 0, 0, 0, 0, 2, 5, 5, 0 }, // Flame Shield - { 0, 0, 3, 4, 3, 0, 0, 0 }, // Sneaker x2 - { 0, 0, 0, 1, 5, 6, 3, 0 }, // Sneaker x3 - { 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3 - { 0, 0, 0, 1, 0, 0, 0, 0 }, // Banana x10 - { 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3 - { 0, 0, 0, 1, 1, 0, 0, 0 }, // Orbinaut x4 - { 0, 0, 1, 2, 0, 0, 0, 0 } // Jawz x2 -}; - -static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS][2] = -{ - //K L - { 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 -}; - -#define DISTVAR (1280) // Magic number distance for use with item roulette tiers -#define SPBSTARTDIST (5*DISTVAR) // Distance when SPB can start appearing -#define SPBFORCEDIST (15*DISTVAR) // Distance when SPB is forced onto 2nd place -#define ENDDIST (12*DISTVAR) // Distance when the game stops giving you bananas - -INT32 K_GetShieldFromPlayer(player_t *player) -{ - - if (player->flametimer > 0) - { - return KSHIELD_FLAME; - } - - switch (player->itemtype) - { - case KITEM_THUNDERSHIELD: return KSHIELD_THUNDER; - case KITEM_BUBBLESHIELD: return KSHIELD_BUBBLE; - //case KITEM_FLAMESHIELD: return KSHIELD_FLAME; not active until flametimer is active. - default: return KSHIELD_NONE; - } -} - -INT32 K_GetShieldFromItem(INT32 item) -{ - switch (item) - { - case KITEM_THUNDERSHIELD: return KSHIELD_THUNDER; - case KITEM_BUBBLESHIELD: return KSHIELD_BUBBLE; - case KITEM_FLAMESHIELD: return KSHIELD_FLAME; - default: return KSHIELD_NONE; - } -} - -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 (getitem == KITEM_HYUDORO) // Hyudoro cooldown - hyubgone = 20*TICRATE; - - 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; -} - -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 = 8; // 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: x2.5 - playerScaling = (basePlayer - playerCount) * (FRACUNIT / 4); - } - 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 4 -#define INVINDESPERATION 4 - -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, 20 * INVINDESPERATION); - } - 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(20 * INVINDESPERATION, finodds); -} - -/** \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) -{ - 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; - - 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; - - if (gametyperules & GTR_BATTLEODDS) - { - I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table - newodds = K_KartItemOddsBattle[item-1][pos]; - } - else if (gametyperules & GTR_RACEODDS) - { - I_Assert(pos < MAXODDS); // Ditto - newodds = K_KartItemOddsRace[item-1][pos]; - } - else - { - newodds = 0; - } - - // Base multiplication to ALL item odds to simulate fractional precision - newodds *= 4; - - 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 - { - firstDist = players[first].distancetofinish; - - if (mapobjectscale != FRACUNIT) - { - firstDist = FixedDiv(firstDist * FRACUNIT, mapobjectscale) / FRACUNIT; - } - - secondToFirst = K_ScaleItemDistance( - players[second].distancetofinish - players[first].distancetofinish, - pingame, spbrush - ); - } - - switch (item) - { - case KITEM_BANANA: - case KITEM_EGGMAN: - notNearEnd = true; - break; - case KITEM_SUPERRING: - notNearEnd = true; - - if ((K_RingsActive() == false)) // No rings rolled if rings are turned off. - { - newodds = 0; - } - - break; - case KITEM_ROCKETSNEAKER: - case KITEM_JAWZ: - case KITEM_LANDMINE: - case KITEM_BALLHOG: - case KRITEM_TRIPLESNEAKER: - case KRITEM_TRIPLEORBINAUT: - case KRITEM_QUADORBINAUT: - case KRITEM_DUALJAWZ: - powerItem = true; - break; - case KRITEM_TRIPLEBANANA: - case KRITEM_TENFOLDBANANA: - powerItem = true; - notNearEnd = true; - break; - case KITEM_INVINCIBILITY: - if (K_GetKartInvinType() == KARTINVIN_ALTERN) - { - // 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; - - // Unique odds for Invincibility. - newodds = K_KartGetInvincibilityOdds(clusterDist); - - newodds *= 4; - break; - } - /*FALLTHRU*/ - case KITEM_MINE: - case KITEM_GROW: - case KITEM_BUBBLESHIELD: - case KITEM_FLAMESHIELD: - cooldownOnStart = true; - powerItem = true; - break; - case KITEM_SPB: - cooldownOnStart = true; - indirectItem = true; - notNearEnd = true; - - if (firstDist < ENDDIST) // No SPB near the end of the race - { - newodds = 0; - } - else - { - const INT32 distFromStart = max(0, (INT32)secondToFirst - SPBSTARTDIST); - const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST; - const INT32 mulMax = 3; - - 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; - - if (hyubgone > 0) - newodds = 0; - break; - default: - break; - } - - if (newodds == 0) - { - // Nothing else we want to do with odds matters at this point :p - return newodds; - } - - 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 ((notNearEnd == true) && (ourDist < ENDDIST)) - { - // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) - 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; -} - -INT32 K_KartGetLegacyItemOdds(UINT8 pos, SINT8 item, fixed_t clusterDist, fixed_t mashed, boolean spbrush) -{ - INT32 newodds; - INT32 i; - UINT8 pingame = 0, pexiting = 0; - SINT8 first = -1, second = -1; - UINT32 secondToFirst = 0; - INT32 shieldtype = KSHIELD_NONE; - - boolean powerItem = false; - boolean cooldownOnStart = false; - boolean indirectItem = false; - - I_Assert(item > KITEM_NONE); // too many off by one scenarioes. - - if (!KartItemCVars[item-1]->value && !modeattacking) - return 0; - - if (gametyperules & GTR_BATTLEODDS) - newodds = K_KartItemOddsBattle[item-1][pos]; - else if (gametyperules & GTR_RACEODDS) - newodds = K_KartItemOddsRace[item-1][pos]; - else - newodds = 0; - - // Base multiplication to ALL item odds to simulate fractional precision - newodds *= 4; - - 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 - { - secondToFirst = P_AproxDistance(P_AproxDistance(players[first].mo->x - players[second].mo->x, - players[first].mo->y - players[second].mo->y), - players[first].mo->z - players[second].mo->z) / FRACUNIT; - secondToFirst = K_ScaleItemDistance(secondToFirst, pingame, spbrush); - } - - switch (item) - { - case KITEM_ROCKETSNEAKER: - case KITEM_JAWZ: - case KITEM_BALLHOG: - case KITEM_LANDMINE: - case KRITEM_TRIPLESNEAKER: - case KRITEM_TRIPLEBANANA: - case KRITEM_TENFOLDBANANA: - case KRITEM_TRIPLEORBINAUT: - case KRITEM_QUADORBINAUT: - case KRITEM_DUALJAWZ: - powerItem = true; - break; - case KITEM_INVINCIBILITY: - if (K_GetKartInvinType() == KARTINVIN_ALTERN) - { - // 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; - - // Unique odds for Invincibility. - newodds = K_KartGetInvincibilityOdds(clusterDist); - - newodds *= 4; - break; - } - /*FALLTHRU*/ - case KITEM_MINE: - case KITEM_GROW: - case KITEM_BUBBLESHIELD: - case KITEM_FLAMESHIELD: - cooldownOnStart = true; - powerItem = true; - break; - case KITEM_SPB: - cooldownOnStart = true; - indirectItem = true; - //powerItem = true; - - if (pexiting > 0) - { - newodds = 0; - } - else if (pos != 9) // Force SPB - { - const INT32 distFromStart = max(0, (INT32)secondToFirst - SPBSTARTDIST); - const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST; - const fixed_t mulMax = 3*FRACUNIT; - - fixed_t multiplier = (distFromStart * mulMax) / distRange; - - if (multiplier < 0) - multiplier = 0; - - if (multiplier > mulMax) - multiplier = mulMax; - - newodds = FixedMul(newodds * FRACUNIT, multiplier) / FRACUNIT; - } - break; - case KITEM_SHRINK: - cooldownOnStart = true; - powerItem = true; - indirectItem = true; - - if (pingame-1 <= pexiting) - newodds = 0; - break; - case KITEM_THUNDERSHIELD: - cooldownOnStart = true; - powerItem = true; - break; - case KITEM_HYUDORO: - cooldownOnStart = true; - - if (hyubgone > 0) - newodds = 0; - break; - default: - break; - } - - if (newodds == 0) - { - // Nothing else we want to do with odds matters at this point :p - return newodds; - } - - 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 (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; - } - - 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) -{ - UINT8 i; - UINT8 useodds = 0; - UINT8 disttable[14]; - UINT8 distlen = 0; - boolean oddsvalid[8]; - - // Unused now, oops :V - (void)bestbumper; - - for (i = 0; i < 8; i++) - { - UINT8 j; - 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) - ) > 0) - { - available = true; - break; - } - } - - oddsvalid[i] = available; - } - -#define SETUPDISTTABLE(odds, num) \ - if (oddsvalid[odds]) \ - for (i = num; i; --i) \ - disttable[distlen++] = odds; - - 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) - { - SETUPDISTTABLE(0,1); - SETUPDISTTABLE(1,1); - SETUPDISTTABLE(2,1); - SETUPDISTTABLE(3,2); - SETUPDISTTABLE(4,2); - SETUPDISTTABLE(5,3); - SETUPDISTTABLE(6,3); - SETUPDISTTABLE(7,1); - - if (pdis == 0) - useodds = disttable[0]; - else if (pdis > DISTVAR * ((12 * distlen) / 14)) - useodds = disttable[distlen-1]; - else - { - for (i = 1; i < 13; i++) - { - if (pdis <= DISTVAR * ((i * distlen) / 14)) - { - useodds = disttable[((i * distlen) / 14)]; - break; - } - } - } - } - -#undef SETUPDISTTABLE - - return useodds; -} - -INT32 K_FindLegacyUseodds(player_t *player, fixed_t mashed, INT32 pingame, INT32 bestbumper, boolean spbrush, boolean dontforcespb) -{ - SINT8 sortedPlayers[MAXPLAYERS]; - UINT8 sortLength = 0; - - UINT32 pdis = 0; - - UINT8 disttable[14]; - UINT8 distlen = 0; - - boolean oddsvalid[8]; - INT32 useodds = 0; - - INT32 i; - - // Unused now, oops :V - (void)bestbumper; - - for (i = 0; i < 8; i++) - { - INT32 j; - boolean available = false; - - if ((gametyperules & GTR_BATTLEODDS) && i > 1) - { - oddsvalid[i] = false; - break; - } - - for (j = 1; j < NUMKARTRESULTS; j++) - { - - if (K_KartGetLegacyItemOdds(i, j, player->distancefromcluster, mashed, spbrush) > 0) - { - available = true; - break; - } - - } - - oddsvalid[i] = available; - } - - 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. - pdis += P_AproxDistance(P_AproxDistance( - firstPlayer->mo->x - secondPlayer->mo->x, - firstPlayer->mo->y - secondPlayer->mo->y), - firstPlayer->mo->z - secondPlayer->mo->z) / FRACUNIT; - - // Advance to next index. - firstIndex = secondIndex; - } - } - } - } - } - -#define SETUPDISTTABLE(odds, num) \ - if (oddsvalid[odds]) \ - for (i = num; i; --i) \ - disttable[distlen++] = odds; - - 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 - { - SETUPDISTTABLE(0,1); - SETUPDISTTABLE(1,1); - SETUPDISTTABLE(2,1); - SETUPDISTTABLE(3,2); - SETUPDISTTABLE(4,2); - SETUPDISTTABLE(5,3); - SETUPDISTTABLE(6,3); - SETUPDISTTABLE(7,1); - - pdis = K_ScaleItemDistance(pdis, pingame, spbrush); - - if (player->bot && player->botvars.rival) - { - // Rival has better odds :) - pdis = (15 * pdis) / 14; - } - - if (pingame == 1 && oddsvalid[0]) - { - // Record Attack / FREE PLAY - useodds = 0; - } - else if (pdis <= 0) - { - // 1st place - useodds = disttable[0]; - } - else if (player->position == 2 && pdis > SPBFORCEDIST - && spbplace == -1 && !indirectitemcooldown && !dontforcespb) - { - // Force SPB in 2nd - useodds = 69; - } - - else if (pdis > DISTVAR * ((12 * distlen) / 14)) - { - // Back of the pack - useodds = disttable[distlen-1]; - } - else - { - for (i = 1; i < 13; i++) - { - if (pdis <= (unsigned)(DISTVAR * ((i * distlen) / 14))) - { - useodds = disttable[((i * distlen) / 14)]; - break; - } - } - } - } - -#undef SETUPDISTTABLE - - //CONS_Printf("Got useodds %d. (position: %d, distance: %d)\n", useodds, player->position, pdis); - - return 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]; -} - -static 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]; - INT32 totalspawnchance = 0; - 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; - - 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; - } - } - } - - 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 - if (!(K_UsingLegacyCheckpoints())) - pdis = (3 * pdis) / 2; - spbrush = true; - } - - if (!(K_UsingLegacyCheckpoints())) - { - pdis = K_ScaleItemDistance(pdis, pingame, spbrush); - - if (player->bot && player->botvars.rival) - { - // Rival has better odds :) - pdis = (15 * pdis) / 14; - } - } - - - // 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; - } - } - - if (!(K_UsingLegacyCheckpoints())) - { - // SPECIAL CASE No. 6: - // Force SPB onto 2nd if they get too far behind - if ((gametyperules & GTR_CIRCUIT) && player->position == 2 && pdis > SPBFORCEDIST - && 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 - // Use a legacy version for maps not using waypoints. - if (K_UsingLegacyCheckpoints()) - useodds = K_FindLegacyUseodds(player, mashed, pingame, bestbumper, spbrush, dontforcespb); - else - useodds = K_FindUseodds(player, mashed, pdis, bestbumper, spbrush); - - if (useodds == 69) - { - 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; - } - - for (i = 1; i < NUMKARTRESULTS; i++) - { - if (K_UsingLegacyCheckpoints()) - { - spawnchance[i] = (totalspawnchance += K_KartGetLegacyItemOdds(useodds, i, player->distancefromcluster, mashed, spbrush)); - - } - else - { - spawnchance[i] = (totalspawnchance += K_KartGetItemOdds( - useodds, i, - player->distancetofinish, - player->distancefromcluster, - mashed, - spbrush, player->bot, (player->bot && player->botvars.rival)) - ); - } - } - - // 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 -} - -//} - //{ SRB2kart p_user.c Stuff static fixed_t K_PlayerWeight(mobj_t *mobj, mobj_t *against) diff --git a/src/k_kart.h b/src/k_kart.h index 935c50966..c95709aad 100644 --- a/src/k_kart.h +++ b/src/k_kart.h @@ -68,9 +68,6 @@ extern boolean clusterplayer[MAXPLAYERS]; extern UINT32 clusterid; // ID of the "cluster player", the one closest to the cluster point. extern vector3_t clusterpoint, clusterdtf; -// :) -#define MAXODDS 8 - // Bubble Shield's maximum health #define MAXBUBBLEHEALTH 6 @@ -189,19 +186,6 @@ UINT32 K_GetPlayerDontDrawFlag(player_t *player); boolean K_IsPlayerLosing(player_t *player); fixed_t K_GetKartGameSpeedScalar(SINT8 value); -extern consvar_t *KartItemCVars[NUMKARTRESULTS-1]; - -UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper, boolean spbrush); -INT32 K_FindLegacyUseodds(player_t *player, fixed_t mashed, INT32 pingame, INT32 bestbumper, boolean spbrush, boolean dontforcespb); -fixed_t K_ItemOddsScale(UINT8 numPlayers, boolean spbrush); -UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers, boolean spbrush); -INT32 K_KartGetItemOdds(UINT8 pos, SINT8 item, UINT32 ourDist, UINT32 clusterDist, fixed_t mashed, boolean spbrush, boolean bot, boolean rival); -INT32 K_KartGetLegacyItemOdds(UINT8 pos, SINT8 item, fixed_t clusterDist, fixed_t mashed, boolean spbrush); -INT32 K_GetRollingRouletteItem(player_t *player); -INT32 K_GetShieldFromPlayer(player_t *player); -INT32 K_GetShieldFromItem(INT32 item); -SINT8 K_ItemResultToType(SINT8 getitem); -UINT8 K_ItemResultToAmount(SINT8 getitem); fixed_t K_GetMobjWeight(mobj_t *mobj, mobj_t *against); boolean K_KartBouncing(mobj_t *mobj1, mobj_t *mobj2, boolean bounce, boolean solid); void K_KartPainEnergyFling(player_t *player); diff --git a/src/k_odds.cpp b/src/k_odds.cpp new file mode 100644 index 000000000..8877fd481 --- /dev/null +++ b/src/k_odds.cpp @@ -0,0 +1,1623 @@ +// 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.hpp" + +extern "C" { + +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, 11, 22, 18, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Sneaker + { 0, 0, 0, 0, 0, 0, 0, 0, 3, 17, 37, 48, 60, 48, 37, 37}, // Rocket Sneaker + { 0, 0, 0, 0, 0, 0, 0, 3, 7, 18, 30, 37, 45, 60, 75, 75}, // Invincibility + {67, 48, 30, 22, 15, 11, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana + {22, 18, 15, 11, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Eggman Monitor + {37, 41, 45, 37, 30, 22, 15, 7, 0, 0, 0, 0, 0, 0, 0, 0}, // Orbinaut + { 0, 11, 22, 18, 15, 11, 7, 7, 7, 3, 0, 0, 0, 0, 0, 0}, // Jawz + { 0, 7, 15, 15, 15, 11, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0}, // Mine + { 0, 0, 0, 7, 15, 11, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0}, // Ballhog + { 0, 0, 0, 1, 5, 8, 12, 13, 9, 6, 2, 0, 0, 0, 0, 0}, // Self-Propelled Bomb + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 15, 26, 37, 45, 52, 52}, // Grow + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 7, 3, 0, 0}, // Shrink + {15, 11, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Thunder Shield + { 0, 0, 0, 0, 0, 3, 7, 11, 15, 11, 7, 3, 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, 15, 15, 7, 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, 12, 22, 15, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Bubble Shield + { 0, 0, 0, 0, 0, 0, 0, 7, 15, 26, 37, 37, 37, 18, 0, 0}, // Flame Shield + { 0, 0, 0, 11, 22, 30, 22, 11, 3, 0, 0, 0, 0, 0, 0, 0}, // Sneaker x2 + { 0, 0, 0, 0, 0, 3, 7, 26, 45, 22, 7, 0, 0, 0, 0, 0}, // Sneaker x3 + { 0, 3, 7, 7, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana x3 + { 0, 0, 0, 0, 0, 3, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0}, // Banana x10 + { 0, 0, 0, 3, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Orbinaut x3 + { 0, 0, 0, 0, 0, 3, 7, 7, 7, 3, 0, 0, 0, 0, 0, 0}, // Orbinaut x4 + { 0, 0, 0, 3, 7, 11, 15, 7, 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 +}; + +#define DISTVAR (2048) // Magic number distance for use with item roulette tiers +#define SPBSTARTDIST (5*DISTVAR) // Distance when SPB can start appearing +#define SPBFORCEDIST (15*DISTVAR) // Distance when SPB is forced onto 2nd place +#define ENDDIST (12*DISTVAR) // Distance when the game stops giving you bananas + +INT32 K_GetShieldFromPlayer(player_t *player) +{ + + if (player->flametimer > 0) + { + return KSHIELD_FLAME; + } + + switch (player->itemtype) + { + case KITEM_THUNDERSHIELD: return KSHIELD_THUNDER; + case KITEM_BUBBLESHIELD: return KSHIELD_BUBBLE; + //case KITEM_FLAMESHIELD: return KSHIELD_FLAME; not active until flametimer is active. + default: return KSHIELD_NONE; + } +} + +INT32 K_GetShieldFromItem(INT32 item) +{ + switch (item) + { + case KITEM_THUNDERSHIELD: return KSHIELD_THUNDER; + case KITEM_BUBBLESHIELD: return KSHIELD_BUBBLE; + case KITEM_FLAMESHIELD: return KSHIELD_FLAME; + default: return KSHIELD_NONE; + } +} + +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 (getitem == KITEM_HYUDORO) // Hyudoro cooldown + hyubgone = 20*TICRATE; + + 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; +} + +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: x2.5 + playerScaling = (basePlayer - playerCount) * (FRACUNIT / 4); + } + 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 - std::min(extPlayer, static_cast(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) + +static INT32 K_KartGetInvincibilityOdds(UINT32 dist) +{ + UINT32 invindist = INVINDIST/2; + + if (dist < invindist) + return 0; + + INT32 finodds = 0; + fixed_t fac = (std::min(32000, (fixed_t)dist) * FRACUNIT) / (INVINDIST); + + if (fac > FRACUNIT) + { + // Desperation! Climb exponentially until Invincibility is practically guaranteed. + fac = (((std::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 std::min(MAXINVODDS, finodds); +} + +/** \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) +{ + 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; + + 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 + { + firstDist = players[first].distancetofinish; + + if (mapobjectscale != FRACUNIT) + { + firstDist = FixedDiv(firstDist * FRACUNIT, mapobjectscale) / FRACUNIT; + } + + secondToFirst = K_ScaleItemDistance( + players[second].distancetofinish - players[first].distancetofinish, + pingame, spbrush + ); + } + + switch (item) + { + case KITEM_BANANA: + case KITEM_EGGMAN: + notNearEnd = true; + break; + case KITEM_SUPERRING: + notNearEnd = true; + + if ((K_RingsActive() == false)) // No rings rolled if rings are turned off. + { + newodds = 0; + } + + break; + case KITEM_ROCKETSNEAKER: + case KITEM_JAWZ: + case KITEM_LANDMINE: + case KITEM_BALLHOG: + case KRITEM_TRIPLESNEAKER: + case KRITEM_TRIPLEORBINAUT: + case KRITEM_QUADORBINAUT: + case KRITEM_DUALJAWZ: + powerItem = true; + break; + case KRITEM_TRIPLEBANANA: + case KRITEM_TENFOLDBANANA: + powerItem = 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; + + // Unique odds for Invincibility. + newodds = K_KartGetInvincibilityOdds(clusterDist); + + newodds *= BASEODDSMUL; + break; + } + /*FALLTHRU*/ + case KITEM_MINE: + case KITEM_GROW: + case KITEM_BUBBLESHIELD: + case KITEM_FLAMESHIELD: + cooldownOnStart = true; + powerItem = true; + break; + case KITEM_SPB: + cooldownOnStart = true; + indirectItem = true; + notNearEnd = true; + + if (firstDist < ENDDIST) // No SPB near the end of the race + { + newodds = 0; + } + else + { + const INT32 distFromStart = std::max(0, static_cast(secondToFirst) - SPBSTARTDIST); + const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST; + const INT32 mulMax = 3; + + 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; + + if (hyubgone > 0) + newodds = 0; + break; + default: + break; + } + + if (newodds == 0) + { + // Nothing else we want to do with odds matters at this point :p + return newodds; + } + + 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 ((notNearEnd == true) && (ourDist < ENDDIST)) + { + // This item should not appear at the end of a race. (Usually trap items that lose their effectiveness) + 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; +} + +INT32 K_KartGetLegacyItemOdds(UINT8 pos, SINT8 item, fixed_t clusterDist, fixed_t mashed, boolean spbrush) +{ + INT32 newodds; + INT32 i; + UINT8 pingame = 0, pexiting = 0; + SINT8 first = -1, second = -1; + UINT32 secondToFirst = 0; + INT32 shieldtype = KSHIELD_NONE; + + boolean powerItem = false; + boolean cooldownOnStart = false; + boolean indirectItem = false; + + I_Assert(item > KITEM_NONE); // too many off by one scenarioes. + + if (!KartItemCVars[item-1]->value && !modeattacking) + return 0; + + INT32 oddsmul = BASEODDSMUL; + + if (gametyperules & GTR_BATTLEODDS) + { + newodds = K_KartItemOddsBattle[item-1][pos]; + oddsmul = BATTLEODDSMUL; + } + else if (gametyperules & GTR_RACEODDS) + 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 + { + secondToFirst = P_AproxDistance(P_AproxDistance(players[first].mo->x - players[second].mo->x, + players[first].mo->y - players[second].mo->y), + players[first].mo->z - players[second].mo->z) / FRACUNIT; + secondToFirst = K_ScaleItemDistance(secondToFirst, pingame, spbrush); + } + + switch (item) + { + case KITEM_ROCKETSNEAKER: + case KITEM_JAWZ: + case KITEM_BALLHOG: + case KITEM_LANDMINE: + case KRITEM_TRIPLESNEAKER: + case KRITEM_TRIPLEBANANA: + case KRITEM_TENFOLDBANANA: + case KRITEM_TRIPLEORBINAUT: + case KRITEM_QUADORBINAUT: + case KRITEM_DUALJAWZ: + powerItem = 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; + + // Unique odds for Invincibility. + newodds = K_KartGetInvincibilityOdds(clusterDist); + + newodds *= BASEODDSMUL; + break; + } + /*FALLTHRU*/ + case KITEM_MINE: + case KITEM_GROW: + case KITEM_BUBBLESHIELD: + case KITEM_FLAMESHIELD: + cooldownOnStart = true; + powerItem = true; + break; + case KITEM_SPB: + cooldownOnStart = true; + indirectItem = true; + //powerItem = true; + + if (pexiting > 0) + { + newodds = 0; + } + else if (pos != 9) // Force SPB + { + const INT32 distFromStart = std::max(0, static_cast(secondToFirst) - SPBSTARTDIST); + const INT32 distRange = SPBFORCEDIST - SPBSTARTDIST; + const fixed_t mulMax = 3*FRACUNIT; + + fixed_t multiplier = (distFromStart * mulMax) / distRange; + + if (multiplier < 0) + multiplier = 0; + + if (multiplier > mulMax) + multiplier = mulMax; + + newodds = FixedMul(newodds * FRACUNIT, multiplier) / FRACUNIT; + } + break; + case KITEM_SHRINK: + cooldownOnStart = true; + powerItem = true; + indirectItem = true; + + if (pingame-1 <= pexiting) + newodds = 0; + break; + case KITEM_THUNDERSHIELD: + cooldownOnStart = true; + powerItem = true; + break; + case KITEM_HYUDORO: + cooldownOnStart = true; + + if (hyubgone > 0) + newodds = 0; + break; + default: + break; + } + + if (newodds == 0) + { + // Nothing else we want to do with odds matters at this point :p + return newodds; + } + + 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 (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; + } + + 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 = std::max(FRACUNIT, (MAXODDS * FRACUNIT) / 8); + INT32 oddsdiv = ((MAXODDS - 1) * 2); + + UINT8 i; + INT32 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++) + { + UINT8 j; + 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) + ) > 0) + { + available = true; + break; + } + } + + oddsvalid[i] = available; + } + +#define SETUPDISTTABLE(odds, num) \ + if (oddsvalid[odds]) \ + for (i = num; i; --i) \ + { \ + disttable[distlen++] = odds; \ + distlen = std::min(static_cast(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 = std::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(DISTVAR, oddsfac); + + if (pdis == 0) + useodds = disttable[0]; + else if (pdis > DISTVAR * ((12 * distlen) / oddsdiv)) + useodds = disttable[distlen-1]; + else + { + for (i = 1; i < (oddsdiv - 1); i++) + { + INT32 distcalc = std::min(static_cast(distlen-1), (i * distlen) / oddsdiv); + + if (pdis <= usedistvar * distcalc) + { + useodds = disttable[distcalc]; + break; + } + } + } + } + +#undef SETUPDISTTABLE + + return useodds; +} + +INT32 K_FindLegacyUseodds(player_t *player, fixed_t mashed, INT32 pingame, INT32 bestbumper, boolean spbrush, boolean dontforcespb) +{ + fixed_t oddsfac = std::max(FRACUNIT, (MAXODDS * FRACUNIT) / 8); + INT32 oddsdiv = (MAXODDS - 1) * 2; + + SINT8 sortedPlayers[MAXPLAYERS]; + UINT8 sortLength = 0; + + UINT32 pdis = 0; + + UINT8 disttable[oddsdiv]; + UINT8 distlen = 0; + + boolean oddsvalid[MAXODDS]; + INT32 useodds = 0; + + INT32 i, j; + + // Unused now, oops :V + (void)bestbumper; + + for (i = 0; i < MAXODDS; i++) + { + INT32 j; + boolean available = false; + + if ((gametyperules & GTR_BATTLEODDS) && i > 1) + { + oddsvalid[i] = false; + break; + } + + for (j = 1; j < NUMKARTRESULTS; j++) + { + + if (K_KartGetLegacyItemOdds(i, j, player->distancefromcluster, mashed, spbrush) > 0) + { + available = true; + break; + } + + } + + oddsvalid[i] = available; + } + + 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. + pdis += P_AproxDistance(P_AproxDistance( + firstPlayer->mo->x - secondPlayer->mo->x, + firstPlayer->mo->y - secondPlayer->mo->y), + firstPlayer->mo->z - secondPlayer->mo->z) / FRACUNIT; + + // Advance to next index. + firstIndex = secondIndex; + } + } + } + } + } + +#define SETUPDISTTABLE(odds, num) \ + if (oddsvalid[odds]) \ + for (i = num; i; --i) \ + { \ + disttable[distlen++] = odds; \ + distlen = std::min(static_cast(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 + { + 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 = std::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);*/ + + pdis = K_ScaleItemDistance(pdis, pingame, spbrush); + + const INT32 usedistvar = FixedDiv(DISTVAR, oddsfac); + + if (player->bot && player->botvars.rival) + { + // Rival has better odds :) + pdis = (15 * pdis) / 14; + } + + if (pingame == 1 && oddsvalid[0]) + { + // Record Attack / FREE PLAY + useodds = 0; + } + else if (pdis <= 0) + { + // 1st place + useodds = disttable[0]; + } + else if (player->position == 2 && pdis > SPBFORCEDIST + && spbplace == -1 && !indirectitemcooldown && !dontforcespb) + { + // Force SPB in 2nd + useodds = 69; + } + + else if (pdis > DISTVAR * ((12 * distlen) / oddsdiv)) + { + // Back of the pack + useodds = disttable[distlen-1]; + } + else + { + for (i = 1; i < (oddsdiv - 1); i++) + { + INT32 distcalc = std::min(static_cast(distlen-1), (i * distlen) / oddsdiv); + if (pdis <= (unsigned)(usedistvar * distcalc)) + { + useodds = disttable[distcalc]; + break; + } + } + } + } + +#undef SETUPDISTTABLE + + //CONS_Printf("Got useodds %d. (position: %d, distance: %d)\n", useodds, player->position, pdis); + + return 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]; +} + +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, static_cast(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; + + 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; + } + } + } + + 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 + if (!(K_UsingLegacyCheckpoints())) + pdis = (3 * pdis) / 2; + spbrush = true; + } + + if (!(K_UsingLegacyCheckpoints())) + { + pdis = K_ScaleItemDistance(pdis, pingame, spbrush); + + if (player->bot && player->botvars.rival) + { + // Rival has better odds :) + pdis = (15 * pdis) / 14; + } + } + + + // 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 = std::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; + } + } + + if (!(K_UsingLegacyCheckpoints())) + { + // SPECIAL CASE No. 6: + // Force SPB onto 2nd if they get too far behind + if ((gametyperules & GTR_CIRCUIT) && player->position == 2 && pdis > SPBFORCEDIST + && 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 + // Use a legacy version for maps not using waypoints. + if (K_UsingLegacyCheckpoints()) + useodds = K_FindLegacyUseodds(player, mashed, pingame, bestbumper, spbrush, dontforcespb); + else + useodds = K_FindUseodds(player, mashed, pdis, bestbumper, spbrush); + + if (useodds == 69) + { + 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; + } + + for (i = 1; i < NUMKARTRESULTS; i++) + { + if (K_UsingLegacyCheckpoints()) + { + spawnchance[i] = (totalspawnchance += K_KartGetLegacyItemOdds(useodds, i, player->distancefromcluster, mashed, spbrush)); + + } + else + { + spawnchance[i] = (totalspawnchance += K_KartGetItemOdds( + useodds, i, + player->distancetofinish, + player->distancefromcluster, + mashed, + spbrush, player->bot, (player->bot && player->botvars.rival)) + ); + } + } + + // 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 +} + +} \ No newline at end of file diff --git a/src/k_odds.hpp b/src/k_odds.hpp new file mode 100644 index 000000000..446954619 --- /dev/null +++ b/src/k_odds.hpp @@ -0,0 +1,50 @@ +// 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.hpp +/// \brief Kart item odds systems. + +#ifndef __K_ODDS__ +#define __K_ODDS__ + +#include "command.h" // Need for player_t +#include "doomdef.h" +#include "doomtype.h" +#include "d_player.h" // Need for player_t +#include "d_ticcmd.h" +#include "m_fixed.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Max odds count +#define MAXODDS 16 + +extern consvar_t *KartItemCVars[NUMKARTRESULTS-1]; + +UINT8 K_FindUseodds(const player_t *player, fixed_t mashed, UINT32 pdis, UINT8 bestbumper, boolean spbrush); +INT32 K_FindLegacyUseodds(player_t *player, fixed_t mashed, INT32 pingame, INT32 bestbumper, boolean spbrush, boolean dontforcespb); +fixed_t K_ItemOddsScale(UINT8 numPlayers, boolean spbrush); +UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers, boolean spbrush); +INT32 K_KartGetItemOdds(UINT8 pos, SINT8 item, UINT32 ourDist, UINT32 clusterDist, fixed_t mashed, boolean spbrush, boolean bot, boolean rival); +INT32 K_KartGetLegacyItemOdds(UINT8 pos, SINT8 item, fixed_t clusterDist, fixed_t mashed, boolean spbrush); +INT32 K_GetRollingRouletteItem(player_t *player); +INT32 K_GetShieldFromPlayer(player_t *player); +INT32 K_GetShieldFromItem(INT32 item); +SINT8 K_ItemResultToType(SINT8 getitem); +UINT8 K_ItemResultToAmount(SINT8 getitem); +void K_KartItemRoulette(player_t *player, ticcmd_t *cmd); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __K_ODDS__ \ No newline at end of file diff --git a/src/m_menu.c b/src/m_menu.c index 60371b6c1..c4084cbcd 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -66,7 +66,8 @@ #include "st_stuff.h" #include "i_sound.h" #include "k_hud.h" // SRB2kart -#include "k_kart.h" // KartItemCVars +#include "k_kart.h" +#include "k_odds.hpp" // KartItemCVars #include "k_pwrlv.h" #include "k_stats.h" // SRB2kart #include "d_player.h" // KITEM_ constants diff --git a/src/p_inter.c b/src/p_inter.c index 334e1d53d..31c33f087 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -39,6 +39,7 @@ #include "k_boss.h" #include "p_spec.h" #include "k_objects.h" +#include "k_odds.hpp" #include "acs/interface.h" // CTF player names diff --git a/src/p_mobj.c b/src/p_mobj.c index 07b10a815..a997e5a1f 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -50,6 +50,7 @@ #include "k_terrain.h" #include "k_collide.h" #include "k_objects.h" +#include "k_odds.hpp" // BlanKart #include "blan/b_soc.h" diff --git a/src/p_user.c b/src/p_user.c index 50c530573..65248ba3f 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -61,6 +61,7 @@ #include "k_terrain.h" // K_SpawnSplashForMobj #include "k_color.h" #include "k_follower.h" +#include "k_odds.hpp" #include "g_party.h" #include "acs/interface.h"