RingRacers/src/k_roulette.c
Antonio Martinez c7fcf34a8a Vibetweaking
2025-09-23 22:12:38 -04:00

2131 lines
56 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Sally "TehRealSalt" Cochenour
// Copyright (C) 2025 by Kart Krew
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_roulette.c
/// \brief Item roulette code.
#include "k_roulette.h"
#include "d_player.h"
#include "doomdef.h"
#include "hu_stuff.h"
#include "g_game.h"
#include "m_random.h"
#include "p_local.h"
#include "p_slopes.h"
#include "p_setup.h"
#include "r_draw.h"
#include "r_local.h"
#include "r_things.h"
#include "s_sound.h"
#include "st_stuff.h"
#include "v_video.h"
#include "z_zone.h"
#include "m_misc.h"
#include "m_cond.h"
#include "f_finale.h"
#include "lua_hud.h" // For Lua hud checks
#include "lua_hook.h" // For MobjDamage and ShouldDamage
#include "m_cheat.h" // objectplacing
#include "p_spec.h"
#include "k_kart.h"
#include "k_battle.h"
#include "k_boss.h"
#include "k_pwrlv.h"
#include "k_color.h"
#include "k_respawn.h"
#include "k_waypoint.h"
#include "k_bot.h"
#include "k_hud.h"
#include "k_terrain.h"
#include "k_director.h"
#include "k_collide.h"
#include "k_follower.h"
#include "k_objects.h"
#include "k_grandprix.h"
#include "k_specialstage.h"
#include "k_hud.h" // distribution debugger
#include "m_easing.h"
// Magic number distance for use with item roulette tiers
#define DISTVAR (2048)
// Distance when SPB can start appearing
#define SPBSTARTDIST (8*DISTVAR)
// Distance when SPB is forced onto the next person who rolls an item
#define SPBFORCEDIST (16*DISTVAR)
// Distance when the game stops giving you bananas
#define ENDDIST (14*DISTVAR)
// Consistent seed used for item reels
#define ITEM_REEL_SEED (0x22D5FAA8)
#define FRANTIC_ITEM_SCALE (FRACUNIT*6/5)
#define ROULETTE_SPEED_SLOWEST (20)
#define ROULETTE_SPEED_FASTEST (2)
#define ROULETTE_SPEED_DIST (150*DISTVAR)
#define ROULETTE_SPEED_TIMEATTACK (9)
#define ROULETTE_SPEED_VERSUS_SLOWEST (12)
static UINT32 K_DynamicItemOddsRace[NUMKARTRESULTS-1][2] =
{
// distance, duplication tolerance
{25, 10}, // sneaker
{63, 12}, // rocketsneaker
{60, 19}, // invincibility
{8, 4}, // banana
{3, 1}, // eggmark
{11, 4}, // orbinaut
{16, 4}, // jawz
{19, 4}, // mine
{1, 3}, // landmine
{25, 3}, // ballhog
{58, 6}, // selfpropelledbomb
{60, 7}, // grow
{70, 8}, // shrink
{1, 1}, // lightningshield
{25, 4}, // bubbleshield
{66, 9}, // flameshield
{1, 2}, // hyudoro
{0, 0}, // pogospring
{30, 8}, // superring (SPECIAL! distance value specifies when this can NO LONGER appear)
{0, 0}, // kitchensink
{1, 2}, // droptarget
{43, 5}, // gardentop
{0, 0}, // gachabom
{1, 2}, // stoneshoe
{1, 2}, // toxomister
{45, 6}, // dualsneaker
{55, 8}, // triplesneaker
{25, 2}, // triplebanana
{25, 1}, // tripleorbinaut
{35, 2}, // quadorbinaut
{30, 4}, // dualjawz
{0, 0}, // triplegachabom
};
static UINT32 K_DynamicItemOddsBattle[NUMKARTRESULTS-1][2] =
{
// distance, duplication tolerance
{20, 1}, // sneaker
{0, 0}, // rocketsneaker
{20, 1}, // invincibility
{0, 0}, // banana
{0, 0}, // eggmark
{10, 2}, // orbinaut
{12, 4}, // jawz
{13, 3}, // mine
{0, 0}, // landmine
{13, 3}, // ballhog
{0, 0}, // selfpropelledbomb
{15, 2}, // grow
{0, 0}, // shrink
{0, 0}, // lightningshield
{10, 1}, // bubbleshield
{0, 0}, // flameshield
{0, 0}, // hyudoro
{0, 0}, // pogospring
{0, 0}, // superring
{0, 0}, // kitchensink
{0, 0}, // droptarget
{0, 0}, // gardentop
{10, 5}, // gachabom
{0, 0}, // stoneshoe
{0, 0}, // toxomister
{0, 0}, // dualsneaker
{20, 1}, // triplesneaker
{0, 0}, // triplebanana
{10, 2}, // tripleorbinaut
{13, 3}, // quadorbinaut
{13, 3}, // dualjawz
{10, 2}, // triplegachabom
};
static UINT32 K_DynamicItemOddsSpecial[NUMKARTRESULTS-1][2] =
{
// distance, duplication tolerance
{15, 2}, // sneaker
{0, 0}, // rocketsneaker
{0, 0}, // invincibility
{0, 0}, // banana
{0, 0}, // eggmark
{20, 3}, // orbinaut
{15, 2}, // jawz
{0, 0}, // mine
{0, 0}, // landmine
{0, 0}, // ballhog
{70, 1}, // selfpropelledbomb
{0, 0}, // grow
{0, 0}, // shrink
{0, 0}, // lightningshield
{0, 0}, // bubbleshield
{0, 0}, // flameshield
{0, 0}, // hyudoro
{0, 0}, // pogospring
{0, 0}, // superring
{0, 0}, // kitchensink
{0, 0}, // droptarget
{0, 0}, // gardentop
{0, 0}, // gachabom
{0, 0}, // stoneshoe
{0, 0}, // toxomister
{35, 2}, // dualsneaker
{0, 0}, // triplesneaker
{0, 0}, // triplebanana
{35, 2}, // tripleorbinaut
{0, 0}, // quadorbinaut
{35, 2}, // dualjawz
{0, 0}, // triplegachabom
};
static UINT8 K_KartLegacyBattleOdds[NUMKARTRESULTS-1][2] =
{
{ 0, 1 }, // Sneaker
{ 0, 0 }, // Rocket Sneaker
{ 0, 1 }, // Invincibility
{ 0, 0 }, // Banana
{ 0, 0 }, // Eggman Monitor
{ 2, 0 }, // Orbinaut
{ 3, 1 }, // Jawz
{ 2, 1 }, // Mine
{ 0, 0 }, // Land Mine
{ 2, 1 }, // Ballhog
{ 0, 0 }, // Self-Propelled Bomb
{ 1, 1 }, // Grow
{ 0, 0 }, // Shrink
{ 0, 0 }, // Lightning Shield
{ 1, 0 }, // Bubble Shield
{ 0, 0 }, // Flame Shield
{ 0, 0 }, // Hyudoro
{ 0, 0 }, // Pogo Spring
{ 0, 0 }, // Super Ring
{ 0, 0 }, // Kitchen Sink
{ 0, 0 }, // Drop Target
{ 0, 0 }, // Garden Top
{ 5, 0 }, // Gachabom
{ 0, 1 }, // Stone Shoe
{ 0, 1 }, // Toxomister
{ 0, 0 }, // Sneaker x2
{ 0, 1 }, // Sneaker x3
{ 0, 0 }, // Banana x3
{ 2, 0 }, // Orbinaut x3
{ 2, 1 }, // Orbinaut x4
{ 2, 1 }, // Jawz x2
{ 2, 0 } // Gachabom x3
};
static kartitems_t K_KartItemReelSpecialEnd[] =
{
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelRingSneaker[] =
{
KITEM_SNEAKER,
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelSPBAttack[] =
{
KITEM_DROPTARGET,
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelBreakTheCapsules[] =
{
KITEM_GACHABOM,
KRITEM_TRIPLEGACHABOM,
KITEM_NONE
};
static kartitems_t K_KartItemReelBoss[] =
{
KITEM_GACHABOM,
KITEM_ORBINAUT,
KITEM_ORBINAUT,
KITEM_ORBINAUT,
KITEM_ORBINAUT,
KITEM_GACHABOM,
KITEM_ORBINAUT,
KITEM_ORBINAUT,
KITEM_ORBINAUT,
KITEM_NONE
};
static kartslotmachine_t K_KartItemReelRingBox[] =
{
KSM_BAR,
KSM_DOUBLEBAR,
KSM_TRIPLEBAR,
KSM_RING,
KSM_SEVEN,
KSM_JACKPOT,
KSM__MAX
};
static sfxenum_t ringboxsound[] =
{
sfx_slot00,
sfx_slot01,
sfx_slot02,
sfx_slot03,
sfx_slot04,
sfx_slot05
};
/*--------------------------------------------------
boolean K_ItemEnabled(kartitems_t item)
See header file for description.
--------------------------------------------------*/
boolean K_ItemEnabled(kartitems_t item)
{
if (item < 1 || item >= NUMKARTRESULTS)
{
// Not a real item.
return false;
}
if (K_CanChangeRules(true) == false)
{
// Force all items to be enabled.
return true;
}
// Allow the user preference.
return cv_items[item - 1].value;
}
/*--------------------------------------------------
boolean K_ItemSingularity(kartitems_t item)
See header file for description.
--------------------------------------------------*/
boolean K_ItemSingularity(kartitems_t item)
{
switch (item)
{
case KITEM_SPB:
case KITEM_SHRINK:
{
return true;
}
default:
{
return false;
}
}
}
/*--------------------------------------------------
botItemPriority_e K_GetBotItemPriority(kartitems_t result)
See header file for description.
--------------------------------------------------*/
botItemPriority_e K_GetBotItemPriority(kartitems_t result)
{
result = K_ItemResultToType(result);
switch (result)
{
case KITEM_SPB:
{
// Items that are intended to improve the game balance for everyone.
return BOT_ITEM_PR_SPB;
}
case KITEM_INVINCIBILITY:
case KITEM_GROW:
case KITEM_SHRINK:
{
// Items that drastically improve your own defense and/or speed.
return BOT_ITEM_PR_POWER;
}
case KITEM_SUPERRING:
{
// Items that get you out of ring debt.
return BOT_ITEM_PR_RINGDEBT;
}
case KITEM_SNEAKER:
case KITEM_ROCKETSNEAKER:
case KITEM_GARDENTOP:
case KITEM_POGOSPRING:
{
// Used when not in 1st place and relatively far from players.
// Items that give you speed with no protection.
return BOT_ITEM_PR_SPEED;
}
case KITEM_HYUDORO:
case KITEM_LANDMINE:
case KITEM_DROPTARGET:
case KITEM_EGGMAN:
case KITEM_GACHABOM:
case KITEM_STONESHOE:
case KITEM_TOXOMISTER:
case KITEM_KITCHENSINK:
{
// Used when in 1st place and relatively far from players.
// Typically attack items that don't give you protection.
return BOT_ITEM_PR_FRONTRUNNER;
}
case KITEM_ORBINAUT:
case KITEM_BALLHOG:
case KITEM_JAWZ:
case KITEM_BANANA:
case KITEM_MINE:
case KITEM_LIGHTNINGSHIELD:
case KITEM_BUBBLESHIELD:
case KITEM_FLAMESHIELD:
{
// Used in all other instances (close to other players, no priority override)
// Typically attack items that give you protection.
return BOT_ITEM_PR_NEUTRAL;
}
default:
{
return BOT_ITEM_PR__FALLBACK;
}
}
}
/*--------------------------------------------------
static fixed_t K_ItemOddsScale(UINT8 playerCount)
See header file for description.
--------------------------------------------------*/
fixed_t K_ItemOddsScale(UINT8 playerCount)
{
const UINT8 basePlayer = 8; // The player count we design most of the game around.
fixed_t playerScaling = 0;
if (playerCount < 2)
{
// Cap to 1v1 scaling
playerCount = 2;
}
// 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 (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.
// 16P: x0.75
playerScaling = (basePlayer - playerCount) * (FRACUNIT / 32);
}
return playerScaling;
}
/*--------------------------------------------------
UINT32 K_UndoMapScaling(UINT32 distance)
Takes a raw map distance and adjusts it to
be in x1 scale.
Input Arguments:-
distance - Original distance.
Return:-
Distance unscaled by mapobjectscale.
--------------------------------------------------*/
UINT32 K_UndoMapScaling(UINT32 distance)
{
if (mapobjectscale != FRACUNIT)
{
// Bring back to normal scale.
return FixedDiv(distance, mapobjectscale);
}
return distance;
}
/*--------------------------------------------------
UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers)
Adjust item distance for lobby-size scaling
as well as Frantic Items.
Input Arguments:-
player - The player to get the distance of.
distance - Original distance.
numPlayers - Number of players in the game.
Return:-
New distance after scaling.
--------------------------------------------------*/
UINT32 K_ScaleItemDistance(INT32 distance, UINT8 numPlayers)
{
#if 0
if (franticitems == true)
{
// Frantic items pretends everyone's farther apart, for crazier items.
distance = FixedMul(distance, FRANTIC_ITEM_SCALE);
}
#endif
// Items get crazier with the fewer players that you have.
distance = FixedMul(
distance,
FRACUNIT + (K_ItemOddsScale(numPlayers) / 2)
);
return distance;
}
static UINT32 K_GetUnscaledFirstDistance(const player_t *player)
{
UINT32 pdis = 0;
if (player == NULL)
{
return 0;
}
if (specialstageinfo.valid == true)
{
UINT32 ufoDis = K_GetSpecialUFODistance();
if (player->distancetofinish <= ufoDis)
{
// You're ahead of the UFO.
pdis = 0;
}
else
{
// Subtract the UFO's distance from your distance!
pdis = player->distancetofinish - ufoDis;
}
}
else
{
UINT8 i;
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;
}
}
}
pdis = K_UndoMapScaling(pdis);
return pdis;
}
/*--------------------------------------------------
static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers)
See header file for description.
--------------------------------------------------*/
UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers)
{
UINT32 pdis = K_GetUnscaledFirstDistance(player);
pdis = K_ScaleItemDistance(pdis, numPlayers);
if (player->bot && (player->botvars.rival || cv_levelskull.value))
{
// Rival has better odds :)
pdis = FixedMul(pdis, FRANTIC_ITEM_SCALE);
}
return pdis;
}
/*--------------------------------------------------
boolean K_DenyShieldOdds(kartitems_t item)
See header file for description.
--------------------------------------------------*/
boolean K_DenyShieldOdds(kartitems_t item)
{
const INT32 shieldType = K_GetShieldFromItem(item);
size_t i;
if ((gametyperules & GTR_CIRCUIT) == 0)
{
return false;
}
if (shieldType == KSHIELD_NONE)
{
return false;
}
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false || players[i].spectator == true)
{
continue;
}
if (shieldType == K_GetShieldFromItem(players[i].itemtype))
{
// Don't allow more than one of each shield type at a time
return true;
}
}
return false;
}
static boolean K_DenyAutoRouletteOdds(kartitems_t item)
{
// Deny items that are too hard for newbies
switch (item)
{
case KITEM_GARDENTOP:
return true;
default:
return false;
}
}
/*--------------------------------------------------
static fixed_t K_PercentSPBOdds(const itemroulette_t *roulette, UINT8 position)
Provide odds of SPB according to distances of first and
second place players.
Input Arguments:-
roulette - The roulette data that we intend to
insert this item into.
position - Position of player to consider for these
odds.
Return:-
New item odds.
--------------------------------------------------*/
static fixed_t K_PercentSPBOdds(const itemroulette_t *roulette, UINT8 position)
{
I_Assert(roulette != NULL);
if (roulette->firstDist < ENDDIST*2 // No SPB when 1st is almost done
|| position == 1) // No SPB for 1st ever
{
return 0;
}
else
{
const UINT32 dist = max(0, ((signed)roulette->secondToFirst) - SPBSTARTDIST);
const UINT32 distRange = SPBFORCEDIST - SPBSTARTDIST;
fixed_t multiplier = FixedDiv(dist, distRange);
if (multiplier < 0)
{
multiplier = 0;
}
if (multiplier > FRACUNIT)
{
multiplier = FRACUNIT;
}
return multiplier;
}
}
/*--------------------------------------------------
INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item)
See header file for description.
--------------------------------------------------*/
INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item)
{
(void)player;
I_Assert(item > KITEM_NONE); // too many off by one scenarioes.
I_Assert(item < NUMKARTRESULTS);
I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table
fixed_t newOdds = K_KartLegacyBattleOdds[item-1][pos];
newOdds <<= FRACBITS;
return newOdds;
}
/*--------------------------------------------------
static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette)
See header file for description.
--------------------------------------------------*/
boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette)
{
if (K_ItemEnabled(KITEM_SPB) == false)
{
return false;
}
if (!(gametyperules & GTR_CIRCUIT))
{
return false;
}
if (specialstageinfo.valid == true)
{
return false;
}
if (player == NULL)
{
return false;
}
if (player->position <= 1)
{
return false;
}
if (spbplace != -1)
{
return false;
}
if (itemCooldowns[KITEM_SPB - 1] > 0)
{
return false;
}
#if 0
if (roulette->playing <= 2)
{
return false;
}
#endif
return (roulette->secondToFirst >= SPBFORCEDIST);
}
/*--------------------------------------------------
static void K_InitRoulette(itemroulette_t *const roulette)
Initializes the data for a new item roulette.
Input Arguments:-
roulette - The item roulette data to initialize.
Return:-
N/A
--------------------------------------------------*/
static void K_InitRoulette(itemroulette_t *const roulette)
{
size_t i;
#ifndef ITEM_LIST_SIZE
if (roulette->itemList.items == NULL)
{
roulette->itemList.cap = 32;
roulette->itemList.items = Z_Calloc(
sizeof(SINT8) * roulette->itemList.cap,
PU_STATIC,
NULL
);
if (roulette->itemList.items == NULL)
{
I_Error("Not enough memory for item roulette list\n");
}
}
#endif
roulette->itemList.len = 0;
roulette->index = 0;
roulette->baseDist = roulette->dist = 0;
roulette->playing = roulette->exiting = 0;
roulette->firstDist = roulette->secondDist = UINT32_MAX;
roulette->secondToFirst = 0;
roulette->elapsed = 0;
roulette->tics = roulette->speed = ROULETTE_SPEED_TIMEATTACK; // Some default speed
roulette->active = true;
roulette->eggman = false;
roulette->ringbox = false;
roulette->autoroulette = false;
roulette->popcorn = 1;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false || players[i].spectator == true)
{
continue;
}
roulette->playing++;
if (players[i].exiting)
{
roulette->exiting++;
}
if (specialstageinfo.valid == true)
{
UINT32 dis = K_UndoMapScaling(players[i].distancetofinish);
if (dis < roulette->secondDist)
{
roulette->secondDist = dis;
}
}
else
{
if (players[i].position == 1)
{
roulette->firstDist = K_UndoMapScaling(players[i].distancetofinish);
}
if (players[i].position == 2)
{
roulette->secondDist = K_UndoMapScaling(players[i].distancetofinish);
}
}
}
if (specialstageinfo.valid == true)
{
roulette->firstDist = K_UndoMapScaling(K_GetSpecialUFODistance());
}
// Calculate 2nd's distance from 1st, for SPB
if (roulette->firstDist != UINT32_MAX && roulette->secondDist != UINT32_MAX
&& roulette->secondDist > roulette->firstDist)
{
roulette->secondToFirst = roulette->secondDist - roulette->firstDist;
roulette->secondToFirst = K_ScaleItemDistance(roulette->secondToFirst, 16 - roulette->playing); // Reversed scaling
}
}
/*--------------------------------------------------
void K_PushToRouletteItemList(itemroulette_t *const roulette, INT32 item)
See header file for description.
--------------------------------------------------*/
void K_PushToRouletteItemList(itemroulette_t *const roulette, INT32 item)
{
#ifdef ITEM_LIST_SIZE
if (roulette->itemList.len >= ITEM_LIST_SIZE)
{
I_Error("Out of space for item reel! Go and make ITEM_LIST_SIZE bigger I guess?\n");
return;
}
#else
I_Assert(roulette->itemList.items != NULL);
if (!roulette->ringbox && item >= NUMKARTRESULTS)
{
CONS_Alert(CONS_WARNING, M_GetText("Item Roulette rejected an out-of-range item.\n"));
return;
}
if (roulette->ringbox && item >= KSM__MAX)
{
CONS_Alert(CONS_WARNING, M_GetText("Casino Roulette rejected an out-of-range item.\n"));
return;
}
if (roulette->itemList.len >= roulette->itemList.cap)
{
roulette->itemList.cap *= 2;
roulette->itemList.items = Z_Realloc(
roulette->itemList.items,
sizeof(SINT8) * roulette->itemList.cap,
PU_STATIC,
NULL
);
if (roulette->itemList.items == NULL)
{
I_Error("Not enough memory for item roulette list\n");
}
}
#endif
roulette->itemList.items[ roulette->itemList.len ] = item;
roulette->itemList.len++;
}
/*--------------------------------------------------
void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item)
See header file for description.
--------------------------------------------------*/
void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item)
{
if (player && K_PlayerUsesBotMovement(player) && !K_BotUnderstandsItem(item))
return;
K_PushToRouletteItemList(roulette, item);
if (player == NULL)
{
return;
}
// If we're in ring debt, pad out the reel with
// a BUNCH of Super Rings.
if (K_ItemEnabled(KITEM_SUPERRING) == true
&& player->rings <= -10
&& player->position == 1
&& (gametyperules & GTR_SPHERES) == 0)
{
K_PushToRouletteItemList(roulette, KITEM_SUPERRING);
}
}
/*--------------------------------------------------
void K_CalculateRouletteSpeed(itemroulette_t *const roulette)
See header file for description.
--------------------------------------------------*/
void K_CalculateRouletteSpeed(itemroulette_t *const roulette)
{
fixed_t frontRun = 0;
fixed_t progress = 0;
fixed_t total = 0;
if (K_CheckBossIntro() == true)
{
// Boss in action, use a speed controlled by boss health
total = FixedDiv(bossinfo.healthbar, BOSSHEALTHBARLEN);
roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_VERSUS_SLOWEST - ROULETTE_SPEED_FASTEST, total);
return;
}
if (roulette->autoroulette == true)
{
roulette->speed = ROULETTE_SPEED_FASTEST;
return;
}
if (K_TimeAttackRules() == true && !(modeattacking & ATTACKING_SPB))
{
// Time Attack rules; use a consistent speed.
roulette->tics = roulette->speed = ROULETTE_SPEED_TIMEATTACK;
return;
}
if (roulette->baseDist > ENDDIST)
{
// Being farther in the course makes your roulette faster.
progress = min(FRACUNIT, FixedDiv(roulette->baseDist - ENDDIST, ROULETTE_SPEED_DIST));
}
if (roulette->baseDist > roulette->firstDist)
{
// Frontrunning makes your roulette faster.
frontRun = min(FRACUNIT, FixedDiv(roulette->baseDist - roulette->firstDist, ENDDIST));
}
// Combine our two factors together.
total = min(FRACUNIT, (frontRun / 2) + (progress / 2));
if (leveltime < starttime + 30*TICRATE)
{
// Don't impact as much at the start.
// This makes it so that everyone gets to enjoy the lowest speed at the start.
if (leveltime < starttime)
{
total = FRACUNIT;
}
else
{
const fixed_t lerp = FixedDiv(leveltime - starttime, 30*TICRATE);
total = FRACUNIT + FixedMul(lerp, total - FRACUNIT);
}
}
roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_SLOWEST - ROULETTE_SPEED_FASTEST, total);
}
// Honestly, the "power item" class is kind of a vestigial concept,
// but we'll faithfully port it over since it's not hurting anything so far
// (and it's at least ostensibly a Rival balancing mechanism, wheee).
static boolean K_IsItemPower(kartitems_t item)
{
switch (item)
{
case KITEM_ROCKETSNEAKER:
case KITEM_JAWZ:
case KITEM_LANDMINE:
case KITEM_DROPTARGET:
case KITEM_BALLHOG:
case KRITEM_TRIPLESNEAKER:
case KRITEM_TRIPLEORBINAUT:
case KRITEM_QUADORBINAUT:
case KRITEM_DUALJAWZ:
case KITEM_HYUDORO:
case KRITEM_TRIPLEBANANA:
case KITEM_FLAMESHIELD:
case KITEM_GARDENTOP:
case KITEM_SHRINK:
case KITEM_LIGHTNINGSHIELD:
return true;
default:
return false;
}
}
static boolean K_IsItemFirstOnly(kartitems_t item)
{
switch (item)
{
case KITEM_LANDMINE:
case KITEM_LIGHTNINGSHIELD:
case KITEM_HYUDORO:
case KITEM_DROPTARGET:
case KITEM_STONESHOE:
case KITEM_TOXOMISTER:
return true;
default:
return false;
}
}
static boolean K_IsItemFirstPermitted(kartitems_t item)
{
if (K_IsItemFirstOnly(item))
return true;
switch (item)
{
case KITEM_BANANA:
case KITEM_EGGMAN:
case KITEM_ORBINAUT:
case KITEM_SUPERRING:
return true;
default:
return false;
}
}
ATTRUNUSED static boolean K_IsItemUselessAlone(kartitems_t item)
{
switch (item)
{
case KITEM_JAWZ:
case KRITEM_DUALJAWZ:
case KITEM_LIGHTNINGSHIELD:
case KITEM_ORBINAUT:
case KRITEM_TRIPLEORBINAUT:
case KRITEM_QUADORBINAUT:
case KITEM_BALLHOG:
case KITEM_BUBBLESHIELD: // shhhhhh
return true;
default:
return false;
}
}
static boolean K_IsItemSpeed(kartitems_t item)
{
switch (item)
{
case KITEM_ROCKETSNEAKER:
case KITEM_GROW:
case KITEM_INVINCIBILITY:
case KITEM_SNEAKER:
case KRITEM_DUALSNEAKER:
case KRITEM_TRIPLESNEAKER:
case KITEM_FLAMESHIELD:
case KITEM_SHRINK:
case KITEM_SUPERRING:
return true;
default:
return false;
}
}
static fixed_t K_RequiredXPForItem(kartitems_t item)
{
switch (item)
{
case KITEM_GARDENTOP:
case KITEM_SHRINK:
return FRACUNIT; // "Base" item odds
default:
return 0;
}
}
// Which items are disallowed for this player's specific placement?
static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player)
{
if (!(gametyperules & GTR_CIRCUIT))
return true;
if (specialstageinfo.valid == true)
return true;
if (player->position == 1)
return K_IsItemFirstPermitted(item);
else
{
// A little inelegant: filter the most chaotic items from courses with early sets and tight layouts.
if (K_IsItemPower(item) && (leveltime < ((15*TICRATE) + starttime)))
return false;
// GIGA power items reserved only for players who were doing great and died.
if (K_EffectiveGradingFactor(player) < K_RequiredXPForItem(item))
return false;
// Expert items are G2+ only, no Top in Relaxed!
if (K_RequiredXPForItem(item) >= FRACUNIT && gamespeed == KARTSPEED_EASY)
return false;
return !K_IsItemFirstOnly(item);
}
}
// Which items are disallowed because it's the wrong time for them?
static boolean K_TimingPermitsItem(kartitems_t item, const itemroulette_t *roulette)
{
if (!(gametyperules & GTR_CIRCUIT))
return true;
if (specialstageinfo.valid == true)
return true;
boolean notNearEnd = false;
boolean cooldownOnStart = false;
switch (item)
{
case KITEM_BANANA:
case KITEM_EGGMAN:
case KITEM_SUPERRING:
{
notNearEnd = true;
break;
}
case KITEM_HYUDORO:
case KRITEM_TRIPLEBANANA:
{
notNearEnd = true;
break;
}
case KITEM_INVINCIBILITY:
case KITEM_MINE:
case KITEM_GROW:
case KITEM_BUBBLESHIELD:
{
cooldownOnStart = true;
break;
}
case KITEM_FLAMESHIELD:
case KITEM_GARDENTOP:
{
cooldownOnStart = true;
notNearEnd = true;
break;
}
case KITEM_SPB:
{
// In Race, we reintroduce and reenable this item to counter breakaway frontruns.
// No need to roll it if that's not the case.
return false;
break;
}
case KITEM_SHRINK:
{
cooldownOnStart = true;
notNearEnd = true;
break;
}
case KITEM_LIGHTNINGSHIELD:
{
cooldownOnStart = true;
if ((gametyperules & GTR_CIRCUIT) && spbplace != -1)
{
return false;
}
break;
}
default:
break;
}
if (cooldownOnStart && (leveltime < ((30*TICRATE) + starttime)))
return false;
if (notNearEnd && (roulette != NULL && roulette->baseDist < ENDDIST))
return false;
return true;
}
static void K_FixEmptyRoulette(const player_t *player, itemroulette_t *const roulette)
{
if (roulette->itemList.len > 0)
return;
if (K_PlayerUsesBotMovement(player)) // Bots can't use certain items. Give them _something_.
K_PushToRouletteItemList(roulette, KITEM_SUPERRING);
else // Players can use all items, so this should never happen.
K_PushToRouletteItemList(roulette, KITEM_SAD);
}
/*--------------------------------------------------
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox)
See header file for description.
--------------------------------------------------*/
void K_FillItemRoulette(player_t *const player, itemroulette_t *const roulette, boolean ringbox)
{
K_InitRoulette(roulette);
if (player != NULL)
{
roulette->baseDist = K_UndoMapScaling(player->distancetofinish);
if (player->pflags & PF_AUTOROULETTE)
roulette->autoroulette = true;
K_CalculateRouletteSpeed(roulette);
}
// Lua may want to intercept reelbuilder entirely.
LUA_HookPreFillItemRoulette(player, roulette, ringbox);
// If prehook did something, no need to continue.
if (roulette->itemList.len != 0) {
return;
}
K_FillItemRouletteData(player, roulette, ringbox, false);
// Lua can modify the final result.
LUA_HookFillItemRoulette(player, roulette, ringbox);
// If somehow there's no items, add sad.
if (roulette->itemList.len == 0) {
if (roulette->ringbox)
K_PushToRouletteItemList(roulette, KSM_BAR);
else
K_AddItemToReel(player, roulette, KITEM_SAD);
}
}
/*--------------------------------------------------
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun)
See header file for description.
--------------------------------------------------*/
void K_FillItemRouletteData(player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun)
{
UINT32 spawnChance[NUMKARTRESULTS] = {0};
UINT32 totalSpawnChance = 0;
size_t rngRoll = 0;
UINT8 numItems = 0;
kartitems_t singleItem = KITEM_SAD;
size_t i, j;
if (roulette->itemList.items == NULL)
{
K_InitRoulette(roulette);
}
if (ringbox == true)
{
// If this is being invoked by a Ring Box, it should literally never produce items.
kartslotmachine_t *presetlist = K_KartItemReelRingBox;
roulette->ringbox = true;
for (i = 0; presetlist[i] != KSM__MAX; i++)
{
K_PushToRouletteItemList(roulette, presetlist[i]);
}
return;
}
// SPECIAL CASE No. 1:
// Give only the debug item if specified
if (cv_kartdebugitem.value != KITEM_NONE)
{
K_PushToRouletteItemList(roulette, cv_kartdebugitem.value);
return;
}
// SPECIAL CASE No. 2:
// Use a special, pre-determined item reel for Time Attack / Free Play / End of Sealed Stars
if (specialstageinfo.valid)
{
if (K_GetPossibleSpecialTarget() == NULL)
{
for (i = 0; K_KartItemReelSpecialEnd[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelSpecialEnd[i]);
}
return;
}
}
else if (K_CheckBossIntro() == true)
{
for (i = 0; K_KartItemReelBoss[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelBoss[i]);
}
return;
}
else if (K_TimeAttackRules() == true)
{
kartitems_t *presetlist = NULL;
// If the objective is not to go fast, it's to cause serious damage.
if (battleprisons == true)
{
presetlist = K_KartItemReelBreakTheCapsules;
}
else if (modeattacking & ATTACKING_SPB)
{
presetlist = K_KartItemReelSPBAttack;
}
else if (K_CanChangeRules(true) == false) // GT_TUTORIAL, time attack
{
presetlist = K_KartItemReelRingSneaker;
}
if (presetlist != NULL)
{
for (i = 0; presetlist[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, presetlist[i]);
}
}
else
{
// New FREE PLAY behavior;
// every item in the game!
// Create the same item reel given the same inputs.
P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED);
for (i = 1; i < NUMKARTRESULTS; i++)
{
if (K_ItemEnabled(i) == true)
{
spawnChance[i] = ( totalSpawnChance += 1 );
}
}
while (totalSpawnChance > 0)
{
rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance);
for (i = 1; i < NUMKARTRESULTS && spawnChance[i] <= rngRoll; i++)
{
continue;
}
K_PushToRouletteItemList(roulette, i);
for (; i < NUMKARTRESULTS; i++)
{
// Be sure to fix the remaining items' odds too.
if (spawnChance[i] > 0)
{
spawnChance[i]--;
}
}
totalSpawnChance--;
}
}
return;
}
// SPECIAL CASE No. 3:
// Only give the SPB if conditions are right
if (K_ForcedSPB(player, roulette) == true)
{
K_AddItemToReel(player, roulette, KITEM_SPB);
K_FixEmptyRoulette(player, roulette);
return;
}
// SPECIAL CASE No. 4:
// If only one item is enabled, always use it
for (i = 1; i < NUMKARTRESULTS; i++)
{
if (K_ItemEnabled(i) == true)
{
numItems++;
if (numItems > 1)
{
break;
}
singleItem = i;
}
}
if (numItems < 2)
{
// singleItem = KITEM_SAD by default,
// so it will be used when all items are turned off.
K_AddItemToReel(player, roulette, singleItem);
K_FixEmptyRoulette(player, roulette);
return;
}
// Special cases are all handled, we can now
// actually calculate actual item reels.
roulette->preexpdist = K_GetItemRouletteDistance(player, roulette->playing);
roulette->dist = roulette->preexpdist;
// ===============================================================================
// Dynamic Roulette. Oh boy!
// Alright, here's the broad plan:
// 1: Determine what items are permissible
// 2: Determine the permitted item that's most appropriate for our distance from leader
// 3: Pick that item, then penalize it so it's less likely to be repicked
// 4: Repeat 3 until we've picked enough stuff
// 5: Skim any items that are much weaker than the reel's average out of the roulette
// 6: Cram it all in
UINT32 powers[NUMKARTRESULTS]; // how strong is each item? think of this as a "target distance" for this item to spawn at
UINT32 deltas[NUMKARTRESULTS]; // how different is that strength from target?
UINT32 candidates[NUMKARTRESULTS]; // how many of this item should we try to insert?
UINT32 dupetolerance[NUMKARTRESULTS]; // how willing are we to select this item after already selecting it? higher values = lower dupe penalty
boolean permit[NUMKARTRESULTS]; // is this item allowed?
UINT32 lonelinessSuppressor = DISTVAR; // This close to 1st? Dampen loneliness (you have a target!)
UINT32 maxEXPDistanceCut = 3*DISTVAR; // The maximum amount you can be displaced by EXP
// If we're too close to 1st in absolute units, crush our top-end item odds down.
fixed_t crowdingFirst = 0;
if (player->position != 1)
crowdingFirst = FixedRescale(K_GetUnscaledFirstDistance(player), 0, 4*DISTVAR, Easing_InCubic, FRACUNIT, 0);
if ((gametyperules & GTR_CIRCUIT) && !K_Cooperative())
{
roulette->dist = FixedMul(roulette->preexpdist, K_EffectiveGradingFactor(player));
if (roulette->dist < roulette->preexpdist)
{
if (roulette->preexpdist - roulette->dist > maxEXPDistanceCut)
{
roulette->dist = roulette->preexpdist - maxEXPDistanceCut;
}
}
}
fixed_t largegamescaler = roulette->playing * 8 + 100; // Spread out item odds in large games for a less insane experience.
if (franticitems)
largegamescaler = 100; // Except in Frantic, where you know what you're getting
UINT32 targetpower = 100 * roulette->dist / largegamescaler; // fill roulette with items around this value!
if (!(specialstageinfo.valid))
targetpower = Easing_Linear(crowdingFirst, targetpower, targetpower/2);
boolean rival = (player->bot && (player->botvars.rival || cv_levelskull.value));
boolean filterweakitems = true; // strip unusually weak items from reel?
UINT8 reelsize = 15; // How many items to attempt to add in prepass?
UINT32 humanscaler = 250; // Scaler that converts "useodds" style distances in odds tables to raw distances. Affects general item distance scale.
// == ARE THESE ITEMS ALLOWED?
// We have a fuckton of rules about when items are allowed to show up,
// like limiting trap items at the end of the race, limiting strong
// items at the start of the race... Dynamic stuff, not always trivial.
// We're about to do a bunch of work with items, so let's cache them all.
for (i = 1; i < NUMKARTRESULTS; i++)
{
if (!K_TimingPermitsItem(i, roulette))
permit[i] = false;
else if (!K_ShouldPlayerAllowItem(i, player))
permit[i] = false;
else if (K_GetItemCooldown(i))
permit[i] = false;
else if (!K_ItemEnabled(i))
permit[i] = false;
else if (K_DenyShieldOdds(i))
permit[i] = false;
else if (roulette && roulette->autoroulette == true && K_DenyAutoRouletteOdds(i))
permit[i] = false;
else
permit[i] = true;
}
// == ODDS TIME
// Set up the right item odds for the gametype we're in.
UINT32 maxpower = 0; // Clamp target power to the lowest item that exists, or some of the math gets hard to reason about.
for (i = 1; i < NUMKARTRESULTS; i++)
{
// NOTE: Battle odds are underspecified, we don't invoke roulettes in this mode!
if (gametyperules & GTR_BUMPERS)
{
powers[i] = humanscaler * K_DynamicItemOddsBattle[i-1][0];
dupetolerance[i] = K_DynamicItemOddsBattle[i-1][1];
filterweakitems = false;
}
else if (specialstageinfo.valid == true)
{
powers[i] = humanscaler * K_DynamicItemOddsSpecial[i-1][0];
dupetolerance[i] = K_DynamicItemOddsSpecial[i-1][1];
reelsize = 8; // Smaller roulette in Special because there are much fewer standard items.
filterweakitems = false;
}
else
{
powers[i] = humanscaler * K_DynamicItemOddsRace[i-1][0];
dupetolerance[i] = K_DynamicItemOddsRace[i-1][1];
// Bias towards attack items when close to the leader, gotta work for the slingshot pass!
if (K_IsItemSpeed(i) && i != KITEM_SUPERRING)
powers[i] = Easing_Linear(crowdingFirst, powers[i], 2*powers[i]);
}
maxpower = max(maxpower, powers[i]);
}
targetpower = min(maxpower, targetpower); // Make sure that we don't fall out of the bottom of the odds table.
// == GTFO WEIRD ITEMS
// If something is set to distance 0 in its odds table, that means the item
// is completely ineligible for the gametype we're in, and should never be selected.
for (i = 1; i < NUMKARTRESULTS; i++)
{
if (powers[i] == 0)
{
permit[i] = false;
}
}
// == REEL CANDIDATE PREP
// Dynamic Roulette works by comparing an item's "ideal" distance to our current distance from 1st.
// It'll pick the most suitable item, do some math, then move on to the next most suitable item.
// Calculate starting deltas and clear out the "candidates" array that stores what we pick.
for (i = 1; i < NUMKARTRESULTS; i++)
{
candidates[i] = 0;
deltas[i] = min(targetpower - powers[i], powers[i] - targetpower);
}
// == "POPCORN" Super Ring in Race.
// This can appear anywhere from 0 to its specified distance, to pad the
// reels with non-disruptive catchup (since we have a ton of offensive items
// and not many front/mid speed items).
boolean canfiltersuperring = true;
if ((gametyperules & GTR_CIRCUIT) && (specialstageinfo.valid == false) && K_ItemEnabled(KITEM_SUPERRING))
{
if (targetpower > powers[KITEM_SUPERRING])
{
permit[KITEM_SUPERRING] = false;
}
else
{
permit[KITEM_SUPERRING] = true;
deltas[KITEM_SUPERRING] = 0;
canfiltersuperring = false;
roulette->popcorn = (player->position > 1) ? max(1, targetpower/humanscaler/3) : 1;
}
}
// == LONELINESS DETECTION
// A lot of items suck if no players are nearby to interact with them.
// Should we bias towards items that get us back to the action?
// This will set the "loneliness" percentage to be used later.
UINT32 lonelinessThreshold = 4*DISTVAR; // How far away can we be before items are considered useless?
UINT32 toFront = lonelinessThreshold; // Distance to the player trying to kill us.
UINT32 toBack = lonelinessThreshold; // Distance to the player we are trying to kill.
fixed_t loneliness = 0;
if (player->position > 1) // Loneliness is expected when frontrunnning, don't influence their item table.
{
if ((gametyperules & GTR_CIRCUIT) && specialstageinfo.valid == false)
{
player_t *front = NULL;
player_t *back = NULL;
// Find the closest enemy players ahead of and behind us.
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false || players[i].spectator == true || players[i].exiting || G_SameTeam(&players[i], player))
continue;
player_t *check = &players[i];
if (check->distancetofinish > player->distancetofinish)
{
if (!back || check->distancetofinish < back->distancetofinish)
back = check;
}
else if (check->distancetofinish < player->distancetofinish)
{
if (!front || check->distancetofinish > front->distancetofinish)
front = check;
}
}
if (front)
toFront = K_UndoMapScaling(player->distancetofinish - front->distancetofinish);
if (back)
toBack = K_UndoMapScaling(back->distancetofinish - player->distancetofinish);
}
// Your relationship to each closest player counts for half, but will be eased later.
// If you're far from an attacker but close to a defender, that Ballhog is still useful!
loneliness += min(FRACUNIT/2, FRACUNIT * toFront / lonelinessThreshold / 2);
loneliness += min(FRACUNIT/2, FRACUNIT * toBack / lonelinessThreshold / 2);
loneliness = Easing_InCubic(loneliness, 0, FRACUNIT);
// You are not lonely if you're super close to 1st, even if 3nd is far away.
if (roulette->preexpdist < lonelinessSuppressor)
{
loneliness = FixedRescale(roulette->preexpdist, 0, lonelinessSuppressor, Easing_InCubic, 0, loneliness);
}
// Give interaction items a nudge against initial selection if you're lonely..
for (i = 1; i < NUMKARTRESULTS; i++)
{
if (!K_IsItemSpeed(i))
{
deltas[i] = Easing_Linear(loneliness, deltas[i], deltas[i] + (4*DISTVAR));
}
}
}
// == INTRODUCE TRYHARD-EATING PREDATOR
// If the frontrunner's making a major breakaway, "break the rules"
// and insert the SPB into the roulette. This doesn't have to be
// incredibly forceful; there's a truly forced special case above.
fixed_t spb_odds = K_PercentSPBOdds(roulette, player->position);
if ((gametyperules & GTR_CIRCUIT)
&& specialstageinfo.valid == false
&& (spb_odds > 0) & (spbplace == -1)
&& (roulette->preexpdist >= powers[KITEM_SPB]) // SPECIAL CASE: Check raw distance instead of EXP-influenced target distance.
&& !K_GetItemCooldown(KITEM_SPB)
&& K_ItemEnabled(KITEM_SPB))
{
// When reenabling the SPB, we also adjust its delta to ensure that it has good odds of showing up.
// Players who are _seriously_ struggling are more likely to see Invinc or Rockets, since those items
// have a lower target distance, so we nudge the SPB towards them.
permit[KITEM_SPB] = true;
deltas[KITEM_SPB] = Easing_Linear(spb_odds, deltas[KITEM_SPB], 0);
}
// == ITEM SELECTION
// All the prep work's done: let's pick out a sampler platter of items until we fill the reel.
UINT8 added = 0; // How many items added so far?
UINT32 totalreelpower = 0; // How much total item power in the reel? Used for an average later.
UINT32 basepenalty = 4*DISTVAR; // How much to penalize repicked items, to ensure item variety.
// BUT, keep the item distribution tighter if we're close to the frontrunner...
UINT32 penalty = Easing_Linear(crowdingFirst, basepenalty, basepenalty/2);
if (player->position == 1) // ...unless we ARE the frontrunner.
penalty = basepenalty;
for (i = 0; i < reelsize; i++)
{
UINT32 lowestdelta = INT32_MAX;
size_t bestitem = 0;
// Each rep, get the legal item with the lowest delta...
for (j = 1; j < NUMKARTRESULTS; j++)
{
if (!permit[j])
continue;
if (lowestdelta > deltas[j])
{
bestitem = j;
lowestdelta = deltas[j];
}
}
// Couldn't find any eligible items at all? GTFO.
// (This should never trigger, but you never know with the item switch menu.)
if (bestitem == 0)
break;
// Impose a penalty to this item's delta, to bias against selecting it again.
// This is naively slashed by an item's "duplicate tolerance":
// lower tolerance means that an item is less likely to be reselected (it's "rarer").
UINT32 deltapenalty = penalty*(1+candidates[bestitem])/dupetolerance[bestitem];
// Power items get better odds in frantic, or if you're the rival.
// (For the rival, this is way more likely to matter at lower skills, where they're
// worse at selecting their item—but it always matters in frantic gameplay.)
if (K_IsItemPower(bestitem) && rival)
deltapenalty = 3 * deltapenalty / 4;
#if 0
if (K_IsItemPower(bestitem) && franticitems)
deltapenalty = 3 * deltapenalty / 4;
#endif
// Conversely, if we're lonely, try not to reselect an item that wouldn't be useful to us
// without any players to use it on.
if (!K_IsItemSpeed(bestitem))
deltapenalty = Easing_Linear(loneliness, deltapenalty, 3*deltapenalty);
// Draw complex odds debugger. This one breaks down all the calcs in order.
if (cv_kartdebugdistribution.value > 1)
{
UINT16 BASE_X = 18;
UINT16 BASE_Y = 5+12*i;
INT32 FLAGS = V_SNAPTOTOP|V_SNAPTOLEFT;
V_DrawRightAlignedThinString(BASE_X + 35, BASE_Y, FLAGS, va("P%d", powers[bestitem]/humanscaler));
V_DrawRightAlignedThinString(BASE_X + 65, BASE_Y, FLAGS, va("D%d", deltas[bestitem]/humanscaler));
V_DrawRightAlignedThinString(BASE_X + 20, BASE_Y, FLAGS, va("%d", dupetolerance[bestitem]));
V_DrawFixedPatch(BASE_X*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(bestitem), NULL);
UINT8 amount = K_ItemResultToAmount(bestitem, roulette);
if (amount > 1)
V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount));
}
// Add the selected item to our list of candidates and update its working delta.
candidates[bestitem]++;
deltas[bestitem] += deltapenalty;
// Then update our ongoing average of the reel's power.
totalreelpower += powers[bestitem];
added++;
}
// No items?!
if (added == 0)
{
// Guess we're making circles now.
// Just do something that doesn't crash.
K_AddItemToReel(player, roulette, singleItem);
return;
}
// Frontrunner roulette is precise, no need to filter it.
if (player->position <= 1)
filterweakitems = false;
UINT8 debugcount = 0; // For the "simple" odds debugger.
UINT32 meanreelpower = totalreelpower/max(added, 1); // Average power for the "moth filter".
// == PREP FOR ADDING TO THE ROULETTE REEL
// Sal's prior work for this is rock-solid.
// This fills the spawnChance array with a rolling count of items,
// so that we can loop upward through it until we hit our random index.
for (i = 1; i < NUMKARTRESULTS; i++)
{
// If an item is far too week for this reel, reject it.
// This can happen in regions of the odds with a lot of items that
// don't really like to be duplicated. Favor the player; high-rolling
// feels exciting, low-rolling feels punishing!
boolean reject = (filterweakitems) && (powers[i] + DISTVAR < meanreelpower);
// If we're far away from interactions, be extra aggressive about tossing attack items.
if (filterweakitems && !reject && !K_IsItemSpeed(i))
reject = (powers[i] + Easing_Linear(loneliness, DISTVAR, -2 * DISTVAR) < meanreelpower);
// Popcorn Super Ring is always strong enough, we put it there on purpose.
if (i == KITEM_SUPERRING && !canfiltersuperring)
reject = false;
// Before we actually apply that rejection, draw the simple odds debugger.
// This one is just to watch the distribution for vibes as you drive around.
if (cv_kartdebugdistribution.value && candidates[i])
{
UINT16 BASE_X = 280;
UINT16 BASE_Y = 5+12*debugcount;
INT32 FLAGS = V_SNAPTOTOP|V_SNAPTORIGHT;
V_DrawRightAlignedThinString(BASE_X - 12, 5, FLAGS, va("TP %d", targetpower/humanscaler));
V_DrawRightAlignedThinString(BASE_X - 12, 5+12, FLAGS, va("FB %d / %d", toFront, toBack));
V_DrawRightAlignedThinString(BASE_X - 12, 5+24, FLAGS, va("L %d / CF %d", loneliness, crowdingFirst));
V_DrawRightAlignedThinString(BASE_X - 12, 5+36, FLAGS, va("D %d / %d", roulette->preexpdist, roulette->dist));
for(UINT8 k = 0; k < candidates[i]; k++)
V_DrawFixedPatch((BASE_X + 3*k)*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(i), NULL);
UINT8 amount = K_ItemResultToAmount(i, roulette);
if (amount > 1)
V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount));
/*
if (reject)
V_DrawThinString(BASE_X, BASE_Y, FLAGS|V_60TRANS, va("WEAK"));
*/
debugcount++;
}
// Okay, apply the rejection now.
if (reject)
candidates[i] = 0;
// Bump totalSpawnChance, write that rolling counter, and move on.
spawnChance[i] = (
totalSpawnChance += candidates[i]
);
}
if (dryrun) // We're being called from the debugger on a view conditional!
return; // This is net unsafe if we do things with side effects. GTFO!
// == FINALLY ADD THIS SHIT TO THE REEL
// Super simple: generate a random index,
// count up until we hit that index,
// insert that item and decrement everything after.
while (totalSpawnChance > 0)
{
rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance);
for (i = 1; i < NUMKARTRESULTS && spawnChance[i] <= rngRoll; i++)
{
continue;
}
K_AddItemToReel(player, roulette, i);
for (; i < NUMKARTRESULTS; i++)
{
if (spawnChance[i] > 0)
{
spawnChance[i]--;
}
}
totalSpawnChance--;
}
K_FixEmptyRoulette(player, roulette);
}
/*--------------------------------------------------
void K_StartItemRoulette(player_t *const player)
See header file for description.
--------------------------------------------------*/
void K_StartItemRoulette(player_t *const player, boolean ringbox)
{
itemroulette_t *const roulette = &player->itemRoulette;
size_t i;
K_FillItemRoulette(player, roulette, ringbox);
if (roulette->autoroulette)
roulette->index = P_RandomRange(PR_AUTOROULETTE, 0, roulette->itemList.len - 1);
if (K_PlayerUsesBotMovement(player) == true)
{
K_BotPickItemPriority(player);
}
// Prevent further duplicates of items that
// are intended to only have one out at a time.
for (i = 0; i < roulette->itemList.len; i++)
{
kartitems_t item = roulette->itemList.items[i];
if (K_ItemSingularity(item) == true)
{
K_SetItemCooldown(item, TICRATE<<4);
}
}
}
/*--------------------------------------------------
void K_StartEggmanRoulette(player_t *const player)
See header file for description.
--------------------------------------------------*/
void K_StartEggmanRoulette(player_t *const player)
{
itemroulette_t *const roulette = &player->itemRoulette;
K_StartItemRoulette(player, false);
roulette->eggman = true;
}
/*--------------------------------------------------
void K_StopRoulette(itemroulette_t *const roulette)
See header file for description.
--------------------------------------------------*/
void K_StopRoulette(itemroulette_t *const roulette)
{
roulette->active = false;
roulette->eggman = false;
roulette->ringbox = false;
roulette->reserved = 0;
}
/*--------------------------------------------------
fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta, UINT8 fudge)
See header file for description.
--------------------------------------------------*/
fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta, UINT8 fudge)
{
const fixed_t curTic = (roulette->tics << FRACBITS) - renderDelta;
const fixed_t midTic = roulette->speed * (FRACUNIT >> 1);
fixed_t result = FixedMul(FixedDiv(midTic - curTic, ((roulette->speed + 1) << FRACBITS)), ROULETTE_SPACING);
if (fudge > 0)
{
result += (roulette->speed + 1) * fudge;
}
return result;
}
/*--------------------------------------------------
fixed_t K_GetSlotOffset(itemroulette_t *const roulette, fixed_t renderDelta)
See header file for description.
--------------------------------------------------*/
fixed_t K_GetSlotOffset(itemroulette_t *const roulette, fixed_t renderDelta, UINT8 fudge)
{
const fixed_t curTic = (roulette->tics << FRACBITS) - renderDelta;
const fixed_t midTic = roulette->speed * (FRACUNIT >> 1);
fixed_t result = FixedMul(FixedDiv(midTic - curTic, ((roulette->speed + 1) << FRACBITS)), SLOT_SPACING);
if (fudge > 0)
{
result += (roulette->speed + 1) * fudge;
}
return result;
}
/*--------------------------------------------------
void K_KartGetItemResult(player_t *const player, kartitems_t getitem)
See header file for description.
--------------------------------------------------*/
void K_KartGetItemResult(player_t *const player, kartitems_t getitem)
{
if (K_ItemSingularity(getitem) == true)
{
K_SetItemCooldown(getitem, 20*TICRATE);
}
player->botvars.itemdelay = TICRATE;
player->botvars.itemconfirm = 0;
player->itemtype = K_ItemResultToType(getitem);
UINT8 itemamount = K_ItemResultToAmount(getitem, &player->itemRoulette);
if (cv_kartdebugitem.value != KITEM_NONE && cv_kartdebugitem.value == player->itemtype && cv_kartdebugamount.value > 1)
itemamount = cv_kartdebugamount.value;
player->itemamount = itemamount;
if (player->itemtype == KITEM_SPB)
Obj_SPBEradicateCapsules();
}
/*--------------------------------------------------
void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd)
See header file for description.
--------------------------------------------------*/
void K_KartItemRoulette(player_t *const player, ticcmd_t *const cmd)
{
itemroulette_t *const roulette = &player->itemRoulette;
boolean confirmItem = false;
if (roulette->reserved > 0)
{
roulette->reserved--;
return;
}
// This makes the roulette cycle through items.
// If this isn't active, you shouldn't be here.
if (roulette->active == false)
{
return;
}
if (roulette->itemList.len == 0
#ifndef ITEM_LIST_SIZE
|| roulette->itemList.items == NULL
#endif
)
{
// Invalid roulette setup.
// Escape before we run into issues.
roulette->active = false;
return;
}
if (roulette->elapsed > TICRATE>>1) // Prevent accidental immediate item confirm
{
if (roulette->elapsed > TICRATE<<4 || (roulette->eggman && !roulette->autoroulette && roulette->elapsed > TICRATE*4))
{
// Waited way too long, forcefully confirm the item.
confirmItem = true;
}
else if (roulette->autoroulette)
{
// confirmItem = (roulette->speed > 15);
confirmItem = (roulette->elapsed >= TICRATE*2);
}
else
{
// We can stop our item when we choose.
confirmItem = !!(cmd->buttons & BT_ATTACK);
}
}
// 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 (confirmItem == true && ((roulette->autoroulette) || (player->itemflags & (IF_ITEMOUT|IF_EGGMANOUT|IF_USERINGS)) == 0))
{
if (roulette->eggman == true)
{
// FATASS JUMPSCARE instead of your actual item
player->eggmanexplode = 6*TICRATE;
//player->karthud[khud_itemblink] = TICRATE;
//player->karthud[khud_itemblinkmode] = 1;
//player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT, 0);
if (K_IsPlayingDisplayPlayer(player))
{
S_StartSound(NULL, sfx_itrole);
}
}
else
{
UINT8 baseFudge = player->cmd.latency; // max(0, player->cmd.latency - 2);
if (roulette->autoroulette)
{
baseFudge = 0; // We didn't manually stop this, you jackwagon
}
UINT8 fudgedDelay = baseFudge;
while (fudgedDelay > 0)
{
UINT8 gap = (roulette->speed - roulette->tics); // How long has the roulette been on this entry?
if (fudgedDelay > gap) // Did the roulette tick over in-flight?
{
fudgedDelay = fudgedDelay - gap; // We're compensating for this gap's worth of delay, so cut it down.
roulette->index = roulette->index == 0 ? roulette->itemList.len - 1 : roulette->index - 1; // Roll the roulette index back...
roulette->tics = 0; // And just in case our delay is SO high that a fast roulette needs to roll back again...
}
else
{
break;
}
}
// And one more nudge for the remaining delay.
roulette->tics = (roulette->tics + fudgedDelay) % roulette->speed;
INT32 finalItem = roulette->itemList.items[ roulette->index ];
if (roulette->ringbox == true)
{
player->ringboxdelay = TICRATE;
player->ringboxaward = finalItem;
player->karthud[khud_rouletteoffset] = K_GetSlotOffset(roulette, FRACUNIT, baseFudge);
}
else
{
K_KartGetItemResult(player, finalItem);
player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT, baseFudge);
}
player->karthud[khud_itemblink] = TICRATE;
player->karthud[khud_itemblinkmode] = 0;
if (K_IsPlayingDisplayPlayer(player))
{
if (roulette->ringbox)
{
// Hi modders! Boost your treble and Loudness Normalize to 0 LUFS.
// I'm a responsible audio engineer. -Tyron 2023-07-30
UINT8 volume = (finalItem > 2) ? (15 * finalItem + 60) : 80;
S_StartSoundAtVolume(NULL, ringboxsound[finalItem], volume);
}
else
S_StartSound(NULL, sfx_itrolf);
}
}
// We're done, disable the roulette
roulette->active = false;
return;
}
roulette->elapsed++;
/*
if (roulette->autoroulette && (roulette->elapsed % 5 == 0) && (roulette->elapsed > TICRATE))
roulette->speed++;
*/
if (roulette->tics == 0)
{
roulette->index = (roulette->index + 1) % roulette->itemList.len;
roulette->tics = roulette->speed;
// This makes the roulette produce the random noises.
roulette->sound = (roulette->sound + 1) % 8;
if (K_IsPlayingDisplayPlayer(player))
{
if (roulette->ringbox)
S_StartSound(NULL, sfx_s240);
else
S_StartSound(NULL, sfx_itrol1 + roulette->sound);
if (roulette->index == 0 && roulette->itemList.len > 1)
{
S_StartSound(NULL, sfx_kc50);
S_StartSound(NULL, sfx_kc50);
}
}
}
else
{
roulette->tics--;
}
}