RingRacers/src/k_roulette.c
Oni b9bbb6cb8a Merge branch 'conditions-cascading' into 'master'
Conditions Cascading

Closes #366

See merge request KartKrew/Kart!1053
2023-03-23 23:51:30 +00:00

1531 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)
{
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 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--;
}
}