RingRacers/src/k_roulette.c
toaster 241475155b Rename "Break the Capsules" to "Prison Break"
We had this collective consciousness bigbrain moment in VC together, and it can literally only happen in this branch because unlocks.pk3 is the only main-game asset that needs to change for it
Solves the big problem we had with mixing up Item Capsules and ~~Battle Capsules~~ PRISON EGGS
2023-03-12 20:17:46 +00:00

1541 lines
36 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2022 by Kart Krew
// Copyright (C) 2022 by Sally "TehRealSalt" Cochenour
//
// 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"
// 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)
static UINT8 K_KartItemOddsRace[NUMKARTRESULTS-1][8] =
{
{ 0, 0, 2, 3, 4, 0, 0, 0 }, // Sneaker
{ 0, 0, 0, 0, 0, 3, 4, 5 }, // Rocket Sneaker
{ 0, 0, 0, 0, 2, 5, 5, 7 }, // Invincibility
{ 2, 3, 1, 0, 0, 0, 0, 0 }, // Banana
{ 1, 2, 0, 0, 0, 0, 0, 0 }, // Eggman Monitor
{ 5, 5, 2, 2, 0, 0, 0, 0 }, // Orbinaut
{ 0, 4, 2, 1, 0, 0, 0, 0 }, // Jawz
{ 0, 3, 3, 2, 0, 0, 0, 0 }, // Mine
{ 3, 0, 0, 0, 0, 0, 0, 0 }, // Land Mine
{ 0, 0, 2, 2, 0, 0, 0, 0 }, // Ballhog
{ 0, 0, 0, 0, 0, 2, 4, 0 }, // Self-Propelled Bomb
{ 0, 0, 0, 0, 2, 5, 0, 0 }, // Grow
{ 0, 0, 0, 0, 0, 2, 4, 2 }, // Shrink
{ 1, 0, 0, 0, 0, 0, 0, 0 }, // Lightning Shield
{ 0, 1, 2, 1, 0, 0, 0, 0 }, // Bubble Shield
{ 0, 0, 0, 0, 0, 1, 3, 5 }, // Flame Shield
{ 3, 0, 0, 0, 0, 0, 0, 0 }, // Hyudoro
{ 0, 0, 0, 0, 0, 0, 0, 0 }, // Pogo Spring
{ 2, 1, 1, 0, 0, 0, 0, 0 }, // Super Ring
{ 0, 0, 0, 0, 0, 0, 0, 0 }, // Kitchen Sink
{ 3, 0, 0, 0, 0, 0, 0, 0 }, // Drop Target
{ 0, 0, 0, 1, 2, 2, 0, 0 }, // Garden Top
{ 0, 0, 0, 0, 0, 0, 0, 0 }, // Gachabom
{ 0, 0, 2, 3, 3, 1, 0, 0 }, // Sneaker x2
{ 0, 0, 0, 0, 4, 4, 4, 0 }, // Sneaker x3
{ 0, 1, 1, 0, 0, 0, 0, 0 }, // Banana x3
{ 0, 0, 1, 0, 0, 0, 0, 0 }, // Orbinaut x3
{ 0, 0, 0, 2, 0, 0, 0, 0 }, // Orbinaut x4
{ 0, 0, 1, 2, 1, 0, 0, 0 }, // Jawz x2
{ 0, 0, 0, 0, 0, 0, 0, 0 } // Gachabom x3
};
static UINT8 K_KartItemOddsBattle[NUMKARTRESULTS-1][2] =
{
{ 2, 1 }, // Sneaker
{ 0, 0 }, // Rocket Sneaker
{ 4, 1 }, // Invincibility
{ 0, 0 }, // Banana
{ 1, 0 }, // Eggman Monitor
{ 8, 0 }, // Orbinaut
{ 8, 1 }, // Jawz
{ 6, 1 }, // Mine
{ 2, 0 }, // Land Mine
{ 2, 1 }, // Ballhog
{ 0, 0 }, // Self-Propelled Bomb
{ 2, 1 }, // Grow
{ 0, 0 }, // Shrink
{ 4, 0 }, // Lightning Shield
{ 1, 0 }, // Bubble Shield
{ 1, 0 }, // Flame Shield
{ 2, 0 }, // Hyudoro
{ 3, 0 }, // Pogo Spring
{ 0, 0 }, // Super Ring
{ 0, 0 }, // Kitchen Sink
{ 2, 0 }, // Drop Target
{ 4, 0 }, // Garden Top
{ 0, 0 }, // Gachabom
{ 0, 0 }, // Sneaker x2
{ 0, 1 }, // Sneaker x3
{ 0, 0 }, // Banana x3
{ 2, 0 }, // Orbinaut x3
{ 1, 1 }, // Orbinaut x4
{ 5, 1 }, // Jawz x2
{ 0, 0 } // Gachabom x3
};
static UINT8 K_KartItemOddsSpecial[NUMKARTRESULTS-1][4] =
{
{ 1, 1, 0, 0 }, // Sneaker
{ 0, 0, 0, 0 }, // Rocket Sneaker
{ 0, 0, 0, 0 }, // Invincibility
{ 0, 0, 0, 0 }, // Banana
{ 0, 0, 0, 0 }, // Eggman Monitor
{ 1, 1, 0, 0 }, // Orbinaut
{ 1, 1, 0, 0 }, // Jawz
{ 0, 0, 0, 0 }, // Mine
{ 0, 0, 0, 0 }, // Land Mine
{ 0, 0, 0, 0 }, // Ballhog
{ 0, 0, 0, 1 }, // Self-Propelled Bomb
{ 0, 0, 0, 0 }, // Grow
{ 0, 0, 0, 0 }, // Shrink
{ 0, 0, 0, 0 }, // Lightning Shield
{ 0, 0, 0, 0 }, // Bubble Shield
{ 0, 0, 0, 0 }, // Flame Shield
{ 0, 0, 0, 0 }, // Hyudoro
{ 0, 0, 0, 0 }, // Pogo Spring
{ 0, 0, 0, 0 }, // Super Ring
{ 0, 0, 0, 0 }, // Kitchen Sink
{ 0, 0, 0, 0 }, // Drop Target
{ 0, 0, 0, 0 }, // Garden Top
{ 0, 0, 0, 0 }, // Gachabom
{ 0, 1, 1, 0 }, // Sneaker x2
{ 0, 0, 1, 1 }, // Sneaker x3
{ 0, 0, 0, 0 }, // Banana x3
{ 0, 1, 1, 0 }, // Orbinaut x3
{ 0, 0, 1, 1 }, // Orbinaut x4
{ 0, 0, 1, 1 }, // Jawz x2
{ 0, 0, 0, 0 } // Gachabom x3
};
static kartitems_t K_KartItemReelSpecialEnd[] =
{
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelTimeAttack[] =
{
KITEM_SNEAKER,
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelSPBAttack[] =
{
KITEM_GACHABOM,
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelBreakTheCapsules[] =
{
KITEM_GACHABOM,
KRITEM_TRIPLEGACHABOM,
KITEM_NONE
};
static kartitems_t K_KartItemReelBoss[] =
{
KITEM_ORBINAUT,
KITEM_BANANA,
KITEM_ORBINAUT,
KITEM_BANANA,
KITEM_GACHABOM,
KITEM_NONE
};
/*--------------------------------------------------
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;
}
}
}
/*--------------------------------------------------
static fixed_t K_ItemOddsScale(UINT8 playerCount)
A multiplier for odds and distances to scale
them with the player count.
Input Arguments:-
playerCount - Number of players in the game.
Return:-
Fixed point number, to multiply odds or
distances by.
--------------------------------------------------*/
static 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;
}
/*--------------------------------------------------
static 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.
--------------------------------------------------*/
static UINT32 K_UndoMapScaling(UINT32 distance)
{
if (mapobjectscale != FRACUNIT)
{
// Bring back to normal scale.
return FixedDiv(distance, mapobjectscale);
}
return distance;
}
/*--------------------------------------------------
static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers)
Adjust item distance for lobby-size scaling
as well as Frantic Items.
Input Arguments:-
distance - Original distance.
numPlayers - Number of players in the game.
Return:-
New distance after scaling.
--------------------------------------------------*/
static UINT32 K_ScaleItemDistance(UINT32 distance, UINT8 numPlayers)
{
if (franticitems == true)
{
// Frantic items pretends everyone's farther apart, for crazier items.
distance = FixedMul(distance, FRANTIC_ITEM_SCALE);
}
// Items get crazier with the fewer players that you have.
distance = FixedMul(
distance,
FRACUNIT + (K_ItemOddsScale(numPlayers) / 2)
);
return distance;
}
/*--------------------------------------------------
static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers)
Gets a player's distance used for the item
roulette, including all scaling factors.
Input Arguments:-
player - The player to get the distance of.
numPlayers - Number of players in the game.
Return:-
The player's finalized item distance.
--------------------------------------------------*/
static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers)
{
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);
pdis = K_ScaleItemDistance(pdis, numPlayers);
if (player->bot && player->botvars.rival)
{
// Rival has better odds :)
pdis = FixedMul(pdis, FRANTIC_ITEM_SCALE);
}
return pdis;
}
/*--------------------------------------------------
static boolean K_DenyShieldOdds(kartitems_t item)
Checks if this type of shield already exists in
another player's inventory.
Input Arguments:-
item - The item type of the shield.
Return:-
Whether this item is a shield and may not be awarded
at this time.
--------------------------------------------------*/
static boolean K_DenyShieldOdds(kartitems_t item)
{
INT32 shieldType = K_GetShieldFromItem(item);
if ((gametyperules & GTR_CIRCUIT) == 0)
{
return false;
}
switch (shieldType)
{
case KSHIELD_NONE:
/* Marble Garden Top is not REALLY
a Sonic 3 shield */
case KSHIELD_TOP:
{
break;
}
default:
{
size_t i;
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 fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position)
Adjust 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_AdjustSPBOdds(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;
const fixed_t maxOdds = 20 << FRACBITS;
fixed_t multiplier = FixedDiv(dist, distRange);
if (multiplier < 0)
{
multiplier = 0;
}
if (multiplier > FRACUNIT)
{
multiplier = FRACUNIT;
}
return FixedMul(maxOdds, multiplier);
}
}
typedef struct {
boolean powerItem;
boolean cooldownOnStart;
boolean notNearEnd;
// gameplay state
boolean rival; // player is a bot Rival
} itemconditions_t;
/*--------------------------------------------------
static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette)
Adjust item odds to certain group conditions.
Input Arguments:-
newOdds - The item odds to adjust.
conditions - The conditions state.
roulette - The roulette data that we intend to
insert this item into.
Return:-
New item odds.
--------------------------------------------------*/
static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette)
{
// None if this applies outside of Race modes (for now?)
if ((gametyperules & GTR_CIRCUIT) == 0)
{
return newOdds;
}
if ((conditions->cooldownOnStart == true) && (leveltime < (30*TICRATE) + starttime))
{
// This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items)
newOdds = 0;
}
else if ((conditions->notNearEnd == true) && (roulette != NULL && roulette->baseDist < ENDDIST))
{
// This item should not appear at the end of a race. (Usually trap items that lose their effectiveness)
newOdds = 0;
}
else if (conditions->powerItem == true)
{
// This item is a "power item". This activates "frantic item" toggle related functionality.
if (franticitems == true)
{
// First, power items multiply their odds by 2 if frantic items are on; easy-peasy.
newOdds *= 2;
}
if (conditions->rival == true)
{
// The Rival bot gets frantic-like items, also :p
newOdds *= 2;
}
if (roulette != NULL)
{
newOdds = FixedMul(newOdds, FRACUNIT + K_ItemOddsScale(roulette->playing));
}
}
return newOdds;
}
/*--------------------------------------------------
INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item)
See header file for description.
--------------------------------------------------*/
INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item)
{
boolean bot = false;
UINT8 position = 0;
itemconditions_t conditions = {
.powerItem = false,
.cooldownOnStart = false,
.notNearEnd = false,
.rival = false,
};
fixed_t newOdds = 0;
I_Assert(item > KITEM_NONE); // too many off by one scenarioes.
I_Assert(item < NUMKARTRESULTS);
if (player != NULL)
{
bot = player->bot;
conditions.rival = (bot == true && player->botvars.rival == true);
position = player->position;
}
if (K_ItemEnabled(item) == false)
{
return 0;
}
if (K_GetItemCooldown(item) > 0)
{
// Cooldown is still running, don't give another.
return 0;
}
/*
if (bot)
{
// TODO: Item use on bots should all be passed-in functions.
// Instead of manually inserting these, it should return 0
// for any items without an item use function supplied
switch (item)
{
case KITEM_SNEAKER:
break;
default:
return 0;
}
}
*/
(void)bot;
if (K_DenyShieldOdds(item))
{
return 0;
}
if (gametype == GT_BATTLE)
{
I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table
newOdds = K_KartItemOddsBattle[item-1][pos];
}
else if (specialstageinfo.valid == true)
{
I_Assert(pos < 4); // Ditto
newOdds = K_KartItemOddsSpecial[item-1][pos];
}
else
{
I_Assert(pos < 8); // Ditto
newOdds = K_KartItemOddsRace[item-1][pos];
}
newOdds <<= FRACBITS;
switch (item)
{
case KITEM_BANANA:
case KITEM_EGGMAN:
case KITEM_SUPERRING:
{
conditions.notNearEnd = true;
break;
}
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:
{
conditions.powerItem = true;
break;
}
case KITEM_HYUDORO:
case KRITEM_TRIPLEBANANA:
{
conditions.powerItem = true;
conditions.notNearEnd = true;
break;
}
case KITEM_INVINCIBILITY:
case KITEM_MINE:
case KITEM_GROW:
case KITEM_BUBBLESHIELD:
case KITEM_FLAMESHIELD:
{
conditions.cooldownOnStart = true;
conditions.powerItem = true;
break;
}
case KITEM_SPB:
{
conditions.cooldownOnStart = true;
conditions.notNearEnd = true;
if (roulette != NULL &&
(gametyperules & GTR_CIRCUIT) &&
specialstageinfo.valid == false)
{
newOdds = K_AdjustSPBOdds(roulette, position);
}
break;
}
case KITEM_SHRINK:
{
conditions.cooldownOnStart = true;
conditions.powerItem = true;
conditions.notNearEnd = true;
if (roulette != NULL &&
(gametyperules & GTR_CIRCUIT) &&
roulette->playing - 1 <= roulette->exiting)
{
return 0;
}
break;
}
case KITEM_LIGHTNINGSHIELD:
{
conditions.cooldownOnStart = true;
conditions.powerItem = true;
if ((gametyperules & GTR_CIRCUIT) && spbplace != -1)
{
return 0;
}
break;
}
default:
{
break;
}
}
if (newOdds == 0)
{
// Nothing else we want to do with odds matters at this point :p
return newOdds;
}
newOdds = FixedInt(FixedRound(K_AdjustItemOddsToConditions(newOdds, &conditions, roulette)));
return newOdds;
}
/*--------------------------------------------------
static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette)
Gets which item bracket the player is in.
This can be adjusted depending on which
items being turned off.
Input Arguments:-
player - The player the roulette is for.
roulette - The item roulette data.
Return:-
The item bracket the player is in, as an
index to the array.
--------------------------------------------------*/
static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulette)
{
UINT8 i;
UINT8 useOdds = 0;
UINT8 distTable[14];
UINT8 distLen = 0;
UINT8 totalSize = 0;
boolean oddsValid[8];
for (i = 0; i < 8; i++)
{
UINT8 j;
if (gametype == GT_BATTLE && i > 1)
{
oddsValid[i] = false;
continue;
}
else if (specialstageinfo.valid == true && i > 3)
{
oddsValid[i] = false;
continue;
}
for (j = 1; j < NUMKARTRESULTS; j++)
{
if (K_KartGetItemOdds(player, roulette, i, j) > 0)
{
break;
}
}
oddsValid[i] = (j < NUMKARTRESULTS);
}
#define SETUPDISTTABLE(odds, num) \
totalSize += num; \
if (oddsValid[odds]) \
for (i = num; i; --i) \
distTable[distLen++] = odds;
if (gametype == GT_BATTLE) // Battle Mode
{
useOdds = 0;
}
else
{
if (specialstageinfo.valid == true) // Special Stages
{
SETUPDISTTABLE(0,2);
SETUPDISTTABLE(1,2);
SETUPDISTTABLE(2,3);
SETUPDISTTABLE(3,1);
}
else
{
SETUPDISTTABLE(0,1);
SETUPDISTTABLE(1,1);
SETUPDISTTABLE(2,1);
SETUPDISTTABLE(3,2);
SETUPDISTTABLE(4,2);
SETUPDISTTABLE(5,3);
SETUPDISTTABLE(6,3);
SETUPDISTTABLE(7,1);
}
for (i = 0; i < totalSize; i++)
{
fixed_t pos = 0;
fixed_t dist = 0;
UINT8 index = 0;
if (i == totalSize-1)
{
useOdds = distTable[distLen - 1];
break;
}
pos = ((i << FRACBITS) * distLen) / totalSize;
dist = FixedMul(DISTVAR << FRACBITS, pos) >> FRACBITS;
index = FixedInt(FixedRound(pos));
if (roulette->dist <= (unsigned)dist)
{
useOdds = distTable[index];
break;
}
}
}
#undef SETUPDISTTABLE
return useOdds;
}
/*--------------------------------------------------
static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulette)
Determines special conditions where we want
to forcefully give the player an SPB.
Input Arguments:-
player - The player the roulette is for.
roulette - The item roulette data.
Return:-
true if we want to give the player a forced SPB,
otherwise false.
--------------------------------------------------*/
static 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 == NULL)
{
roulette->itemListCap = 8;
roulette->itemList = Z_Calloc(
sizeof(SINT8) * roulette->itemListCap,
PU_STATIC,
&roulette->itemList
);
if (roulette->itemList == NULL)
{
I_Error("Not enough memory for item roulette list\n");
}
}
#endif
roulette->itemListLen = 0;
roulette->index = 0;
roulette->useOdds = UINT8_MAX;
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;
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
}
}
/*--------------------------------------------------
static void K_PushToRouletteItemList(itemroulette_t *const roulette, kartitems_t item)
Pushes a new item to the end of the item
roulette's item list.
Input Arguments:-
roulette - The item roulette data to modify.
item - The item to push to the list.
Return:-
N/A
--------------------------------------------------*/
static void K_PushToRouletteItemList(itemroulette_t *const roulette, kartitems_t item)
{
#ifdef ITEM_LIST_SIZE
if (roulette->itemListLen >= 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 != NULL);
if (roulette->itemListLen >= roulette->itemListCap)
{
roulette->itemListCap *= 2;
roulette->itemList = Z_Realloc(
roulette->itemList,
sizeof(SINT8) * roulette->itemListCap,
PU_STATIC,
&roulette->itemList
);
if (roulette->itemList == NULL)
{
I_Error("Not enough memory for item roulette list\n");
}
}
#endif
roulette->itemList[ roulette->itemListLen ] = item;
roulette->itemListLen++;
}
/*--------------------------------------------------
static void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item)
Adds an item to a player's item reel. Unlike
pushing directly with K_PushToRouletteItemList,
this function handles special behaviors (like
padding with extra Super Rings).
Input Arguments:-
player - The player to add to the item roulette.
This is valid to be NULL.
roulette - The player's item roulette data.
item - The item to push to the list.
Return:-
N/A
--------------------------------------------------*/
static void K_AddItemToReel(const player_t *player, itemroulette_t *const roulette, kartitems_t item)
{
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 <= 0
&& player->position == 1
&& (gametyperules & GTR_SPHERES) == 0)
{
K_PushToRouletteItemList(roulette, KITEM_SUPERRING);
}
}
/*--------------------------------------------------
static void K_CalculateRouletteSpeed(itemroulette_t *const roulette)
Determines the speed for the item roulette,
adjusted for progress in the race and front
running.
Input Arguments:-
roulette - The item roulette data to modify.
Return:-
N/A
--------------------------------------------------*/
static void K_CalculateRouletteSpeed(itemroulette_t *const roulette)
{
fixed_t frontRun = 0;
fixed_t progress = 0;
fixed_t total = 0;
if (K_TimeAttackRules() == true)
{
// 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);
}
/*--------------------------------------------------
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette)
See header file for description.
--------------------------------------------------*/
void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette)
{
UINT32 spawnChance[NUMKARTRESULTS] = {0};
UINT32 totalSpawnChance = 0;
size_t rngRoll = 0;
UINT8 numItems = 0;
kartitems_t singleItem = KITEM_SAD;
size_t i;
K_InitRoulette(roulette);
if (player != NULL)
{
roulette->baseDist = K_UndoMapScaling(player->distancetofinish);
K_CalculateRouletteSpeed(roulette);
}
// 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 (gametyperules & GTR_BOSS)
{
for (i = 0; K_KartItemReelBoss[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelBoss[i]);
}
return;
}
else if (K_TimeAttackRules() == true)
{
kartitems_t *presetlist = K_KartItemReelTimeAttack;
// If the objective is not to go fast, it's to cause serious damage.
if (gametyperules & GTR_PRISONS)
{
presetlist = K_KartItemReelBreakTheCapsules;
}
else if (modeattacking & ATTACKING_SPB)
{
presetlist = K_KartItemReelSPBAttack;
}
for (i = 0; presetlist[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, presetlist[i]);
}
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);
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);
return;
}
// Special cases are all handled, we can now
// actually calculate actual item reels.
roulette->dist = K_GetItemRouletteDistance(player, roulette->playing);
roulette->useOdds = K_FindUseodds(player, roulette);
for (i = 1; i < NUMKARTRESULTS; i++)
{
spawnChance[i] = (
totalSpawnChance += K_KartGetItemOdds(player, roulette, roulette->useOdds, i)
);
}
if (totalSpawnChance == 0)
{
// This shouldn't happen, but if it does, early exit.
// Maybe can happen if you enable multiple items for
// another gametype, so we give the singleItem as a fallback.
K_AddItemToReel(player, roulette, singleItem);
return;
}
// Create the same item reel given the same inputs.
P_SetRandSeed(PR_ITEM_ROULETTE, ITEM_REEL_SEED);
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++)
{
// Be sure to fix the remaining items' odds too.
if (spawnChance[i] > 0)
{
spawnChance[i]--;
}
}
totalSpawnChance--;
}
}
/*--------------------------------------------------
void K_StartItemRoulette(player_t *const player)
See header file for description.
--------------------------------------------------*/
void K_StartItemRoulette(player_t *const player)
{
itemroulette_t *const roulette = &player->itemRoulette;
size_t i;
K_FillItemRouletteData(player, roulette);
// Make the bots select their item after a little while.
// One of the few instances of bot RNG, would be nice to remove it.
player->botvars.itemdelay = P_RandomRange(PR_UNDEFINED, TICRATE, TICRATE*3);
// Prevent further duplicates of items that
// are intended to only have one out at a time.
for (i = 0; i < roulette->itemListLen; i++)
{
kartitems_t item = roulette->itemList[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);
roulette->eggman = true;
}
/*--------------------------------------------------
fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta)
See header file for description.
--------------------------------------------------*/
fixed_t K_GetRouletteOffset(itemroulette_t *const roulette, fixed_t renderDelta)
{
const fixed_t curTic = (roulette->tics << FRACBITS) - renderDelta;
const fixed_t midTic = roulette->speed * (FRACUNIT >> 1);
return FixedMul(FixedDiv(midTic - curTic, ((roulette->speed + 1) << FRACBITS)), ROULETTE_SPACING);
}
/*--------------------------------------------------
static void K_KartGetItemResult(player_t *const player, kartitems_t getitem)
Initializes a player's item to what was
received from the roulette.
Input Arguments:-
player - The player receiving the item.
getitem - The item to give to the player.
Return:-
N/A
--------------------------------------------------*/
static 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);
player->itemamount = K_ItemResultToAmount(getitem);
}
/*--------------------------------------------------
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;
// This makes the roulette cycle through items.
// If this isn't active, you shouldn't be here.
if (roulette->active == false)
{
return;
}
if (roulette->itemListLen == 0
#ifndef ITEM_LIST_SIZE
|| roulette->itemList == 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->elapsed > TICRATE*4))
{
// Waited way too long, forcefully confirm the item.
confirmItem = true;
}
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 && (player->pflags & (PF_ITEMOUT|PF_EGGMANOUT|PF_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);
if (P_IsDisplayPlayer(player) && !demo.freecam)
{
S_StartSound(NULL, sfx_itrole);
}
}
else
{
// D2 fudge factor. Roulette was originally designed and tested with this delay.
UINT8 fudgedDelay = (player->cmd.latency <= 2) ? 0 : player->cmd.latency - 2;
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->itemListLen - 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;
kartitems_t finalItem = roulette->itemList[ roulette->index ];
K_KartGetItemResult(player, finalItem);
player->karthud[khud_itemblink] = TICRATE;
player->karthud[khud_itemblinkmode] = 0;
player->karthud[khud_rouletteoffset] = K_GetRouletteOffset(roulette, FRACUNIT);
if (P_IsDisplayPlayer(player) && !demo.freecam)
{
S_StartSound(NULL, sfx_itrolf);
}
}
// We're done, disable the roulette
roulette->active = false;
return;
}
roulette->elapsed++;
if (roulette->tics == 0)
{
roulette->index = (roulette->index + 1) % roulette->itemListLen;
roulette->tics = roulette->speed;
// This makes the roulette produce the random noises.
roulette->sound = (roulette->sound + 1) % 8;
if (P_IsDisplayPlayer(player) && !demo.freecam)
{
S_StartSound(NULL, sfx_itrol1 + roulette->sound);
}
}
else
{
roulette->tics--;
}
}