mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
2131 lines
56 KiB
C
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--;
|
|
}
|
|
}
|